display both graph, load on click, warn on the instance site

This commit is contained in:
4cuck committed 2024-02-20 00:17:38 +01:00
1 parent 87230a3703
commit c6028ef899
7 files changed
+413 -254

No files matched your search

+23 -37
View File
@@ -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> {
/// 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> {
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();
}
}
}
+66 -10
View File
@@ -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)]
@@ -75,24 +75,80 @@ 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> {
/// 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> {
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
}
}
+56 -13
View File
@@ -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,33 @@ 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());
res.headers_mut().insert(
"cache-control",
HeaderValue::from_str(&format!("public, max-age={}", config.max_age)).unwrap(),
@@ -70,4 +71,46 @@ pub async fn graph(
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(),
);
res.headers_mut().insert(
"X-Robots-Tag",
HeaderValue::from_static("noindex, nofollow"),
);
Ok(res)
}
+4 -4
View File
@@ -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
View File
@@ -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.";
}
}
+107 -94
View File
@@ -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">&gt;&gt;Read More&lt;&lt;</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>
</html>
<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>
+35 -46
View File
@@ -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>
</html>
<script type="text/javascript" src="/static/dygraph.min.js"></script>
<script type="text/javascript" src="/static/history.2.js"></script>
</body>
</html>