split timings, remove useless kvs

This commit is contained in:
nate 2026-04-10 07:10:11 +04:00
parent a6d0596c9e
commit 8c3cc3739a
9 changed files with 46 additions and 58 deletions

View File

@ -14,14 +14,11 @@ const StatusPageBody = t.Object({
index_search: t.Optional(t.Boolean()),
show_powered_by: t.Optional(t.Boolean()),
show_response_time: t.Optional(t.Boolean()),
show_cert_expiry: t.Optional(t.Boolean()),
bar_frequency: t.Optional(BarFrequency),
bar_count: t.Optional(t.Number({ minimum: 1, maximum: 180 })),
custom_domain: t.Optional(t.Nullable(t.String({ maxLength: 253 }))),
custom_css: t.Optional(t.Nullable(t.String({ maxLength: 50_000 }))),
footer_text: t.Optional(t.Nullable(t.String({ maxLength: 5000 }))),
og_image_url: t.Optional(t.Nullable(t.String({ maxLength: 2048 }))),
analytics_html: t.Optional(t.Nullable(t.String({ maxLength: 5000 }))),
auto_refresh_s: t.Optional(t.Number({ minimum: 10, maximum: 3600 })),
groups: t.Optional(t.Array(t.Object({
name: t.String({ minLength: 1, maxLength: 200 }),
@ -128,18 +125,17 @@ export const statusPages = new Elysia({ prefix: "/pages" })
[row] = await sql`
INSERT INTO status_pages (
account_id, slug, title, description, theme, password_hash, index_search,
show_powered_by, show_response_time, show_cert_expiry,
show_powered_by, show_response_time,
bar_frequency, bar_count,
custom_domain, custom_css, footer_text, og_image_url, analytics_html, auto_refresh_s
custom_domain, custom_css, footer_text, auto_refresh_s
)
VALUES (
${accountId}, ${body.slug}, ${body.title}, ${body.description ?? null},
${body.theme ?? 'auto'}, ${password_hash}, ${body.index_search ?? true},
${body.show_powered_by ?? true}, ${body.show_response_time ?? true},
${body.show_cert_expiry ?? false},
${body.bar_frequency ?? 'daily'}, ${body.bar_count ?? 90},
${body.custom_domain || null}, ${css}, ${body.footer_text ?? null}, ${body.og_image_url ?? null},
${body.analytics_html ?? null}, ${body.auto_refresh_s ?? 60}
${body.custom_domain || null}, ${css}, ${body.footer_text ?? null},
${body.auto_refresh_s ?? 60}
)
RETURNING *
`;
@ -191,14 +187,11 @@ export const statusPages = new Elysia({ prefix: "/pages" })
index_search = COALESCE(${body.index_search ?? null}, index_search),
show_powered_by = COALESCE(${body.show_powered_by ?? null}, show_powered_by),
show_response_time = COALESCE(${body.show_response_time ?? null}, show_response_time),
show_cert_expiry = COALESCE(${body.show_cert_expiry ?? null}, show_cert_expiry),
bar_frequency = COALESCE(${body.bar_frequency ?? null}, bar_frequency),
bar_count = COALESCE(${body.bar_count ?? null}, bar_count),
custom_domain = CASE WHEN ${body.custom_domain !== undefined} THEN ${body.custom_domain || null} ELSE custom_domain END,
custom_css = CASE WHEN ${body.custom_css !== undefined} THEN ${css} ELSE custom_css END,
footer_text = COALESCE(${body.footer_text ?? null}, footer_text),
og_image_url = COALESCE(${body.og_image_url ?? null}, og_image_url),
analytics_html = COALESCE(${body.analytics_html ?? null}, analytics_html),
auto_refresh_s = COALESCE(${body.auto_refresh_s ?? null}, auto_refresh_s),
updated_at = now()
WHERE id = ${params.id} AND account_id = ${accountId}

View File

@ -160,7 +160,6 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
});
let start = Instant::now();
let method = monitor.method.as_deref().unwrap_or("GET").to_uppercase();
let timeout = std::time::Duration::from_millis(monitor.timeout_ms.unwrap_or(30000));
let is_https = monitor.url.starts_with("https://");
@ -170,7 +169,7 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
let req_body = monitor.request_body.clone();
let max_redirects = monitor.max_redirects;
let (tx, rx) = tokio::sync::oneshot::channel::<Result<(u16, HashMap<String, String>, String), String>>();
let (tx, rx) = tokio::sync::oneshot::channel::<Result<(u16, HashMap<String, String>, String, u64, u64), String>>();
std::thread::spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
run_check_blocking(&url, &method, req_headers.as_ref(), req_body.as_deref(), timeout, max_redirects)
@ -187,10 +186,9 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
.and_then(|r| r.map_err(|_| "check thread dropped".to_string()))
.unwrap_or_else(|e| Err(e));
let latency_ms = start.elapsed().as_millis() as u64;
match result {
Err(ref e) => {
let latency_ms = start.elapsed().as_millis() as u64;
debug!("{} check error: {e}", monitor.url);
PingResult {
monitor_id: monitor.id.clone(),
@ -209,7 +207,7 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
run_id: Some(run_id.to_string()),
}
},
Ok((status, headers, body)) => {
Ok((status, headers, body, total_ms, dns_ms)) => {
let cert_handle = if is_https {
let cert_url = monitor.url.clone();
@ -233,7 +231,7 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
status,
body: body.clone(),
headers: headers.clone(),
latency_ms: Some(latency_ms),
latency_ms: Some(total_ms.saturating_sub(dns_ms)),
cert_expiry_days: None,
cert_issuer: None,
};
@ -253,14 +251,18 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
None => None,
};
let cert_expiry_days = cert_info.as_ref().map(|c| c.expiry_days);
let tls_ms = cert_info.as_ref().map(|c| c.tls_ms).unwrap_or(0);
let cert_issuer = cert_info.map(|c| c.issuer);
// Subtract DNS and TLS time from total to get server response time only
let latency_ms = total_ms.saturating_sub(dns_ms).saturating_sub(tls_ms);
let meta = json!({
"headers": headers,
"body_preview": &body[..body.len().min(25_000)],
});
debug!("{} → {status} {latency_ms}ms up={up}", monitor.url);
debug!("{} → {status} {latency_ms}ms (total={total_ms} dns={dns_ms} tls={tls_ms}) up={up}", monitor.url);
PingResult {
monitor_id: monitor.id.clone(),
@ -282,6 +284,7 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
}
}
// Returns (status, headers, body, total_ms, dns_ms)
fn run_check_blocking(
url: &str,
method: &str,
@ -289,7 +292,18 @@ fn run_check_blocking(
body: Option<&str>,
timeout: std::time::Duration,
max_redirects: u32,
) -> Result<(u16, HashMap<String, String>, String), String> {
) -> Result<(u16, HashMap<String, String>, String, u64, u64), String> {
// Measure DNS resolution time separately (lookup only, no connection)
let dns_ms = {
let parsed = reqwest::Url::parse(url).map_err(|e| e.to_string())?;
let host = parsed.host_str().unwrap_or("");
let port = parsed.port().unwrap_or(if parsed.scheme() == "https" { 443 } else { 80 });
let addr = format!("{host}:{port}");
let dns_start = Instant::now();
let _ = std::net::ToSocketAddrs::to_socket_addrs(&addr as &str);
dns_start.elapsed().as_millis() as u64
};
let root_certs = ROOT_CERTS.with(|c| Arc::clone(c));
let tls = ureq::tls::TlsConfig::builder()
@ -306,6 +320,8 @@ fn run_check_blocking(
.build()
.new_agent();
let request_start = Instant::now();
let mut builder = ureq::http::Request::builder()
.method(method)
.uri(url);
@ -368,12 +384,14 @@ fn run_check_blocking(
}
};
Ok((status, resp_headers, body_out))
let total_ms = request_start.elapsed().as_millis() as u64;
Ok((status, resp_headers, body_out, total_ms, dns_ms))
}
struct CertInfo {
expiry_days: i64,
issuer: String,
tls_ms: u64,
}
async fn check_cert(url: &str) -> Result<Option<CertInfo>> {
@ -398,7 +416,9 @@ async fn check_cert(url: &str) -> Result<Option<CertInfo>> {
let server_name = ServerName::try_from(host.to_string())?;
let stream = TcpStream::connect(format!("{host}:{port}")).await?;
let tls_start = Instant::now();
let tls_stream = connector.connect(server_name, stream).await?;
let tls_ms = tls_start.elapsed().as_millis() as u64;
let (_, conn) = tls_stream.get_ref();
let certs = conn.peer_certificates().unwrap_or(&[]);
@ -412,7 +432,7 @@ async fn check_cert(url: &str) -> Result<Option<CertInfo>> {
.as_secs() as i64;
let days = (not_after - now) / 86400;
let issuer = cert.issuer().to_string();
return Ok(Some(CertInfo { expiry_days: days, issuer }));
return Ok(Some(CertInfo { expiry_days: days, issuer, tls_ms }));
}
Ok(None)

View File

@ -129,14 +129,11 @@ export async function migrate(sql: any) {
index_search BOOLEAN NOT NULL DEFAULT true,
show_powered_by BOOLEAN NOT NULL DEFAULT true,
show_response_time BOOLEAN NOT NULL DEFAULT true,
show_cert_expiry BOOLEAN NOT NULL DEFAULT false,
bar_frequency TEXT NOT NULL DEFAULT 'daily',
bar_count INTEGER NOT NULL DEFAULT 90,
custom_domain TEXT UNIQUE,
custom_css TEXT,
footer_text TEXT,
og_image_url TEXT,
analytics_html TEXT,
auto_refresh_s INTEGER NOT NULL DEFAULT 60,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()

View File

@ -17,14 +17,12 @@ export interface StatusPageRow {
index_search: boolean;
show_powered_by: boolean;
show_response_time:boolean;
show_cert_expiry: boolean;
bar_frequency: BucketType;
bar_count: number;
custom_domain: string | null;
custom_css: string | null;
footer_text: string | null;
og_image_url: string | null;
analytics_html: string | null;
auto_refresh_s: number;
}
@ -498,11 +496,6 @@ export async function loadMonitorDetail(slug: string, monitorId: string): Promis
// The shape we actually expose to anonymous visitors. Computed by stripping
// internal IDs and any field a public consumer doesn't need from the row
// types - see redactPageForPublic / redactGroupsAndMonitors below.
//
// custom_css and analytics_html are kept here even though they're noisy to
// JSON consumers, because the HTML render reads from this same object - and
// they're already publicly visible in the rendered HTML, so dropping them
// from JSON wouldn't actually add any privacy.
export interface PublicPageView {
slug: string;
title: string;
@ -511,13 +504,9 @@ export interface PublicPageView {
index_search: boolean;
show_powered_by: boolean;
show_response_time: boolean;
show_cert_expiry: boolean;
bar_frequency: BucketType;
bar_count: number;
custom_css: string | null;
footer_text: string | null;
og_image_url: string | null;
analytics_html: string | null;
auto_refresh_s: number;
has_password: boolean;
}
@ -549,13 +538,9 @@ function redactPageForPublic(p: StatusPageRow): PublicPageView {
index_search: p.index_search,
show_powered_by: p.show_powered_by,
show_response_time: p.show_response_time,
show_cert_expiry: p.show_cert_expiry,
bar_frequency: p.bar_frequency,
bar_count: p.bar_count,
custom_css: p.custom_css,
footer_text: p.footer_text,
og_image_url: p.og_image_url,
analytics_html: p.analytics_html,
auto_refresh_s: p.auto_refresh_s,
has_password: !!p.password_hash,
};

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#0a0a0a"/>
<text x="3" y="23" font-family="system-ui, sans-serif" font-weight="700" font-size="18" fill="#e5e7eb">P</text>
<text x="14" y="23" font-family="system-ui, sans-serif" font-weight="700" font-size="18" fill="#3b82f6">Q</text>
<circle cx="26" cy="6" r="3" fill="#22c55e">
<animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@ -105,12 +105,11 @@
<% if (!page.index_search) { %><meta name="robots" content="noindex,nofollow"><% } %>
<meta property="og:title" content="<%= page.title %>">
<% if (page.description) { %><meta property="og:description" content="<%= page.description %>"><% } %>
<% if (page.og_image_url) { %><meta property="og:image" content="<%= page.og_image_url %>"><% } %>
<link rel="icon" href="/_static/favicon.svg" type="image/svg+xml">
<link rel="alternate" type="application/rss+xml" title="<%= page.title %> incidents" href="/<%= page.slug %>.rss">
<link rel="manifest" href="/<%= page.slug %>/manifest.json">
<link rel="stylesheet" href="/_static/app.css?v=<%= it.appCssHash %>">
<% if (page.custom_css) { %><style><%~ page.custom_css %></style><% } %>
<% if (page.analytics_html) { %><%~ page.analytics_html %><% } %>
</head>
<body>
<main>

View File

@ -738,11 +738,11 @@ export const dashboard = new Elysia()
bar_frequency: b.bar_frequency || "daily",
bar_count: Number(b.bar_count) || 90,
show_response_time: !!b.show_response_time,
show_cert_expiry: !!b.show_cert_expiry,
show_powered_by: !!b.show_powered_by,
index_search: !!b.index_search,
auto_refresh_s: Number(b.auto_refresh_s) || 60,
og_image_url: b.og_image_url || null,
password: b.password || undefined,
custom_domain: b.custom_domain || null,
custom_css: b.custom_css || null,
@ -771,11 +771,9 @@ export const dashboard = new Elysia()
bar_frequency: b.bar_frequency || "daily",
bar_count: Number(b.bar_count) || 90,
show_response_time: !!b.show_response_time,
show_cert_expiry: !!b.show_cert_expiry,
show_powered_by: !!b.show_powered_by,
index_search: !!b.index_search,
auto_refresh_s: Number(b.auto_refresh_s) || 60,
og_image_url: b.og_image_url || null,
custom_domain: b.custom_domain || null,
custom_css: b.custom_css || null,
footer_text: b.footer_text || null,

View File

@ -300,12 +300,9 @@ Content-Type: application/json
<tr><td>custom_domain</td><td>string?</td><td>Custom domain for the page (e.g. <code>status.example.com</code>). CNAME to the status server IP. SSL is automatic.</td></tr>
<tr><td>custom_css</td><td>string?</td><td>Custom CSS injected after the default stylesheet (up to 50KB).</td></tr>
<tr><td>footer_text</td><td>string?</td><td>Text shown in the page footer (up to 5000 chars).</td></tr>
<tr><td>og_image_url</td><td>string?</td><td>OpenGraph image URL for social previews.</td></tr>
<tr><td>analytics_html</td><td>string?</td><td>Raw HTML injected in the head (e.g. analytics snippet).</td></tr>
<tr><td>index_search</td><td>boolean?</td><td>Allow search engine indexing. Default <code>true</code>.</td></tr>
<tr><td>show_powered_by</td><td>boolean?</td><td>Show "Powered by PingQL" footer. Default <code>true</code>.</td></tr>
<tr><td>show_response_time</td><td>boolean?</td><td>Display response time per monitor. Default <code>true</code>.</td></tr>
<tr><td>show_cert_expiry</td><td>boolean?</td><td>Show cert expiry info. Default <code>false</code>.</td></tr>
<tr><td>groups</td><td>array?</td><td>Groups to organize monitors. Each has <code>name</code> (string) and optional <code>position</code> (number).</td></tr>
<tr><td>monitors</td><td>array?</td><td>Monitors to display. See monitor fields below.</td></tr>
</tbody>

View File

@ -223,10 +223,6 @@
<input type="checkbox" name="show_response_time" value="1" <%= (p.show_response_time !== false) ? 'checked' : '' %> class="accent-blue-500">
Show response time
</label>
<label class="flex items-center gap-2 text-sm text-gray-400">
<input type="checkbox" name="show_cert_expiry" value="1" <%= p.show_cert_expiry ? 'checked' : '' %> class="accent-blue-500">
Show cert expiry
</label>
<label class="flex items-center gap-2 text-sm text-gray-400">
<input type="checkbox" name="show_powered_by" value="1" <%= (p.show_powered_by !== false) ? 'checked' : '' %> class="accent-blue-500">
Show "Powered by PingQL"
@ -246,11 +242,6 @@
<% }) %>
</select>
</div>
<div class="flex-1">
<label class="block text-sm text-gray-400 mb-1.5">OG image URL <span class="text-gray-600">(optional)</span></label>
<input name="og_image_url" type="url" value="<%= p.og_image_url || '' %>" placeholder="https://example.com/og.png"
class="w-full bg-surface-solid border border-border-subtle rounded-lg px-4 py-2.5 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm">
</div>
</div>
<% const hasPassword = !it.isNew && p.password_hash; %>