implement history graph

This commit is contained in:
4cuck committed 2023-11-07 03:17:00 +01:00
1 parent 1a84ba758d
commit 76d9e85b74
15 files changed
+421 -123

No files matched your search

Generated
+3
View File
@@ -1057,8 +1057,11 @@ name = "entities"
version = "0.1.0"
dependencies = [
"chrono",
"migration",
"sea-orm",
"sea-query",
"serde",
"tokio",
]
[[package]]
+7
View File
@@ -11,4 +11,11 @@ path = "src/mod.rs"
[dependencies]
chrono = { workspace = true }
sea-orm = { workspace = true }
sea-query = { workspace = true }
serde = { workspace = true, features = ["derive"] }
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
[dev-dependencies.migration]
path = "../migration"
+60 -1
View File
@@ -1,8 +1,11 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use sea_orm::entity::prelude::*;
use sea_orm::{entity::prelude::*, FromQueryResult};
use sea_query::{Query, Order, Alias, SimpleExpr};
use serde::Serialize;
use crate::check_errors;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize)]
#[sea_orm(table_name = "health_check")]
pub struct Model {
@@ -34,3 +37,59 @@ impl Related<super::host::Entity> for Entity {
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, FromQueryResult, Serialize)]
pub struct HealthyAmount {
pub time: i64,
pub alive: i64,
pub dead: i64,
}
impl HealthyAmount {
pub async fn fetch(db: &DatabaseConnection, from: DateTimeUtc, to: DateTimeUtc, hosts: Option<&[i32]>) -> Result<Vec<HealthyAmount>,DbErr> {
let builder = db.get_database_backend();
let mut stmt: sea_query::SelectStatement = Query::select();
stmt.column(self::Column::Time)
.expr_as(SimpleExpr::Custom("SUM(healthy)".to_string()), Alias::new("alive"))
.expr_as(SimpleExpr::Custom("SUM(1-healthy)".to_string()), Alias::new("dead"))
.group_by_col(self::Column::Time)
.from(self::Entity)
.and_where(self::Column::Time.between(from.timestamp(), to.timestamp()));
if let Some(hosts) = hosts {
stmt.and_where(self::Column::Host.is_in(hosts.iter().map(|v| *v)));
}
stmt.group_by_col(self::Column::Time)
.order_by(self::Column::Time, Order::Asc);
HealthyAmount::find_by_statement(builder.build(&stmt)).all(db).await
}
}
#[cfg(test)]
mod test {
use super::*;
use chrono::Utc;
use migration::MigratorTrait;
use sea_orm::{ConnectOptions, Database};
pub(crate) async fn db_init() -> DatabaseConnection {
let db = Database::connect(ConnectOptions::new(
"sqlite:./test_db.db?mode=rwc".to_owned(),
))
.await
.unwrap();
migration::Migrator::up(&db, None).await.unwrap();
db
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
#[ignore]
async fn test_fetch_instance_list() {
let db = db_init().await;
let time_now = Utc::now();
let time_3h = time_now
.checked_sub_signed(chrono::Duration::days(14))
.unwrap();
let hosts = vec![1,2,3];
dbg!(HealthyAmount::fetch(&db, time_3h, time_now,Some(&hosts)).await.unwrap());
}
}
+45 -9
View File
@@ -8,6 +8,8 @@ use axum::response::IntoResponse;
use axum::response::Redirect;
use axum::Form;
use axum::Json;
use chrono::DateTime;
use chrono::Utc;
use constant_time_eq::constant_time_eq;
use entities::check_errors;
use entities::health_check;
@@ -39,6 +41,7 @@ use crate::ServerError;
use crate::ADMIN_OVERVIEW_URL;
use crate::LOGIN_URL;
/// Stored session login information
#[derive(Serialize, Deserialize, Default)]
pub struct ActiveLogin {
/// Hosts this session has access to.
@@ -183,6 +186,7 @@ async fn login_inner(
}
}
/// Admin login form
#[derive(Deserialize, Debug)]
pub struct LoginInput {
domain: String,
@@ -191,12 +195,20 @@ pub struct LoginInput {
verification_method: VerificationMethod,
}
/// Part of the admin login form
#[derive(Serialize, Deserialize, Debug)]
enum VerificationMethod {
DNS,
HTTP,
}
/// Json passed to select a date range
#[derive(Deserialize, Debug)]
pub struct DateRangeInput {
start: DateTime<Utc>,
end: DateTime<Utc>,
}
async fn fetch_host_txt(
instance_url: &str,
client: &Client,
@@ -278,7 +290,7 @@ pub async fn overview(
) -> Result<axum::response::Response> {
tracing::info!(?session);
let (login, hosts) = get_all_login_hosts(&session, db).await?;
let (login, hosts) = get_all_login_hosts(&session, db,true).await?;
let mut context = tera::Context::new();
let res = {
@@ -300,15 +312,37 @@ pub async fn overview(
pub async fn history_json(
State(ref db): State<DatabaseConnection>,
Path(host): Path<i32>,
session: Session,
Json(input): Json<DateRangeInput>,
) -> Result<axum::response::Response> {
#[derive(Debug, Serialize)]
struct ReturnData {
pub global: Vec<health_check::HealthyAmount>,
pub user: Vec<health_check::HealthyAmount>,
}
let (_login, hosts) = get_all_login_hosts(&session, db, false).await?;
let host_ids: Vec<_> = hosts.iter().map(|host|host.id).collect();
let data_owned = health_check::HealthyAmount::fetch(db, input.start, input.end,Some(&host_ids)).await?;
let data_global = health_check::HealthyAmount::fetch(db, input.start, input.end,None).await?;
Ok(Json(ReturnData {
global: data_global,
user: data_owned,
}).into_response())
}
pub async fn history_json_specific(
State(ref db): State<DatabaseConnection>,
session: Session,
Path(host): Path<i32>,
Json(input): Json<DateRangeInput>,
) -> Result<axum::response::Response> {
let host = get_specific_login_host(host, &session, db).await?;
let history: Vec<health_check::Model> = health_check::Entity::find()
.filter(health_check::Column::Host.eq(host.id))
.order_by_asc(health_check::Column::Time)
.limit(20)
.filter(health_check::Column::Time.between(input.start.timestamp(), input.end.timestamp()))
.all(db)
.await?;
@@ -343,16 +377,16 @@ pub async fn history_view(
Ok(res)
}
pub async fn errors_view(
pub async fn instance_view(
State(ref app_state): State<AppState>,
State(ref template): State<Arc<tera::Tera>>,
State(ref db): State<DatabaseConnection>,
Path(host): Path<i32>,
Path(instance): Path<i32>,
session: Session,
) -> Result<axum::response::Response> {
tracing::info!(?session);
let host = get_specific_login_host(host, &session, db).await?;
let host = get_specific_login_host(instance, &session, db).await?;
let errors = check_errors::Entity::find()
.filter(check_errors::Column::Host.eq(host.id))
@@ -371,22 +405,24 @@ pub async fn errors_view(
context.insert("last_updated", &time);
context.insert("ERRORS", &errors);
context.insert("HOST_DOMAIN", &host.domain);
context.insert("HOST_ID", &instance);
let res = Html(template.render("errors_admin.html.j2", &context)?).into_response();
let res = Html(template.render("instance_admin.html.j2", &context)?).into_response();
drop(guard);
res
};
Ok(res)
}
/// Get all [host::Model] for current [Session]
/// Get all [host::Model] for current [Session], optionally return all hosts for admins
async fn get_all_login_hosts(
session: &Session,
db: &DatabaseConnection,
return_admin_hosts: bool,
) -> Result<(ActiveLogin, Vec<host::Model>)> {
let login = get_session_login(&session)?;
let host_res = match login.admin {
let host_res = match login.admin && return_admin_hosts {
true => {
host::Entity::find()
.filter(host::Column::Enabled.eq(true))
+4 -3
View File
@@ -6,7 +6,7 @@ use axum::{
extract::DefaultBodyLimit,
http::HeaderValue,
response::{Html, Redirect},
routing::{get, get_service},
routing::{get, get_service, post},
BoxError, Router,
};
use chrono::TimeZone;
@@ -147,9 +147,10 @@ pub async fn start(
.route("/api/v1/instances", get(api::instances))
.nest(ADMIN_OVERVIEW_URL, Router::new()
.route("/", get(admin::overview))
.route("/errors/:host", get(admin::errors_view))
.route("/instance/:instance", get(admin::instance_view))
// .route("/history/:host", get(admin::history_view))
// .route("/api/history", get(admin::history_json))
.route("/api/history/:instance", post(admin::history_json_specific))
.route("/api/history", post(admin::history_json))
.route("/login", get(admin::login_view).post(admin::login).route_layer(rate_limit_layer))
.route("/logout", get(admin::logout))
// .layer(ServiceBuilder::new().layer(SetResponseHeaderLayer::overriding(header::CACHE_CONTROL, "must-revalidate")))
+111
View File
@@ -0,0 +1,111 @@
document.addEventListener("DOMContentLoaded", function() {
const startDateInput = document.getElementById('startDate');
const endDateInput = document.getElementById('endDate');
const submitTime = document.getElementById('submitDateRange');
// Set initial values in UTC
const initialEndDate = moment.utc();
const initialStartDate = moment().subtract(30, 'days').utc();
startDateInput.value = initialStartDate.format('YYYY-MM-DD');
endDateInput.value = initialEndDate.format('YYYY-MM-DD');;
submitTime.addEventListener('click', function() {
const startDate = moment(startDateInput.value, 'YYYY-MM-DD');
const endDate = moment(endDateInput.value, 'YYYY-MM-DD');
if (!startDate.isValid() || !endDate.isValid()) {
alert('Invalid date format. Please use the YYYY-MM-DD format.');
} else if (startDate.isAfter(endDate)) {
alert('Invalid date range. Start date must be before the end date.');
} else {
fetchDataAndCreateChart(startDate,endDate);
}
});
fetchDataAndCreateChart(initialStartDate,initialEndDate);
});
let chart = undefined;
async function fetchDataAndCreateChart(startDate,endDate) {
let data;
try {
const response = await fetch('/admin/api/history', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({"start": startDate.utc(), "end": endDate.add(1, 'days').utc()})
});
data = await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
}
// Extracting data for chart
const timestamps = data.global.map(entry => entry.time * 1000); // Converting UNIX timestamps to milliseconds
const healthyData = data.global.map(entry => entry.alive);
const unhealthyData = data.global.map(entry => entry.dead);
const userUnhealthyData = data.user.filter(entry => entry.dead > 0).map(entry => ({"x": entry.time * 1000, "y": entry.dead}));
const paramData = {"global": {"healthyData": healthyData, "unhealthyData": unhealthyData}, "user": userUnhealthyData};
console.log(paramData);
createChart(timestamps, paramData);
}
function createChart(timestamps, data) {
const ctx = document.getElementById('graph').getContext('2d');
let high_data = timestamps.length > 1000;
if (chart) {
chart.destroy();
}
chart = new Chart(ctx, {
type: 'line',
data: {
labels: timestamps,
datasets: [
{
label: 'Healthy',
data: data.global.healthyData,
borderColor: 'green',
backgroundColor: 'rgba(0, 255, 0, 0.2)',
fill: true
},
{
label: 'Unhealthy',
data: data.global.unhealthyData,
borderColor: 'orange',
backgroundColor: 'rgba(241, 90, 34, 0.2)',
fill: true
},
{
label: 'Own Instances Unhealthy',
data: data.user,
pointRadius: 5,
pointBackgroundColor: 'rgba(255, 0, 0, 1)',
fill: false,
datasetIndex: "x",
}
]
},
options: {
plugins: {
decimation: {
enabled: high_data,
algorithm: 'min-max',
},
},
animation: !high_data,
scales: {
x: {
type: 'time',
time: {
unit: 'day' // You might want to adjust this based on your data resolution
},
autoSkip: true,
},
y: {
beginAtZero: true
}
}
}
});
}
-1
View File
@@ -1 +0,0 @@
import{r as requestAnimFrame,a as resolve,e as effects,c as color,i as isObject,d as defaults,b as isArray,v as valueOrDefault,u as unlistenArrayEvents,l as listenArrayEvents,f as resolveObjectKey,g as isNumberFinite,h as defined,s as sign,j as createContext,k as isNullOrUndef,_ as _arrayUnique,t as toRadians,m as toPercentage,n as toDimension,T as TAU,o as formatNumber,p as _angleBetween,H as HALF_PI,P as PI,q as _getStartAndCountOfVisiblePoints,w as _scaleRangesChanged,x as isNumber,y as _parseObjectDataRadialScale,z as getRelativePosition,A as _rlookupByKey,B as _lookupByKey,C as _isPointInArea,D as getAngleFromPoint,E as toPadding,F as each,G as getMaximumSize,I as _getParentNode,J as readUsedSize,K as supportsEventListenerOptions,L as throttled,M as _isDomSupported,N as _factorize,O as finiteOrDefault,Q as callback,R as _addGrace,S as _limitValue,U as toDegrees,V as _measureText,W as _int16Range,X as _alignPixel,Y as clipArea,Z as renderText,$ as unclipArea,a0 as toFont,a1 as _toLeftRightCenter,a2 as _alignStartEnd,a3 as overrides,a4 as merge,a5 as _capitalize,a6 as descriptors,a7 as isFunction,a8 as _attachContext,a9 as _createResolver,aa as _descriptors,ab as mergeIf,ac as uid,ad as debounce,ae as retinaScale,af as clearCanvas,ag as setsEqual,ah as _elementsEqual,ai as _isClickEvent,aj as _isBetween,ak as _readValueToProps,al as _updateBezierControlPoints,am as _computeSegments,an as _boundSegments,ao as _steppedInterpolation,ap as _bezierInterpolation,aq as _pointInLine,ar as _steppedLineTo,as as _bezierCurveTo,at as drawPoint,au as addRoundedRectPath,av as toTRBL,aw as toTRBLCorners,ax as _boundSegment,ay as _normalizeAngle,az as getRtlAdapter,aA as overrideTextDirection,aB as _textX,aC as restoreTextDirection,aD as drawPointLegend,aE as distanceBetweenPoints,aF as noop,aG as _setMinAndMaxByKey,aH as niceNum,aI as almostWhole,aJ as almostEquals,aK as _decimalPlaces,aL as Ticks,aM as log10,aN as _longestText,aO as _filterBetween,aP as _lookup}from"./chunks/helpers.segment.js";import"@kurkle/color";class Animator{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(e,i,a,t){const s=i.listeners[t],n=i.duration;s.forEach(t=>t({chart:e,initial:i.initial,numSteps:n,currentStep:Math.min(a-i.start,n)}))}_refresh(){this._request||(this._running=!0,this._request=requestAnimFrame.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(r=Date.now()){let o=0;this._charts.forEach((a,s)=>{if(a.running&&a.items.length){const n=a.items;let t=n.length-1,e=!1,i;for(;0<=t;--t)(i=n[t])._active?(i._total>a.duration&&(a.duration=i._total),i.tick(r),e=!0):(n[t]=n[n.length-1],n.pop());e&&(s.draw(),this._notify(s,a,r,"progress")),n.length||(a.running=!1,this._notify(s,a,r,"complete"),a.initial=!1),o+=n.length}}),this._lastDate=r,0===o&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return 0<this._getAnims(t).items.length}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce((t,e)=>Math.max(t,e._duration),0),this._refresh())}running(t){if(!this._running)return!1;t=this._charts.get(t);return!!(t&&t.running&&t.items.length)}stop(e){const i=this._charts.get(e);if(i&&i.items.length){const a=i.items;let t=a.length-1;for(;0<=t;--t)a[t].cancel();i.items=[],this._notify(e,i,Date.now(),"complete")}}remove(t){return this._charts.delete(t)}}var animator=new Animator;const transparent="transparent",interpolators={boolean(t,e,i){return.5<i?e:t},color(t,e,i){t=color(t||transparent);const a=t.valid&&color(e||transparent);return a&&a.valid?a.mix(t,i).hexString():e},number(t,e,i){return t+(e-t)*i}};class Animation{constructor(t,e,i,a){var s=e[i],s=(a=resolve([t.to,a,s,t.from]),resolve([t.from,s,a]));this._active=!0,this._fn=t.fn||interpolators[t.type||typeof s],this._easing=effects[t.easing]||effects.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=s,this._to=a,this._promises=void 0}active(){return this._active}update(t,e,i){var a,s,n;this._active&&(this._notify(!1),a=this._target[this._prop],s=i-this._start,n=this._duration-s,this._start=i,this._duration=Math.floor(Math.max(n,t.duration)),this._total+=s,this._loop=!!t.loop,this._to=resolve([t.to,e,a,t.from]),this._from=resolve([t.from,a,e]))}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){var t=t-this._start,e=this._duration,i=this._prop,a=this._from,s=this._loop,n=this._to;let r;if(this._active=a!==n&&(s||t<e),!this._active)return this._target[i]=n,void this._notify(!0);t<0?this._target[i]=a:(r=t/e%2,r=s&&1<r?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[i]=this._fLine truncated
+20
View File
@@ -0,0 +1,20 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/chart.js@4.4.0/dist/chart.umd.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
/*!
* Chart.js v4.4.0
* https://www.chartjs.org
* (c) 2023 Chart.js Contributors
* Released under the MIT License
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;a<r;a++)e.call(i,t[a],a);else if(o(t))for(l=Object.keys(t),r=l.length,a=0;a<r;a++)e.call(i,t[l[a]],l[a])}function f(t,e){let i,s,n,o;if(!t||!e||t.length!==e.length)return!1;for(i=0,s=t.length;i<s;++i)if(n=t[i],o=e[i],n.datasetIndex!==o.datasetIndex||n.index!==o.index)return!1;return!0}function g(t){if(n(t))return t.map(g);if(o(t)){const e=Object.create(null),i=Object.keys(t),s=i.length;let n=0;for(;n<s;++n)e[i[n]]=g(t[i[n]]);return e}return t}function p(t){return-1===["__proto__","prototype","constructor"].indexOf(t)}function m(t,e,i,s){if(!p(t))return;const n=e[t],a=i[t];o(n)&&o(a)?b(n,a,s):e[t]=g(a)}function b(t,e,i){const s=n(e)?e:[e],a=s.length;if(!o(t))return t;const r=(i=i||{}).merger||m;let l;for(let e=0;e<a;++e){if(l=s[e],!o(l))continue;const n=Object.keys(l);for(let e=0,s=n.length;e<s;++e)r(n[e],t,l,i)}return t}function x(t,e){return b(t,e,{merger:_})}function _(t,e,i){if(!p(t))return;const s=e[t],n=i[t];o(s)&&o(n)?x(s,n):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=g(n))}const y={"":t=>t,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)<i}function B(t){const e=Math.round(t);t=V(t,e,t/1e3)?e:t;const i=Math.pow(10,Math.floor(z(t))),s=t/i;return(s<=1?1:s<=2?2:s<=5?5:10)*i}function W(t){const e=[],i=Math.sqrt(t);let s;for(s=1;s<i;s++)t%s==0&&(e.push(s),e.push(t/s));return i===(0|i)&&e.push(i),e.sort(((t,e)=>t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;s<n;s++)o=t[s][i],isNaN(o)||(e.min=Math.min(e.min,o),e.max=Math.max(e.max,o))}function $(t){return t*(C/180)}function Y(t){return t*(180/C)}function U(t){if(!a(t))return;let e=1,i=0;for(;Math.round(t*e)/e!==t;)e*=10,i++;return i}function X(t,e){const i=e.x-t.x,s=e.y-t.y,n=Math.sqrt(i*i+s*s);let o=Math.atan2(s,i);return o<-.5*C&&(o+=O),{angle:o,distance:n}}function q(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function K(t,e){return(t-e+A)%O-C}function G(t){return(t%O+O)%O}function Z(t,e,i,s){const n=G(t),o=G(e),a=G(i),r=G(o-n),l=G(a-n),h=G(n-o),c=G(n-a);return n===o||n===a||s&&o===a||r>l&&h<c}function J(t,e,i){return Math.max(e,Math.min(i,t))}function Q(t){return J(t,-32768,32767)}function tt(t,e,i,s=1e-6){return t>=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]<e);let s,n=t.length-1,o=0;for(;n-o>1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return n<i||n===i&&t[s+1][e]===i}:s=>t[s][e]<i),st=(t,e,i)=>et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;s<n&&t[s]<e;)s++;for(;n>s&&t[n-1]>i;)n--;return s>0||n<t.length?t.slice(s,n):t}const ot=["push","pop","shift","splice","unshift"];function at(t,e){t._chartjs?t._chartjs.listeners.push(e):(Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),ot.forEach((e=>{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){Line truncated
/*!
* @kurkle/color v0.3.2
* https://github.com/kurkle/color#readme
* (c) 2023 Jukka Kurkela
* Released under the MIT License
*/function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e<i?6:0):e===n?(i-t)/s+2:(t-e)/s+4}(e,i,s,h,n),r=60*r+.5),[0|r,l||0,a]}function zt(t,e,i,s){return(Array.isArray(e)?t(e[0],e[1],e[2]):t(e,i,s)).map(Mt)}function Ft(t,e,i){return zt(Lt,t,e,i)}function Vt(t){return(t%360+360)%360}function Bt(t){const e=Tt.exec(t);let i,s=255;if(!e)return;e[5]!==i&&(s=e[6]?vt(+e[5]):Mt(+e[5]));const n=Vt(+e[2]),o=+e[3]/100,a=+e[4]/100;return i="hwb"===e[1]?function(t,e,i){return zt(Rt,t,e,i)}(n,o,a):"hsv"===e[1]?function(t,e,i){return zt(Et,t,e,i)}(n,o,a):Ft(n,o,a),{r:i[0],g:i[1],b:i[2],a:s}}const Wt={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Nt={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};let Ht;function jt(t){Ht||(Ht=function(){const t={},e=Object.keys(Nt),i=Object.keys(Wt);let s,n,o,a,r;for(s=0;s<e.length;s++){for(a=r=e[s],n=0;n<i.length;n++)o=i[n],r=r.replace(o,Wt[o]);o=parseInt(Nt[a],16),t[r]=[o>>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),Line truncated
//# sourceMappingURL=chart.umd.js.map
@@ -0,0 +1,7 @@
/*!
* chartjs-adapter-moment v1.0.1
* https://www.chartjs.org
* (c) 2022 chartjs-adapter-moment Contributors
* Released under the MIT license
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("moment"),require("chart.js")):"function"==typeof define&&define.amd?define(["moment","chart.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).moment,e.Chart)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var f=n(e);const a={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};t._adapters._date.override("function"==typeof f.default?{_id:"moment",formats:function(){return a},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=f.default(e,t):e instanceof f.default||(e=f.default(e)),e.isValid()?e.valueOf():null},format:function(e,t){return f.default(e).format(t)},add:function(e,t,n){return f.default(e).add(t,n).valueOf()},diff:function(e,t,n){return f.default(e).diff(f.default(t),n)},startOf:function(e,t,n){return e=f.default(e),"isoWeek"===t?(n=Math.trunc(Math.min(Math.max(0,n),6)),e.isoWeekday(n).startOf("day").valueOf()):e.startOf(t).valueOf()},endOf:function(e,t){return f.default(e).endOf(t).valueOf()}}:{})}));
+106
View File
@@ -0,0 +1,106 @@
let chart = undefined;
document.addEventListener("DOMContentLoaded", function() {
const startDateInput = document.getElementById('startDate');
const endDateInput = document.getElementById('endDate');
const submitTime = document.getElementById('submitDateRange');
// Set initial values in UTC
const initialEndDate = moment.utc();
const initialStartDate = moment().subtract(14, 'days').utc();
startDateInput.value = initialStartDate.format('YYYY-MM-DD');
endDateInput.value = initialEndDate.format('YYYY-MM-DD');;
submitTime.addEventListener('click', function() {
const startDate = moment(startDateInput.value, 'YYYY-MM-DD');
const endDate = moment(endDateInput.value, 'YYYY-MM-DD');
if (!startDate.isValid() || !endDate.isValid()) {
alert('Invalid date format. Please use the YYYY-MM-DD format.');
} else if (startDate.isAfter(endDate)) {
alert('Invalid date range. Start date must be before the end date.');
} else {
fetchDataAndCreateChart(startDate,endDate);
}
});
fetchDataAndCreateChart(initialStartDate,initialEndDate);
});
async function fetchDataAndCreateChart(startDate,endDate) {
let jsonData;
try {
const response = await fetch('/admin/api/history/'+document.getElementById('graph').getAttribute('data-host'), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({"start": startDate.utc(), "end": endDate.add(1, 'days').utc()})
});
jsonData = await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
}
if (jsonData) {
createChart(jsonData);
}
}
function createChart(jsonData) {
const timeLabels = jsonData.map(dataPoint => new Date(dataPoint.time * 1000));
console.log(timeLabels);
const healthyData = jsonData.map(dataPoint => dataPoint.healthy ? (dataPoint.resp_time ? dataPoint.resp_time : 0) : null);
const unhealthyData = jsonData.map(dataPoint => dataPoint.healthy ? null : (dataPoint.resp_time ? dataPoint.resp_time : 0));
const ctx = document.getElementById('graph').getContext('2d');
if (chart) {
chart.destroy();
}
chart = new Chart(ctx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [
{
label: 'Healthy',
data: healthyData,
borderColor: 'green',
fill: false,
},
{
label: 'Unhealthy',
data: unhealthyData,
borderColor: 'red',
fill: false,
},
]
},
options: {
scales: {
x: {
type: 'time',
// time: {
// // unit: 'hour'
// },
scaleLabel: {
display: true,
labelString: 'Time'
}
},
y: {
scaleLabel: {
display: true,
labelString: 'Response Time'
}
}
}
}
});
document.getElementById('graph').onclick = function (evt) {
const activePoints = chart.getElementsAtEventForMode(evt, 'index', {intersect: true});
if (activePoints.length > 0) {
const timestamp = jsonData[activePoints[0]._index].time;
document.getElementById("error_"+timestamp).scrollIntoView();
}
};
}
+15
View File
@@ -0,0 +1,15 @@
/**
* Minified by jsDelivr using Terser v5.14.1.
* Original file: /npm/moment@2.29.4/moment.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
//! moment.js
//! version : 2.29.4
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
//! license : MIT
//! momentjs.com
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,(function(){"use strict";var e,t;function n(){return e.apply(null,arguments)}function s(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function i(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function r(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function a(e){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(e).length;var t;for(t in e)if(r(e,t))return!1;return!0}function o(e){return void 0===e}function u(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function l(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function h(e,t){var n,s=[],i=e.length;for(n=0;n<i;++n)s.push(t(e[n],n));return s}function d(e,t){for(var n in t)r(t,n)&&(e[n]=t[n]);return r(t,"toString")&&(e.toString=t.toString),r(t,"valueOf")&&(e.valueOf=t.valueOf),e}function c(e,t,n,s){return Dt(e,t,n,s,!0).utc()}function f(e){return null==e._pf&&(e._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidEra:null,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],era:null,meridiem:null,rfc2822:!1,weekdayMismatch:!1}),e._pf}function m(e){if(null==e._isValid){var n=f(e),s=t.call(n.parsedDateParts,(function(e){return null!=e})),i=!isNaN(e._d.getTime())&&n.overflow<0&&!n.empty&&!n.invalidEra&&!n.invalidMonth&&!n.invalidWeekday&&!n.weekdayMismatch&&!n.nullInput&&!n.invalidFormat&&!n.userInvalidated&&(!n.meridiem||n.meridiem&&s);if(e._strict&&(i=i&&0===n.charsLeftOver&&0===n.unusedTokens.length&&void 0===n.bigHour),null!=Object.isFrozen&&Object.isFrozen(e))return i;e._isValid=i}return e._isValid}function _(e){var t=c(NaN);return null!=e?d(f(t),e):f(t).userInvalidated=!0,t}t=Array.prototype.some?Array.prototype.some:function(e){var t,n=Object(this),s=n.length>>>0;for(t=0;t<s;t++)if(t in n&&e.call(this,n[t],t,n))return!0;return!1};var y=n.momentProperties=[],g=!1;function w(e,t){var n,s,i,r=y.length;if(o(t._isAMomentObject)||(e._isAMomentObject=t._isAMomentObject),o(t._i)||(e._i=t._i),o(t._f)||(e._f=t._f),o(t._l)||(e._l=t._l),o(t._strict)||(e._strict=t._strict),o(t._tzm)||(e._tzm=t._tzm),o(t._isUTC)||(e._isUTC=t._isUTC),o(t._offset)||(e._offset=t._offset),o(t._pf)||(e._pf=f(t)),o(t._locale)||(e._locale=t._locale),r>0)for(n=0;n<r;n++)o(i=t[s=y[n]])||(e[s]=i);return e}function p(e){w(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===g&&(g=!0,n.updateOffset(this),g=!1)}function v(e){return e instanceof p||null!=e&&null!=e._isAMomentObject}function k(e){!1===n.suppressDeprecationWarnings&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+e)}function M(e,t){var s=!0;return d((function(){if(null!=n.deprecationHandler&&n.deprecationHandler(null,e),s){var i,a,o,u=[],l=arguments.length;for(a=0;a<l;a++){if(i="","object"==typeof arguments[a]){for(o in i+="\n["+a+"] ",arguments[0])r(arguments[0],o)&&(i+=o+": "+arguments[0][o]+", ");i=i.slice(0,-2)}else i=arguments[a];u.push(i)}k(e+"\nArguments: "+Array.prototype.slice.call(u).join("")+"\n"+(new Error).stack),s=!1}return t.apply(this,arguments)}),t)}var D,S={};function Y(e,t){null!=n.deprecationHandler&&n.deprecationHandler(e,t),S[e]||(k(t),S[e]=!0)}function O(e){return"undefined"!=typeof Function&&e instanceof Function||"[object Function]"===Object.prototype.toString.call(e)}function b(e,t){var n,s=d({},e);for(n in t)r(t,n)&&(i(e[n])&&i(t[n])?(s[n]={},d(s[n],e[n]),d(s[n],t[n])):null!=t[n]?s[n]=t[n]:delete s[n]);for(n in e)r(e,n)&&!r(t,n)&&i(e[n])&&(s[n]=d({},s[n]));return s}function x(e){null!=e&&this.set(e)}n.suppressDeprecationWarnings=!1,n.deprecationHandler=null,D=Object.keys?Object.keys:function(e){var t,n=[];for(t in e)r(e,t)&&n.push(t);return n};function T(e,t,n){var s=""+Math.abs(e),i=t-s.length;return(e>=0?n?"+":"":"-")+Math.pow(10,Math.max(0,i)).toString().substr(1)+s}var N=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,P=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,R={},W={};function C(e,t,n,s){var i=s;"string"==typeof s&&(i=function(){return this[s]()}),e&&(W[e]=i),t&&(W[t[0]]=function(){return T(i.apply(this,arguments),t[1],t[2])}),n&&(W[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),e)})}function H(e,t){return e.isValid()?(t=U(t,e.localeData()),R[t]=R[t]||function(e){var t,n,s,i=e.match(N);for(t=0,n=i.length;t<n;t++)W[i[t]]?i[t]=W[i[t]]:i[t]=(s=i[t]).match(/\[[\s\S]/)?s.replace(/^\[|\]$/g,""):s.replace(/\\/g,"");return function(t){var s,r="";for(s=0;s<n;s++)r+=O(i[s])?i[s].call(t,e):i[s];return r}}(t),R[t](e)):e.localeData().invalidDate()}function U(e,t){var n=5;function s(e){Line truncated
//! moment.js
n.version="2.29.4",e=St,n.fn=pn,n.min=function(){var e=[].slice.call(arguments,0);return bt("isBefore",e)},n.max=function(){var e=[].slice.call(arguments,0);return bt("isAfter",e)},n.now=function(){return Date.now?Date.now():+new Date},n.utc=c,n.unix=function(e){return St(1e3*e)},n.months=function(e,t){return Dn(e,t,"months")},n.isDate=l,n.locale=it,n.invalid=_,n.duration=Gt,n.isMoment=v,n.weekdays=function(e,t,n){return Sn(e,t,n,"weekdays")},n.parseZone=function(){return St.apply(null,arguments).parseZone()},n.localeData=at,n.isDuration=Nt,n.monthsShort=function(e,t){return Dn(e,t,"monthsShort")},n.weekdaysMin=function(e,t,n){return Sn(e,t,n,"weekdaysMin")},n.defineLocale=rt,n.updateLocale=function(e,t){if(null!=t){var n,s,i=Xe;null!=Ke[e]&&null!=Ke[e].parentLocale?Ke[e].set(b(Ke[e]._config,t)):(null!=(s=st(e))&&(i=s._config),t=b(i,t),null==s&&(t.abbr=e),(n=new x(t)).parentLocale=Ke[e],Ke[e]=n),it(e)}else null!=Ke[e]&&(null!=Ke[e].parentLocale?(Ke[e]=Ke[e].parentLocale,e===it()&&it(e)):null!=Ke[e]&&delete Ke[e]);return Ke[e]},n.locales=function(){return D(Ke)},n.weekdaysShort=function(e,t,n){return Sn(e,t,n,"weekdaysShort")},n.normalizeUnits=V,n.relativeTimeRounding=function(e){return void 0===e?qn:"function"==typeof e&&(qn=e,!0)},n.relativeTimeThreshold=function(e,t){return void 0!==Bn[e]&&(void 0===t?Bn[e]:(Bn[e]=t,"s"===e&&(Bn.ss=t-1),!0))},n.calendarFormat=function(e,t){var n=e.diff(t,"days",!0);return n<-6?"sameElse":n<-1?"lastWeek":n<0?"lastDay":n<1?"sameDay":n<2?"nextDay":n<7?"nextWeek":"sameElse"},n.prototype=pn,n.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},n}));
//# sourceMappingURL=/sm/67dbe90480f98dbadab5f76f93197118b03ce65a6932fed6c64b024935f736b4.map
-99
View File
@@ -1,99 +0,0 @@
const config = {
type: 'line',
data: data,
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: 'Chart.js Line Chart'
}
}
},
};
const DATA_COUNT = 7;
const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100};
const labels = Utils.months({count: 7});
const data = {
labels: labels,
datasets: [
{
label: 'Dataset 1',
data: Utils.numbers(NUMBER_CFG),
borderColor: Utils.CHART_COLORS.red,
backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5),
},
// {
// label: 'Dataset 2',
// data: Utils.numbers(NUMBER_CFG),
// borderColor: Utils.CHART_COLORS.blue,
// backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5),
// }
]
};
const actions = [
{
name: 'Randomize',
handler(chart) {
chart.data.datasets.forEach(dataset => {
dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100});
});
chart.update();
}
},
{
name: 'Add Dataset',
handler(chart) {
const data = chart.data;
const dsColor = Utils.namedColor(chart.data.datasets.length);
const newDataset = {
label: 'Dataset ' + (data.datasets.length + 1),
backgroundColor: Utils.transparentize(dsColor, 0.5),
borderColor: dsColor,
data: Utils.numbers({count: data.labels.length, min: -100, max: 100}),
};
chart.data.datasets.push(newDataset);
chart.update();
}
},
{
name: 'Add Data',
handler(chart) {
const data = chart.data;
if (data.datasets.length > 0) {
data.labels = Utils.months({count: data.labels.length + 1});
for (let index = 0; index < data.datasets.length; ++index) {
data.datasets[index].data.push(Utils.rand(-100, 100));
}
chart.update();
}
}
},
{
name: 'Remove Dataset',
handler(chart) {
chart.data.datasets.pop();
chart.update();
}
},
{
name: 'Remove Data',
handler(chart) {
chart.data.labels.splice(-1, 1); // remove the label first
chart.data.datasets.forEach(dataset => {
dataset.data.pop();
});
chart.update();
}
}
];
+22 -4
View File
@@ -15,7 +15,23 @@
<p><a href="/admin/logout">Logout</a> <a href="/admin/login">Add more instances</a></p>
<h4>Instances</h4>
{% if is_admin %}<p class="text-danger-emphasis">Admin mode.</p>{% endif %}
{% if is_admin %}<p class="text-danger-emphasis">root mode</p>{% endif %}
<div><canvas id="graph" width="600" height="200"></canvas><noscript>History feature works only with javascript enabled.</noscript></div>
<div class="container mt-5">
<form>
<div class="row">
<div class="col">
<label for="startDate">Start Date:</label>
<input type="date" class="form-control" id="startDate" name="startDate">
</div>
<div class="col">
<label for="endDate">End Date:</label>
<input type="date" class="form-control" id="endDate" name="endDate">
</div>
</div>
<button type="button" class="btn btn-primary mt-1" id="submitDateRange">Change</button>
</form>
</div>
<div class="table-responsive">
<table id="status-tbl" class="table sortable table-hover table-sm">
<thead>
@@ -28,13 +44,15 @@
<tr>
<td>{{instance.domain}}</td>
{# <td><a href="/admin/history/{{instance.id}}">History</a></td> #}
<td><a href="/admin/errors/{{instance.id}}">Errors</a></td>
<td><a href="/admin/instance/{{instance.id}}">Errors</a></td>
</tr>
{%- endfor %}
</tbody>
</table>
</div>
<p>License: AGPL3</p>
<script src="/static/moment.min_2.29.4.js"></script>
<script src="/static/chart.min_4.4.0.js"></script>
<script src="/static/chartjs-adapter-moment_1.0.1.js"></script>
<script src="/static/admin.js"></script>
</body>
</html>
-2
View File
@@ -18,8 +18,6 @@
<div id="chart">
</div>
<p>License: AGPL3</p>
<script src="/static/overview_admin.js"></script>
<script src="/static/chart.min.js"></script>
</body>
@@ -13,7 +13,22 @@
<div class="container">
<h1>Admin Interface</h1>
<p><a href="/admin">Overview</a> <a href="/admin/logout">Logout</a> <a href="/admin/login">Add more instances</a></p>
<div><canvas data-host="{{HOST_ID}}" id="graph" width="600" height="200"></canvas><noscript>History feature works only with javascript enabled.</noscript></div>
<div class="container mt-5">
<form>
<div class="row">
<div class="col">
<label for="startDate">Start Date:</label>
<input type="date" class="form-control" id="startDate" name="startDate">
</div>
<div class="col">
<label for="endDate">End Date:</label>
<input type="date" class="form-control" id="endDate" name="endDate">
</div>
</div>
<button type="button" class="btn btn-primary mt-1" id="submitDateRange">Change</button>
</form>
</div>
<h4>Last errors from {{HOST_DOMAIN}}</h4>
<div class="table-responsive">
<table id="status-tbl" class="table sortable table-hover table-sm">
@@ -27,7 +42,7 @@
</thead>
<tbody>
{% for error in ERRORS -%}
<tr>
<tr id="error_{{error.time}}">
<td>{{fmt_date(value=error.time)}}</td>
<td>{{error.message}}</td>
<td>{{error.http_body}}</td>
@@ -37,7 +52,9 @@
</tbody>
</table>
</div>
<p>License: AGPL3</p>
<script src="/static/moment.min_2.29.4.js"></script>
<script src="/static/chart.min_4.4.0.js"></script>
<script src="/static/chartjs-adapter-moment_1.0.1.js"></script>
<script src="/static/instance.js"></script>
</body>
</html>