mirror of
https://github.com/4cuck/nitter-status.git
synced 2026-10-26 00:52:25 +00:00
implement history graph
This commit is contained in:
1 parent
1a84ba758d
commit
76d9e85b74
16 files changed
+422
-124
No files matched your search
Generated
+3
@@ -1057,8 +1057,11 @@ name = "entities"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"migration",
|
||||
"sea-orm",
|
||||
"sea-query",
|
||||
"serde",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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"
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -115,4 +115,4 @@ impl Related<super::health_check::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
+45
-9
@@ -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
@@ -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")))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Vendored
-1
@@ -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
|
||||
@@ -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()}}:{})}));
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user