mirror of
https://github.com/4cuck/nitter-status.git
synced 2026-10-26 00:52:25 +00:00
display both graph, load on click, warn on the instance site
This commit is contained in:
1 parent
87230a3703
commit
c6028ef899
7 files changed
+413
-254
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
||||||
|
|
||||||
use sea_orm::{entity::prelude::*, FromQueryResult};
|
use sea_orm::{entity::prelude::*, FromQueryResult};
|
||||||
use sea_query::{Query, Order, Alias, SimpleExpr};
|
use sea_query::{Alias, Order, Query, SimpleExpr};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::check_errors;
|
use crate::check_errors;
|
||||||
@@ -45,27 +45,31 @@ pub struct HealthyAmount {
|
|||||||
pub dead: i64,
|
pub dead: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, FromQueryResult, Serialize)]
|
|
||||||
pub struct InstanceGraphEntry {
|
|
||||||
pub time: i64,
|
|
||||||
pub alive: i64,
|
|
||||||
pub dead: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HealthyAmount {
|
impl HealthyAmount {
|
||||||
/// Fetch health check graph data for all or selected hosts in the selected time range.
|
/// Fetch health check graph data for all or selected hosts in the selected time range.
|
||||||
pub async fn fetch(db: &DatabaseConnection, from: Option<DateTimeUtc>, to: Option<DateTimeUtc>, hosts: Option<&[i32]>) -> Result<Vec<Self>,DbErr> {
|
pub async fn fetch(
|
||||||
|
db: &DatabaseConnection,
|
||||||
|
from: Option<DateTimeUtc>,
|
||||||
|
to: Option<DateTimeUtc>,
|
||||||
|
hosts: Option<&[i32]>,
|
||||||
|
) -> Result<Vec<Self>, DbErr> {
|
||||||
let builder = db.get_database_backend();
|
let builder = db.get_database_backend();
|
||||||
let mut stmt: sea_query::SelectStatement = Query::select();
|
let mut stmt: sea_query::SelectStatement = Query::select();
|
||||||
stmt.column(self::Column::Time)
|
stmt.column(self::Column::Time)
|
||||||
.expr_as(SimpleExpr::Custom("SUM(healthy)".to_string()), Alias::new("alive"))
|
.expr_as(
|
||||||
.expr_as(SimpleExpr::Custom("SUM(1-healthy)".to_string()), Alias::new("dead"))
|
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)
|
.group_by_col(self::Column::Time)
|
||||||
.from(self::Entity);
|
.from(self::Entity);
|
||||||
if let (Some(from),Some(to)) = (from,to) {
|
if let (Some(from), Some(to)) = (from, to) {
|
||||||
stmt.and_where(self::Column::Time.between(from.timestamp(), to.timestamp()));
|
stmt.and_where(self::Column::Time.between(from.timestamp(), to.timestamp()));
|
||||||
}
|
}
|
||||||
if let Some(hosts) = hosts {
|
if let Some(hosts) = hosts {
|
||||||
stmt.and_where(self::Column::Host.is_in(hosts.iter().map(|v| *v)));
|
stmt.and_where(self::Column::Host.is_in(hosts.iter().map(|v| *v)));
|
||||||
}
|
}
|
||||||
stmt.group_by_col(self::Column::Time)
|
stmt.group_by_col(self::Column::Time)
|
||||||
@@ -74,26 +78,6 @@ impl HealthyAmount {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstanceGraphEntry {
|
|
||||||
/// Fetch health check graph data for all or selected hosts in the selected time range.
|
|
||||||
pub async fn fetch(db: &DatabaseConnection, to: Option<DateTimeUtc>) -> Result<Vec<Self>,DbErr> {
|
|
||||||
let builder = db.get_database_backend();
|
|
||||||
let mut stmt: sea_query::SelectStatement = Query::select();
|
|
||||||
stmt//.expr_as(SimpleExpr::Custom("strftime('%Y/%m/%d %H:%M:%S', datetime(time, 'unixepoch', 'utc'))".to_string()), Alias::new("time"))
|
|
||||||
.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);
|
|
||||||
if let Some(to) = to {
|
|
||||||
stmt.and_where(self::Column::Time.lte(to.timestamp()));
|
|
||||||
}
|
|
||||||
stmt.group_by_col(self::Column::Time)
|
|
||||||
.order_by(self::Column::Time, Order::Asc);
|
|
||||||
Self::find_by_statement(builder.build(&stmt)).all(db).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
||||||
@@ -119,7 +103,9 @@ mod test {
|
|||||||
let time_3h = time_now
|
let time_3h = time_now
|
||||||
.checked_sub_signed(chrono::Duration::days(14))
|
.checked_sub_signed(chrono::Duration::days(14))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let hosts = vec![1,2,3];
|
let hosts = vec![1, 2, 3];
|
||||||
HealthyAmount::fetch(&db, Some(time_3h), Some(time_now),Some(&hosts)).await.unwrap();
|
HealthyAmount::fetch(&db, Some(time_3h), Some(time_now), Some(&hosts))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
||||||
|
|
||||||
use sea_orm::{entity::prelude::*, FromQueryResult};
|
use sea_orm::{entity::prelude::*, FromQueryResult};
|
||||||
use sea_query::{Query, SimpleExpr, Order, Alias};
|
use sea_query::{Alias, Order, Query, SimpleExpr};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize)]
|
||||||
@@ -75,24 +75,80 @@ pub struct StatsAmount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StatsAmount {
|
impl StatsAmount {
|
||||||
/// Fetch health check graph data for all or selected hosts in the selected time range.
|
/// Fetch health check graph data for all or selected hosts in the selected time range.
|
||||||
pub async fn fetch(db: &DatabaseConnection, from: DateTimeUtc, to: DateTimeUtc, hosts: Option<&[i32]>) -> Result<Vec<StatsAmount>,DbErr> {
|
pub async fn fetch(
|
||||||
|
db: &DatabaseConnection,
|
||||||
|
from: DateTimeUtc,
|
||||||
|
to: DateTimeUtc,
|
||||||
|
hosts: Option<&[i32]>,
|
||||||
|
) -> Result<Vec<StatsAmount>, DbErr> {
|
||||||
let builder = db.get_database_backend();
|
let builder = db.get_database_backend();
|
||||||
let columns = ["limited_accs","total_accs","total_requests","req_photo_rail","req_user_screen_name","req_search","req_list_tweets","req_user_media","req_tweet_detail","req_list","req_user_tweets","req_user_tweets_and_replies"];
|
let columns = [
|
||||||
|
"limited_accs",
|
||||||
|
"total_accs",
|
||||||
|
"total_requests",
|
||||||
|
"req_photo_rail",
|
||||||
|
"req_user_screen_name",
|
||||||
|
"req_search",
|
||||||
|
"req_list_tweets",
|
||||||
|
"req_user_media",
|
||||||
|
"req_tweet_detail",
|
||||||
|
"req_list",
|
||||||
|
"req_user_tweets",
|
||||||
|
"req_user_tweets_and_replies",
|
||||||
|
];
|
||||||
let mut stmt: sea_query::SelectStatement = Query::select();
|
let mut stmt: sea_query::SelectStatement = Query::select();
|
||||||
let col_stmt = stmt.column(self::Column::Time);
|
let col_stmt = stmt.column(self::Column::Time);
|
||||||
for col in columns {
|
for col in columns {
|
||||||
col_stmt.expr_as(SimpleExpr::Custom(format!("MAX({col})")), Alias::new(format!("{col}_max")))
|
col_stmt
|
||||||
.expr_as(SimpleExpr::Custom(format!("CAST(ifnull(AVG({col}),0) as int)")), Alias::new(format!("{col}_avg")));
|
.expr_as(
|
||||||
|
SimpleExpr::Custom(format!("MAX({col})")),
|
||||||
|
Alias::new(format!("{col}_max")),
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
SimpleExpr::Custom(format!("CAST(ifnull(AVG({col}),0) as int)")),
|
||||||
|
Alias::new(format!("{col}_avg")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
col_stmt.group_by_col(self::Column::Time)
|
col_stmt
|
||||||
|
.group_by_col(self::Column::Time)
|
||||||
.from(self::Entity)
|
.from(self::Entity)
|
||||||
.and_where(self::Column::Time.between(from.timestamp(), to.timestamp()));
|
.and_where(self::Column::Time.between(from.timestamp(), to.timestamp()));
|
||||||
if let Some(hosts) = hosts {
|
if let Some(hosts) = hosts {
|
||||||
stmt.and_where(self::Column::Host.is_in(hosts.iter().map(|v| *v)));
|
stmt.and_where(self::Column::Host.is_in(hosts.iter().map(|v| *v)));
|
||||||
}
|
}
|
||||||
stmt.group_by_col(self::Column::Time)
|
stmt.group_by_col(self::Column::Time)
|
||||||
.order_by(self::Column::Time, Order::Asc);
|
.order_by(self::Column::Time, Order::Asc);
|
||||||
StatsAmount::find_by_statement(builder.build(&stmt)).all(db).await
|
StatsAmount::find_by_statement(builder.build(&stmt))
|
||||||
|
.all(db)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromQueryResult, Serialize)]
|
||||||
|
pub struct StatsCSVEntry {
|
||||||
|
pub time: i64,
|
||||||
|
pub limited_accs_avg: i32,
|
||||||
|
pub total_accs_avg: i32,
|
||||||
|
pub total_requests_avg: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatsCSVEntry {
|
||||||
|
/// Fetch health check graph data for all or selected hosts in the selected time range.
|
||||||
|
pub async fn fetch(db: &DatabaseConnection) -> Result<Vec<Self>, DbErr> {
|
||||||
|
let builder = db.get_database_backend();
|
||||||
|
let columns = ["limited_accs", "total_accs", "total_requests"];
|
||||||
|
let mut stmt: sea_query::SelectStatement = Query::select();
|
||||||
|
stmt.column(self::Column::Time);
|
||||||
|
for col in columns {
|
||||||
|
stmt.expr_as(
|
||||||
|
SimpleExpr::Custom(format!("CAST (AVG({col}) as int)")),
|
||||||
|
Alias::new(format!("{col}_avg")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
stmt.group_by_col(self::Column::Time).from(self::Entity);
|
||||||
|
stmt.group_by_col(self::Column::Time)
|
||||||
|
.order_by(self::Column::Time, Order::Asc);
|
||||||
|
Self::find_by_statement(builder.build(&stmt)).all(db).await
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
-13
@@ -3,8 +3,8 @@ use crate::{Result, ServerError};
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{extract::State, Json};
|
use axum::{extract::State, Json};
|
||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
use entities::health_check;
|
|
||||||
use entities::state::AppState;
|
use entities::state::AppState;
|
||||||
|
use entities::{health_check, instance_stats};
|
||||||
use hyper::http::HeaderValue;
|
use hyper::http::HeaderValue;
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -35,32 +35,33 @@ pub async fn instances(
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn graph(
|
pub async fn graph_csv_health(
|
||||||
State(ref db): State<DatabaseConnection>,
|
State(ref db): State<DatabaseConnection>,
|
||||||
State(ref config): State<Arc<crate::Config>>,
|
State(ref config): State<Arc<crate::Config>>,
|
||||||
) -> Result<axum::response::Response> {
|
) -> Result<axum::response::Response> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let healthy_data = health_check::InstanceGraphEntry::fetch(db, None).await?;
|
let healthy_data = health_check::HealthyAmount::fetch(db, None, None, None).await?;
|
||||||
let queried = std::time::Instant::now();
|
let queried = std::time::Instant::now();
|
||||||
let mut data = String::with_capacity(8*healthy_data.len());
|
let mut data = String::with_capacity(8 * healthy_data.len());
|
||||||
|
|
||||||
data.push_str("Date,Healthy,Dead\n");
|
data.push_str("Date,Healthy,Dead\n");
|
||||||
|
|
||||||
for entry in healthy_data {
|
for entry in healthy_data {
|
||||||
let time = Utc.timestamp_opt(entry.time,0).unwrap().format("%Y-%m-%dT%H:%M:%SZ");
|
let time = Utc
|
||||||
writeln!(&mut data, "{},{},{}",time,entry.alive,entry.dead)
|
.timestamp_opt(entry.time, 0)
|
||||||
.map_err(|e|ServerError::CSV(e.to_string()))?;
|
.unwrap()
|
||||||
|
.format("%Y-%m-%dT%H:%M:%SZ");
|
||||||
|
writeln!(&mut data, "{time},{},{}", entry.alive, entry.dead)
|
||||||
|
.map_err(|e| ServerError::CSV(e.to_string()))?;
|
||||||
}
|
}
|
||||||
let formatted = std::time::Instant::now();
|
let formatted = std::time::Instant::now();
|
||||||
let query_time = queried - start;
|
let query_time = queried - start;
|
||||||
let format_time = formatted - queried;
|
let format_time = formatted - queried;
|
||||||
tracing::debug!(?query_time,?format_time);
|
tracing::debug!(?query_time, ?format_time);
|
||||||
|
|
||||||
let mut res = data.into_response();
|
let mut res = data.into_response();
|
||||||
res.headers_mut().insert(
|
res.headers_mut()
|
||||||
"content-type",
|
.insert("content-type", HeaderValue::from_str("text/csv").unwrap());
|
||||||
HeaderValue::from_str("text/csv").unwrap(),
|
|
||||||
);
|
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
"cache-control",
|
"cache-control",
|
||||||
HeaderValue::from_str(&format!("public, max-age={}", config.max_age)).unwrap(),
|
HeaderValue::from_str(&format!("public, max-age={}", config.max_age)).unwrap(),
|
||||||
@@ -70,4 +71,46 @@ pub async fn graph(
|
|||||||
HeaderValue::from_static("noindex, nofollow"),
|
HeaderValue::from_static("noindex, nofollow"),
|
||||||
);
|
);
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn graph_csv_stats(
|
||||||
|
State(ref db): State<DatabaseConnection>,
|
||||||
|
State(ref config): State<Arc<crate::Config>>,
|
||||||
|
) -> Result<axum::response::Response> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let healthy_data = instance_stats::StatsCSVEntry::fetch(db).await?;
|
||||||
|
let queried = std::time::Instant::now();
|
||||||
|
let mut data = String::with_capacity(8 * healthy_data.len());
|
||||||
|
|
||||||
|
data.push_str("Date,Tokens AVG,Limited Tokens AVG,Requests AVG\n");
|
||||||
|
|
||||||
|
for entry in healthy_data {
|
||||||
|
let time = Utc
|
||||||
|
.timestamp_opt(entry.time, 0)
|
||||||
|
.unwrap()
|
||||||
|
.format("%Y-%m-%dT%H:%M:%SZ");
|
||||||
|
writeln!(
|
||||||
|
&mut data,
|
||||||
|
"{time},{},{},{}",
|
||||||
|
entry.total_accs_avg, entry.limited_accs_avg, entry.total_requests_avg
|
||||||
|
)
|
||||||
|
.map_err(|e| ServerError::CSV(e.to_string()))?;
|
||||||
|
}
|
||||||
|
let formatted = std::time::Instant::now();
|
||||||
|
let query_time = queried - start;
|
||||||
|
let format_time = formatted - queried;
|
||||||
|
tracing::debug!(?query_time, ?format_time);
|
||||||
|
|
||||||
|
let mut res = data.into_response();
|
||||||
|
res.headers_mut()
|
||||||
|
.insert("content-type", HeaderValue::from_str("text/csv").unwrap());
|
||||||
|
res.headers_mut().insert(
|
||||||
|
"cache-control",
|
||||||
|
HeaderValue::from_str(&format!("public, max-age={}", config.max_age)).unwrap(),
|
||||||
|
);
|
||||||
|
res.headers_mut().insert(
|
||||||
|
"X-Robots-Tag",
|
||||||
|
HeaderValue::from_static("noindex, nofollow"),
|
||||||
|
);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
+4
-4
@@ -145,11 +145,11 @@ pub async fn start(
|
|||||||
ServeDir::new("server/static").append_index_html_on_directories(false),
|
ServeDir::new("server/static").append_index_html_on_directories(false),
|
||||||
)
|
)
|
||||||
.route("/api/v1/instances", get(api::instances))
|
.route("/api/v1/instances", get(api::instances))
|
||||||
.route("/api/graph", get(api::graph))
|
.route("/api/csv/health", get(api::graph_csv_health))
|
||||||
|
.route("/api/csv/stats", get(api::graph_csv_stats))
|
||||||
.nest(ADMIN_OVERVIEW_URL, Router::new()
|
.nest(ADMIN_OVERVIEW_URL, Router::new()
|
||||||
.route("/", get(admin::overview))
|
.route("/", get(admin::overview))
|
||||||
.route("/instance/:instance", get(admin::instance_view))
|
.route("/instance/:instance", get(admin::instance_view))
|
||||||
// .route("/history/:host", get(admin::history_view))
|
|
||||||
.route("/api/history/:instance", post(admin::history_json_specific))
|
.route("/api/history/:instance", post(admin::history_json_specific))
|
||||||
.route("/api/history", post(admin::history_json))
|
.route("/api/history", post(admin::history_json))
|
||||||
.route("/login", get(admin::login_view).post(admin::login).route_layer(rate_limit_layer))
|
.route("/login", get(admin::login_view).post(admin::login).route_layer(rate_limit_layer))
|
||||||
@@ -157,7 +157,6 @@ pub async fn start(
|
|||||||
// .layer(ServiceBuilder::new().layer(SetResponseHeaderLayer::overriding(header::CACHE_CONTROL, "must-revalidate")))
|
// .layer(ServiceBuilder::new().layer(SetResponseHeaderLayer::overriding(header::CACHE_CONTROL, "must-revalidate")))
|
||||||
.layer(session_service)
|
.layer(session_service)
|
||||||
)
|
)
|
||||||
// .route("/admin", get(admin::view))
|
|
||||||
.route("/about", get(website::about))
|
.route("/about", get(website::about))
|
||||||
.route("/rip", get(website::rip))
|
.route("/rip", get(website::rip))
|
||||||
.route(
|
.route(
|
||||||
@@ -231,7 +230,8 @@ impl axum::response::IntoResponse for ServerError {
|
|||||||
StatusCode::FORBIDDEN,
|
StatusCode::FORBIDDEN,
|
||||||
Cow::Borrowed("Missing permission to access this resource"),
|
Cow::Borrowed("Missing permission to access this resource"),
|
||||||
),
|
),
|
||||||
CSV(_ )| MutexFailure | Templating(_) | DBError(_) | SessionError(_) | HostNotFound(_) => (
|
CSV(_) | MutexFailure | Templating(_) | DBError(_) | SessionError(_)
|
||||||
|
| HostNotFound(_) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Cow::Borrowed("Internal Server Error"),
|
Cow::Borrowed("Internal Server Error"),
|
||||||
),
|
),
|
||||||
|
|||||||
+122
-50
@@ -1,57 +1,129 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function(event) {
|
document.addEventListener("DOMContentLoaded", function (event) {
|
||||||
var loadGraphBtn = document.getElementById("loadGraphBtn");
|
var loadGraphBtn = document.getElementById("loadGraphBtn");
|
||||||
if (loadGraphBtn) {
|
if (loadGraphBtn) {
|
||||||
loadGraphBtn.addEventListener("click", function() {
|
loadGraphBtn.addEventListener("click", function () {
|
||||||
var overlay = document.getElementById("overlay");
|
var elements = document.querySelectorAll('.graph');
|
||||||
if (overlay) {
|
elements.forEach(function (element) {
|
||||||
overlay.style.display = "none";
|
element.style.display = 'block';
|
||||||
loadGraph();
|
});
|
||||||
}
|
loadGraphBtn.style.display = "none";
|
||||||
|
loadHealthGraph();
|
||||||
|
loadStatsGraph();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadGraph() {
|
async function loadHealthGraph() {
|
||||||
let graphDiv = document.getElementById('history');
|
let graphDiv = document.getElementById('graph-health');
|
||||||
try {
|
try {
|
||||||
let g = new Dygraph(
|
let g = new Dygraph(
|
||||||
graphDiv,
|
graphDiv,
|
||||||
"/api/graph",
|
"/api/csv/health",
|
||||||
{
|
{
|
||||||
title: 'Historic Instance Healthiness',
|
title: 'Historic Instance Healthiness',
|
||||||
showRangeSelector: true,
|
showRangeSelector: true,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
rangeSelectorPlotFillColor: 'MediumSlateBlue',
|
rangeSelectorPlotFillColor: 'MediumSlateBlue',
|
||||||
rangeSelectorPlotFillGradientColor: 'rgba(123, 104, 238, 0)',
|
rangeSelectorPlotFillGradientColor: 'rgba(123, 104, 238, 0)',
|
||||||
colorValue: 0.9,
|
colorValue: 0.9,
|
||||||
fillAlpha: 0.4,
|
fillAlpha: 0.4,
|
||||||
colors: ['#008000', '#ffa500'],
|
colors: ['#008000', '#ffa500'],
|
||||||
}
|
ylabel: 'Nitter Instances',
|
||||||
);
|
}
|
||||||
g.ready(function() {
|
);
|
||||||
g.setAnnotations([
|
g.ready(function () {
|
||||||
{
|
g.setAnnotations([
|
||||||
series: "Healthy",
|
{
|
||||||
x: "2023-08-15T07:10:17Z",
|
series: "Healthy",
|
||||||
shortText: "G",
|
x: "2023-08-15T07:10:17Z",
|
||||||
text: "First API Change"
|
shortText: "G",
|
||||||
},
|
text: "First API Change"
|
||||||
{
|
},
|
||||||
series: "Dead",
|
{
|
||||||
x: "2023-10-21T14:37:44Z",
|
series: "Dead",
|
||||||
shortText: "C",
|
x: "2023-10-21T14:37:44Z",
|
||||||
text: "Wiki Cleanup"
|
shortText: "C",
|
||||||
},
|
text: "Wiki Cleanup"
|
||||||
{
|
},
|
||||||
series: "Healthy",
|
{
|
||||||
x: "2024-01-25T15:24:16Z",
|
series: "Healthy",
|
||||||
shortText: "R",
|
x: "2024-01-25T15:24:16Z",
|
||||||
text: "API Shutdown"
|
shortText: "R",
|
||||||
}
|
text: "API Shutdown"
|
||||||
]);
|
}
|
||||||
});
|
]);
|
||||||
} catch (error) {
|
});
|
||||||
console.error('Failed to fetch data:', error);
|
} catch (error) {
|
||||||
graphDiv.textContent = "Failed to load data.";
|
console.error('Failed to fetch data:', error);
|
||||||
}
|
graphDiv.textContent = "Failed to load data.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function loadStatsGraph() {
|
||||||
|
let graphDiv = document.getElementById('graph-stats');
|
||||||
|
try {
|
||||||
|
let g = new Dygraph(
|
||||||
|
graphDiv,
|
||||||
|
"/api/csv/stats",
|
||||||
|
{
|
||||||
|
title: 'Historic Average Instance Statistics',
|
||||||
|
showRangeSelector: true,
|
||||||
|
width: '100%',
|
||||||
|
rangeSelectorPlotFillColor: 'MediumSlateBlue',
|
||||||
|
rangeSelectorPlotFillGradientColor: 'rgba(123, 104, 238, 0)',
|
||||||
|
colorValue: 0.9,
|
||||||
|
fillAlpha: 0.4,
|
||||||
|
colors: ['#008000', '#ffa500', '#6495ED'],
|
||||||
|
series: {
|
||||||
|
'Tokens AVG': {
|
||||||
|
axis: 'y'
|
||||||
|
},
|
||||||
|
'Limited Tokens AVG': {
|
||||||
|
axis: 'y'
|
||||||
|
},
|
||||||
|
'Requests AVG': {
|
||||||
|
axis: 'y2'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axes: {
|
||||||
|
y: {
|
||||||
|
axisLabelWidth: 60,
|
||||||
|
logscale: "y log scale",
|
||||||
|
},
|
||||||
|
y2: {
|
||||||
|
// set axis-related properties here
|
||||||
|
drawGrid: false,
|
||||||
|
drawAxis: false,
|
||||||
|
logscale: "y log scale",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ylabel: 'Tokens',
|
||||||
|
y2label: 'Requests',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
g.ready(function () {
|
||||||
|
g.setAnnotations([
|
||||||
|
{
|
||||||
|
series: "Healthy",
|
||||||
|
x: "2023-08-15T07:10:17Z",
|
||||||
|
shortText: "G",
|
||||||
|
text: "First API Change"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
series: "Dead",
|
||||||
|
x: "2023-10-21T14:37:44Z",
|
||||||
|
shortText: "C",
|
||||||
|
text: "Wiki Cleanup"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
series: "Healthy",
|
||||||
|
x: "2024-01-25T15:24:16Z",
|
||||||
|
shortText: "R",
|
||||||
|
text: "API Shutdown"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch data:', error);
|
||||||
|
graphDiv.textContent = "Failed to load data.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,124 +1,136 @@
|
|||||||
{# SPDX-License-Identifier: AGPL-3.0-only #}
|
{# SPDX-License-Identifier: AGPL-3.0-only #}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-bs-theme="dark">
|
<html lang="en" data-bs-theme="dark">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
<head>
|
||||||
<meta name="keywords" content="nitter, status, uptime" />
|
<meta charset="utf-8">
|
||||||
<meta name="description" content="Nitter instance uptime and status tracker." />
|
<meta name="keywords" content="nitter, status, uptime" />
|
||||||
<meta name="author" content="" />
|
<meta name="description" content="Nitter instance uptime and status tracker." />
|
||||||
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
<meta name="author" content="" />
|
||||||
<style>
|
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
.uptime-graph rect {
|
.uptime-graph rect {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uptime-graph rect:hover {
|
.uptime-graph rect:hover {
|
||||||
fill: #5e6c84;
|
fill: #5e6c84;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Nitter Instance Health</title>
|
<title>Nitter Instance Health</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Nitter Instance Uptime & Health</h1>
|
<h1>Nitter Instance Uptime & Health</h1>
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
Nitter is Dead! <a href="/rip">>>Read More<<</a>
|
||||||
|
</div>
|
||||||
<p><a href="/about">About</a></p>
|
<p><a href="/about">About</a></p>
|
||||||
<p>Please use the <a rel="nofollow" href="/about#api">API</a> for bots. Please <b>do NOT use these instances for scraping</b>, host nitter yourself.</p>
|
<p>Please use the <a rel="nofollow" href="/about#api">API</a> for bots. Please <b>do NOT use these instances for
|
||||||
|
scraping</b>, host nitter yourself.</p>
|
||||||
<p>Last Updated {{last_updated}} <a rel="nofollow" href="https://time.is/UTC">UTC</a>.</p>
|
<p>Last Updated {{last_updated}} <a rel="nofollow" href="https://time.is/UTC">UTC</a>.</p>
|
||||||
<p>Customize the visible columns <a href="#column-settings">down below</a>.</p>
|
<p>Customize the visible columns <a href="#column-settings">down below</a>.</p>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table id="status-tbl" class="table sortable table-hover table-sm">
|
<table id="status-tbl" class="table sortable table-hover table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Instance</th>
|
<th scope="col">Instance</th>
|
||||||
<th data-name="country" scope="col">Country</th>
|
<th data-name="country" scope="col">Country</th>
|
||||||
<th scope="col">Healthy</th>
|
<th scope="col">Healthy</th>
|
||||||
<th scope="col">Health History</th>
|
<th scope="col">Health History</th>
|
||||||
<th data-name="average" scope="col">Average Time</th>
|
<th data-name="average" scope="col">Average Time</th>
|
||||||
<th scope="col">All Time %</th>
|
<th scope="col">All Time %</th>
|
||||||
<th data-name="rss" scope="col">RSS</th>
|
<th data-name="rss" scope="col">RSS</th>
|
||||||
<th data-name="version" scope="col">Nitter Version</th>
|
<th data-name="version" scope="col">Nitter Version</th>
|
||||||
<th data-name="connectivity" scope="col">Connectivity</th>
|
<th data-name="connectivity" scope="col">Connectivity</th>
|
||||||
<th scope="col">Points</th>
|
<th scope="col">Points</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for host in instances -%}
|
{% for host in instances -%}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a rel="nofollow external" href="{{host.url}}">{{host.domain}}</a></td>
|
<td><a rel="nofollow external" href="{{host.url}}">{{host.domain}}</a></td>
|
||||||
<td data-name="country">{{host.country}}</td>
|
<td data-name="country">{{host.country}}</td>
|
||||||
<td>
|
<td>
|
||||||
{%- if host.is_bad_host -%}
|
{%- if host.is_bad_host -%}
|
||||||
<div title="Host known for blocking healthchecks">❓</div>
|
<div title="Host known for blocking healthchecks">❓</div>
|
||||||
{%- elif host.healthy -%}
|
{%- elif host.healthy -%}
|
||||||
✅
|
✅
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
❌
|
❌
|
||||||
{%- endif -%}</td>
|
{%- endif -%}
|
||||||
<td>
|
</td>
|
||||||
{%- set height = 28 -%}
|
<td>
|
||||||
{%- set width = 110 -%}
|
{%- set height = 28 -%}
|
||||||
{%- set width_bar = 5 -%}
|
{%- set width = 110 -%}
|
||||||
{%- set offset = 5 -%}
|
{%- set width_bar = 5 -%}
|
||||||
{%- if not host.__show_last_seen -%}
|
{%- set offset = 5 -%}
|
||||||
|
{%- if not host.__show_last_seen -%}
|
||||||
<svg class="uptime-graph" viewBox="0 0 {{width}} {{height}}" height="{{height}}">
|
<svg class="uptime-graph" viewBox="0 0 {{width}} {{height}}" height="{{height}}">
|
||||||
{%- for check in host.recent_checks -%}
|
{%- for check in host.recent_checks -%}
|
||||||
{%- if check.1 -%}
|
{%- if check.1 -%}
|
||||||
{%- set title = "Healthy " ~ check.0 -%}
|
{%- set title = "Healthy " ~ check.0 -%}
|
||||||
{%- set color_bar = "#2fcc66" -%}
|
{%- set color_bar = "#2fcc66" -%}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{%- set title = "Unhealthy " ~ check.0 -%}
|
{%- set title = "Unhealthy " ~ check.0 -%}
|
||||||
{%- set color_bar = "#ff6225" -%}
|
{%- set color_bar = "#ff6225" -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<rect x="{{(loop.index-1) * width_bar}}" y="0" width="{{width_bar - 1}}" height="{{height}}" fill="{{color_bar}}" fill-opacity="0.95" shape-rendering="auto" ><title>{{title}}</title></rect>
|
<rect x="{{(loop.index-1) * width_bar}}" y="0" width="{{width_bar - 1}}" height="{{height}}"
|
||||||
|
fill="{{color_bar}}" fill-opacity="0.95" shape-rendering="auto">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
</rect>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</svg>
|
</svg>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{# <!-- no healthy entries, show last time host was seen healthy --> #}
|
{# <!-- no healthy entries, show last time host was seen healthy --> #}
|
||||||
{%- if host.last_healthy -%}
|
{%- if host.last_healthy -%}
|
||||||
LSH: {{host.last_healthy | truncate(length=16, end="") | replace(from="-", to=".") | replace(from="T", to=" ")}}
|
LSH: {{host.last_healthy | truncate(length=16, end="") | replace(from="-", to=".") | replace(from="T",
|
||||||
|
to=" ")}}
|
||||||
<a rel="nofollow" href="https://time.is/UTC">UTC</a>
|
<a rel="nofollow" href="https://time.is/UTC">UTC</a>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
Never seen healthy.
|
Never seen healthy.
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endif -%}
|
|
||||||
</td>
|
|
||||||
<td data-name="average">{%- if not host.__show_last_seen -%}{{host.ping_avg}}ms{% endif %}</td>
|
|
||||||
<td>{{host.healthy_percentage_overall}}%</td>
|
|
||||||
<td data-name="rss">{% if host.rss -%} ✅ {%- else -%} ❌ {%- endif -%}</td>
|
|
||||||
{%- if host.healthy and not host.version -%}
|
|
||||||
{%- set version = "Dmissing" -%}
|
|
||||||
{%- elif host.version and not host.is_upstream -%}
|
|
||||||
{%- set version = "Aforeign" -%}
|
|
||||||
{%- elif host.is_latest_version -%}
|
|
||||||
{%- set version = "Blatest" -%}
|
|
||||||
{%- elif host.version and not host.is_latest_version -%}
|
|
||||||
{%- set version = "Coutdated" -%}
|
|
||||||
{%- else -%}
|
|
||||||
{%- set version = "Eunknown" -%}
|
|
||||||
{%- endif -%}
|
|
||||||
<td data-name="version" data-sort="{{version}}{{host.version | default(value='')}}">
|
|
||||||
{%- if host.version_url -%}
|
|
||||||
<a rel="nofollow external" title="{{host.version}}" href="{{host.version_url}}">
|
|
||||||
{{host.version | truncate(length=18) | default(value="missing version")}}
|
|
||||||
</a>
|
|
||||||
{%- else -%}
|
|
||||||
<a rel="nofollow external" href="{{host.url ~ '/about'}}">missing version</a>
|
|
||||||
{%- endif -%}
|
|
||||||
{%- if version == "Dmissing" -%}
|
|
||||||
<span class="ms-1 badge bg-warning">missing</span>
|
|
||||||
{%- elif version == "Aforeign" -%}
|
|
||||||
<span class="ms-1 badge bg-secondary">foreign repo</span>
|
|
||||||
{%- elif version == "Blatest" -%}
|
|
||||||
<span class="ms-1 badge bg-success">latest</span>
|
|
||||||
{%- elif version == "Coutdated" -%}
|
|
||||||
<span class="ms-1 badge bg-info">outdated</span>
|
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</td>
|
</td>
|
||||||
<td data-name="connectivity">{{host.connectivity}}</td>
|
<td data-name="average">{%- if not host.__show_last_seen -%}{{host.ping_avg}}ms{% endif %}</td>
|
||||||
<td>{{host.points}}</td>
|
<td>{{host.healthy_percentage_overall}}%</td>
|
||||||
|
<td data-name="rss">{% if host.rss -%} ✅ {%- else -%} ❌ {%- endif -%}</td>
|
||||||
|
{%- if host.healthy and not host.version -%}
|
||||||
|
{%- set version = "Dmissing" -%}
|
||||||
|
{%- elif host.version and not host.is_upstream -%}
|
||||||
|
{%- set version = "Aforeign" -%}
|
||||||
|
{%- elif host.is_latest_version -%}
|
||||||
|
{%- set version = "Blatest" -%}
|
||||||
|
{%- elif host.version and not host.is_latest_version -%}
|
||||||
|
{%- set version = "Coutdated" -%}
|
||||||
|
{%- else -%}
|
||||||
|
{%- set version = "Eunknown" -%}
|
||||||
|
{%- endif -%}
|
||||||
|
<td data-name="version" data-sort="{{version}}{{host.version | default(value='')}}">
|
||||||
|
{%- if host.version_url -%}
|
||||||
|
<a rel="nofollow external" title="{{host.version}}" href="{{host.version_url}}">
|
||||||
|
{{host.version | truncate(length=18) | default(value="missing version")}}
|
||||||
|
</a>
|
||||||
|
{%- else -%}
|
||||||
|
<a rel="nofollow external" href="{{host.url ~ '/about'}}">missing version</a>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if version == "Dmissing" -%}
|
||||||
|
<span class="ms-1 badge bg-warning">missing</span>
|
||||||
|
{%- elif version == "Aforeign" -%}
|
||||||
|
<span class="ms-1 badge bg-secondary">foreign repo</span>
|
||||||
|
{%- elif version == "Blatest" -%}
|
||||||
|
<span class="ms-1 badge bg-success">latest</span>
|
||||||
|
{%- elif version == "Coutdated" -%}
|
||||||
|
<span class="ms-1 badge bg-info">outdated</span>
|
||||||
|
{%- endif -%}
|
||||||
|
</td>
|
||||||
|
<td data-name="connectivity">{{host.connectivity}}</td>
|
||||||
|
<td>{{host.points}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<noscript>Toggling table columns and sorting requires JS.</noscript>
|
<noscript>Toggling table columns and sorting requires JS.</noscript>
|
||||||
@@ -156,9 +168,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/bootstrap.min.js"></script>
|
<script src="/static/bootstrap.min.js"></script>
|
||||||
<link href="/static/sorting.css" rel="stylesheet" />
|
<link href="/static/sorting.css" rel="stylesheet" />
|
||||||
<script src="/static/sortable.min.js"></script>
|
<script src="/static/sortable.min.js"></script>
|
||||||
<script src="/static/table_toggle.js"></script>
|
<script src="/static/table_toggle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
@@ -1,39 +1,28 @@
|
|||||||
{# SPDX-License-Identifier: AGPL-3.0-only #}
|
{# SPDX-License-Identifier: AGPL-3.0-only #}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-bs-theme="dark">
|
<html lang="en" data-bs-theme="dark">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
<head>
|
||||||
<meta name="keywords" content="nitter, status, uptime" />
|
<meta charset="utf-8">
|
||||||
<meta name="description" content="Nitter instance uptime and status tracker." />
|
<meta name="keywords" content="nitter, status, uptime" />
|
||||||
<meta name="author" content="" />
|
<meta name="description" content="Nitter instance uptime and status tracker." />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/bootstrap.min.css">
|
<meta name="author" content="" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/dygraph.min.css" />
|
<link rel="stylesheet" type="text/css" href="/static/bootstrap.min.css">
|
||||||
<style>
|
<link rel="stylesheet" type="text/css" href="/static/dygraph.min.css" />
|
||||||
#history .dygraph-axis-label { color: white; }
|
<style>
|
||||||
#history .dygraph-legend { background-color: #101015; }
|
.dygraph-axis-label {
|
||||||
.overlay {
|
color: white;
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(255, 255, 255, 0.5); /* Adjusted to a light color for dark mode */
|
|
||||||
z-index: 2;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
{# display: none; #}
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
.dygraph-legend {
|
||||||
.overlay {
|
background-color: #101015;
|
||||||
background-color: rgba(0, 0, 0, 0.5); /* Dark overlay color for dark mode */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Nitter Is Dead</title>
|
<title>Nitter Is Dead</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>All Nitter public instances shut down.</h1>
|
<h1>All Nitter public instances shut down.</h1>
|
||||||
<p>There are no public instances left for you to use.</p>
|
<p>There are no public instances left for you to use.</p>
|
||||||
@@ -43,32 +32,32 @@
|
|||||||
|
|
||||||
<p>In January 2024, the Twitter API used by Nitter to fetch data from Twitter got shut down.</p>
|
<p>In January 2024, the Twitter API used by Nitter to fetch data from Twitter got shut down.</p>
|
||||||
|
|
||||||
<p>Without this, there is no way to run instances on the scale needed for the public instances that got listed here.</p>
|
<p>Without this, there is no way to run instances on the scale needed for the public instances that got listed here.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>That's unfortunate. But I still want to use Nitter !</h2>
|
<h2>That's unfortunate. But I still want to use Nitter !</h2>
|
||||||
<p>If you want to continue running Nitter, you can do so using a normal Twitter account.</p>
|
<p>If you want to continue running Nitter, you can do so using a normal Twitter account.</p>
|
||||||
|
|
||||||
<p>Do note however, that this cannot and will not scale, and is in a legal grey-area. Proceed at your own risk.</p>
|
<p>Do note however, that this cannot and will not scale, and is in a legal grey-area. Proceed at your own risk.</p>
|
||||||
|
|
||||||
<p>See <a href="https://github.com/zedeus/nitter/wiki/Guest-Account-Branch-Deployment">here</a> for setting up nitter if you feel brave enough and don't fear the command line.</p>
|
<p>See <a href="https://github.com/zedeus/nitter/wiki/Guest-Account-Branch-Deployment">here</a> for setting up
|
||||||
|
Nitter if you feel brave enough and don't fear the command line.</p>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!--<div class="row">
|
<button class="btn btn-primary" id="loadGraphBtn">Show Historical Data Graph (CPU & Network Traffic)</button>
|
||||||
<div class="col">
|
|
||||||
<div id="history" style="height: 400px;"></div>
|
|
||||||
</div>
|
|
||||||
</div>-->
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col" id="overlay">
|
|
||||||
<button class="btn btn-primary" id="loadGraphBtn">Load & Display Graph</button>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div id="history" style="height: 400px;"></div>
|
<div id="graph-health" class="graph" style="height: 400px; display:none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div id="graph-stats" class="graph" style="height: 400px; display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>Last Updated {{last_updated}} <a rel="nofollow" href="https://time.is/UTC">UTC</a>.</p>
|
|
||||||
<p><a href="/instances">Instances</a> <a href="/about">About</a></p>
|
<p><a href="/instances">Instances</a> <a href="/about">About</a></p>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" src="/static/dygraph.min.js"></script>
|
<script type="text/javascript" src="/static/dygraph.min.js"></script>
|
||||||
<script type="text/javascript" src="/static/history.2.js"></script>
|
<script type="text/javascript" src="/static/history.2.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user