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
+404
-245
No files matched your search
@@ -1,7 +1,7 @@
|
||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
||||
|
||||
use sea_orm::{entity::prelude::*, FromQueryResult};
|
||||
use sea_query::{Query, Order, Alias, SimpleExpr};
|
||||
use sea_query::{Alias, Order, Query, SimpleExpr};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::check_errors;
|
||||
@@ -45,27 +45,31 @@ pub struct HealthyAmount {
|
||||
pub dead: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromQueryResult, Serialize)]
|
||||
pub struct InstanceGraphEntry {
|
||||
pub time: i64,
|
||||
pub alive: i64,
|
||||
pub dead: i64,
|
||||
}
|
||||
|
||||
impl HealthyAmount {
|
||||
/// 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 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"))
|
||||
.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(from),Some(to)) = (from,to) {
|
||||
if let (Some(from), Some(to)) = (from, to) {
|
||||
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.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)]
|
||||
mod test {
|
||||
|
||||
@@ -119,7 +103,9 @@ mod test {
|
||||
let time_3h = time_now
|
||||
.checked_sub_signed(chrono::Duration::days(14))
|
||||
.unwrap();
|
||||
let hosts = vec![1,2,3];
|
||||
HealthyAmount::fetch(&db, Some(time_3h), Some(time_now),Some(&hosts)).await.unwrap();
|
||||
let hosts = vec![1, 2, 3];
|
||||
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
|
||||
|
||||
use sea_orm::{entity::prelude::*, FromQueryResult};
|
||||
use sea_query::{Query, SimpleExpr, Order, Alias};
|
||||
use sea_query::{Alias, Order, Query, SimpleExpr};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize)]
|
||||
@@ -76,23 +76,79 @@ pub struct StatsAmount {
|
||||
|
||||
impl StatsAmount {
|
||||
/// 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 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 col_stmt = stmt.column(self::Column::Time);
|
||||
for col in columns {
|
||||
col_stmt.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
|
||||
.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)
|
||||
.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.group_by_col(self::Column::Time)
|
||||
.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
|
||||
}
|
||||
}
|
||||
+53
-10
@@ -3,8 +3,8 @@ use crate::{Result, ServerError};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{extract::State, Json};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use entities::health_check;
|
||||
use entities::state::AppState;
|
||||
use entities::{health_check, instance_stats};
|
||||
use hyper::http::HeaderValue;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use serde::Serialize;
|
||||
@@ -35,32 +35,75 @@ pub async fn instances(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn graph(
|
||||
pub async fn graph_csv_health(
|
||||
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 = 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 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");
|
||||
|
||||
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.alive,entry.dead)
|
||||
.map_err(|e|ServerError::CSV(e.to_string()))?;
|
||||
let time = Utc
|
||||
.timestamp_opt(entry.time, 0)
|
||||
.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 query_time = queried - start;
|
||||
let format_time = formatted - queried;
|
||||
tracing::debug!(?query_time,?format_time);
|
||||
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(
|
||||
"content-type",
|
||||
HeaderValue::from_str("text/csv").unwrap(),
|
||||
"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)
|
||||
}
|
||||
|
||||
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(),
|
||||
|
||||
+4
-4
@@ -145,11 +145,11 @@ pub async fn start(
|
||||
ServeDir::new("server/static").append_index_html_on_directories(false),
|
||||
)
|
||||
.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()
|
||||
.route("/", get(admin::overview))
|
||||
.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", post(admin::history_json))
|
||||
.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(session_service)
|
||||
)
|
||||
// .route("/admin", get(admin::view))
|
||||
.route("/about", get(website::about))
|
||||
.route("/rip", get(website::rip))
|
||||
.route(
|
||||
@@ -231,7 +230,8 @@ impl axum::response::IntoResponse for ServerError {
|
||||
StatusCode::FORBIDDEN,
|
||||
Cow::Borrowed("Missing permission to access this resource"),
|
||||
),
|
||||
CSV(_ )| MutexFailure | Templating(_) | DBError(_) | SessionError(_) | HostNotFound(_) => (
|
||||
CSV(_) | MutexFailure | Templating(_) | DBError(_) | SessionError(_)
|
||||
| HostNotFound(_) => (
|
||||
StatusCode::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");
|
||||
if (loadGraphBtn) {
|
||||
loadGraphBtn.addEventListener("click", function() {
|
||||
var overlay = document.getElementById("overlay");
|
||||
if (overlay) {
|
||||
overlay.style.display = "none";
|
||||
loadGraph();
|
||||
}
|
||||
loadGraphBtn.addEventListener("click", function () {
|
||||
var elements = document.querySelectorAll('.graph');
|
||||
elements.forEach(function (element) {
|
||||
element.style.display = 'block';
|
||||
});
|
||||
loadGraphBtn.style.display = "none";
|
||||
loadHealthGraph();
|
||||
loadStatsGraph();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function loadGraph() {
|
||||
let graphDiv = document.getElementById('history');
|
||||
try {
|
||||
let g = new Dygraph(
|
||||
graphDiv,
|
||||
"/api/graph",
|
||||
{
|
||||
title: 'Historic Instance Healthiness',
|
||||
showRangeSelector: true,
|
||||
width: '100%',
|
||||
rangeSelectorPlotFillColor: 'MediumSlateBlue',
|
||||
rangeSelectorPlotFillGradientColor: 'rgba(123, 104, 238, 0)',
|
||||
colorValue: 0.9,
|
||||
fillAlpha: 0.4,
|
||||
colors: ['#008000', '#ffa500'],
|
||||
}
|
||||
);
|
||||
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.";
|
||||
}
|
||||
async function loadHealthGraph() {
|
||||
let graphDiv = document.getElementById('graph-health');
|
||||
try {
|
||||
let g = new Dygraph(
|
||||
graphDiv,
|
||||
"/api/csv/health",
|
||||
{
|
||||
title: 'Historic Instance Healthiness',
|
||||
showRangeSelector: true,
|
||||
width: '100%',
|
||||
rangeSelectorPlotFillColor: 'MediumSlateBlue',
|
||||
rangeSelectorPlotFillGradientColor: 'rgba(123, 104, 238, 0)',
|
||||
colorValue: 0.9,
|
||||
fillAlpha: 0.4,
|
||||
colors: ['#008000', '#ffa500'],
|
||||
ylabel: 'Nitter Instances',
|
||||
}
|
||||
);
|
||||
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.";
|
||||
}
|
||||
}
|
||||
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 #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="keywords" content="nitter, status, uptime" />
|
||||
<meta name="description" content="Nitter instance uptime and status tracker." />
|
||||
<meta name="author" content="" />
|
||||
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="keywords" content="nitter, status, uptime" />
|
||||
<meta name="description" content="Nitter instance uptime and status tracker." />
|
||||
<meta name="author" content="" />
|
||||
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.uptime-graph rect {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.uptime-graph rect:hover {
|
||||
fill: #5e6c84;
|
||||
}
|
||||
</style>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Nitter Instance Health</title>
|
||||
</head>
|
||||
<body>
|
||||
</style>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Nitter Instance Health</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<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>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>Customize the visible columns <a href="#column-settings">down below</a>.</p>
|
||||
<div class="table-responsive">
|
||||
<table id="status-tbl" class="table sortable table-hover table-sm">
|
||||
<thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Instance</th>
|
||||
<th data-name="country" scope="col">Country</th>
|
||||
<th scope="col">Healthy</th>
|
||||
<th scope="col">Health History</th>
|
||||
<th data-name="average" scope="col">Average Time</th>
|
||||
<th scope="col">All Time %</th>
|
||||
<th data-name="rss" scope="col">RSS</th>
|
||||
<th data-name="version" scope="col">Nitter Version</th>
|
||||
<th data-name="connectivity" scope="col">Connectivity</th>
|
||||
<th scope="col">Points</th>
|
||||
<th scope="col">Instance</th>
|
||||
<th data-name="country" scope="col">Country</th>
|
||||
<th scope="col">Healthy</th>
|
||||
<th scope="col">Health History</th>
|
||||
<th data-name="average" scope="col">Average Time</th>
|
||||
<th scope="col">All Time %</th>
|
||||
<th data-name="rss" scope="col">RSS</th>
|
||||
<th data-name="version" scope="col">Nitter Version</th>
|
||||
<th data-name="connectivity" scope="col">Connectivity</th>
|
||||
<th scope="col">Points</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for host in instances -%}
|
||||
<tr>
|
||||
<td><a rel="nofollow external" href="{{host.url}}">{{host.domain}}</a></td>
|
||||
<td data-name="country">{{host.country}}</td>
|
||||
<td>
|
||||
{%- if host.is_bad_host -%}
|
||||
<div title="Host known for blocking healthchecks">❓</div>
|
||||
{%- elif host.healthy -%}
|
||||
✅
|
||||
{%- else -%}
|
||||
❌
|
||||
{%- endif -%}</td>
|
||||
<td>
|
||||
{%- set height = 28 -%}
|
||||
{%- set width = 110 -%}
|
||||
{%- set width_bar = 5 -%}
|
||||
{%- set offset = 5 -%}
|
||||
{%- if not host.__show_last_seen -%}
|
||||
<td><a rel="nofollow external" href="{{host.url}}">{{host.domain}}</a></td>
|
||||
<td data-name="country">{{host.country}}</td>
|
||||
<td>
|
||||
{%- if host.is_bad_host -%}
|
||||
<div title="Host known for blocking healthchecks">❓</div>
|
||||
{%- elif host.healthy -%}
|
||||
✅
|
||||
{%- else -%}
|
||||
❌
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td>
|
||||
{%- set height = 28 -%}
|
||||
{%- set width = 110 -%}
|
||||
{%- set width_bar = 5 -%}
|
||||
{%- set offset = 5 -%}
|
||||
{%- if not host.__show_last_seen -%}
|
||||
<svg class="uptime-graph" viewBox="0 0 {{width}} {{height}}" height="{{height}}">
|
||||
{%- for check in host.recent_checks -%}
|
||||
{%- if check.1 -%}
|
||||
{%- set title = "Healthy " ~ check.0 -%}
|
||||
{%- set color_bar = "#2fcc66" -%}
|
||||
{%- else -%}
|
||||
{%- set title = "Unhealthy " ~ check.0 -%}
|
||||
{%- set color_bar = "#ff6225" -%}
|
||||
{%- 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>
|
||||
{%- if check.1 -%}
|
||||
{%- set title = "Healthy " ~ check.0 -%}
|
||||
{%- set color_bar = "#2fcc66" -%}
|
||||
{%- else -%}
|
||||
{%- set title = "Unhealthy " ~ check.0 -%}
|
||||
{%- set color_bar = "#ff6225" -%}
|
||||
{%- 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>
|
||||
{%- endfor -%}
|
||||
</svg>
|
||||
{%- else -%}
|
||||
{%- else -%}
|
||||
{# <!-- no healthy entries, show last time host was seen 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>
|
||||
{%- else -%}
|
||||
Never seen healthy.
|
||||
{%- 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 -%}
|
||||
</td>
|
||||
<td data-name="connectivity">{{host.connectivity}}</td>
|
||||
<td>{{host.points}}</td>
|
||||
</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 -%}
|
||||
</td>
|
||||
<td data-name="connectivity">{{host.connectivity}}</td>
|
||||
<td>{{host.points}}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<noscript>Toggling table columns and sorting requires JS.</noscript>
|
||||
@@ -156,9 +168,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/bootstrap.min.js"></script>
|
||||
<link href="/static/sorting.css" rel="stylesheet" />
|
||||
<script src="/static/sortable.min.js"></script>
|
||||
<script src="/static/table_toggle.js"></script>
|
||||
</body>
|
||||
<script src="/static/bootstrap.min.js"></script>
|
||||
<link href="/static/sorting.css" rel="stylesheet" />
|
||||
<script src="/static/sortable.min.js"></script>
|
||||
<script src="/static/table_toggle.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,39 +1,28 @@
|
||||
{# SPDX-License-Identifier: AGPL-3.0-only #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="keywords" content="nitter, status, uptime" />
|
||||
<meta name="description" content="Nitter instance uptime and status tracker." />
|
||||
<meta name="author" content="" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dygraph.min.css" />
|
||||
<style>
|
||||
#history .dygraph-axis-label { color: white; }
|
||||
#history .dygraph-legend { background-color: #101015; }
|
||||
.overlay {
|
||||
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;
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="keywords" content="nitter, status, uptime" />
|
||||
<meta name="description" content="Nitter instance uptime and status tracker." />
|
||||
<meta name="author" content="" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dygraph.min.css" />
|
||||
<style>
|
||||
.dygraph-axis-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.overlay {
|
||||
background-color: rgba(0, 0, 0, 0.5); /* Dark overlay color for dark mode */
|
||||
}
|
||||
.dygraph-legend {
|
||||
background-color: #101015;
|
||||
}
|
||||
</style>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Nitter Is Dead</title>
|
||||
</head>
|
||||
<body>
|
||||
</style>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Nitter Is Dead</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>All Nitter public instances shut down.</h1>
|
||||
<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>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>
|
||||
<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>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="row">
|
||||
<div class="col">
|
||||
<div id="history" style="height: 400px;"></div>
|
||||
</div>
|
||||
</div>-->
|
||||
<button class="btn btn-primary" id="loadGraphBtn">Show Historical Data Graph (CPU & Network Traffic)</button>
|
||||
<div class="row">
|
||||
<div class="col" id="overlay">
|
||||
<button class="btn btn-primary" id="loadGraphBtn">Load & Display Graph</button>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<script type="text/javascript" src="/static/dygraph.min.js"></script>
|
||||
<script type="text/javascript" src="/static/history.2.js"></script>
|
||||
</body>
|
||||
<script type="text/javascript" src="/static/dygraph.min.js"></script>
|
||||
<script type="text/javascript" src="/static/history.2.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in new issue
Block a user