split timings, remove useless kvs
This commit is contained in:
parent
a6d0596c9e
commit
8c3cc3739a
|
|
@ -14,14 +14,11 @@ const StatusPageBody = t.Object({
|
||||||
index_search: t.Optional(t.Boolean()),
|
index_search: t.Optional(t.Boolean()),
|
||||||
show_powered_by: t.Optional(t.Boolean()),
|
show_powered_by: t.Optional(t.Boolean()),
|
||||||
show_response_time: t.Optional(t.Boolean()),
|
show_response_time: t.Optional(t.Boolean()),
|
||||||
show_cert_expiry: t.Optional(t.Boolean()),
|
|
||||||
bar_frequency: t.Optional(BarFrequency),
|
bar_frequency: t.Optional(BarFrequency),
|
||||||
bar_count: t.Optional(t.Number({ minimum: 1, maximum: 180 })),
|
bar_count: t.Optional(t.Number({ minimum: 1, maximum: 180 })),
|
||||||
custom_domain: t.Optional(t.Nullable(t.String({ maxLength: 253 }))),
|
custom_domain: t.Optional(t.Nullable(t.String({ maxLength: 253 }))),
|
||||||
custom_css: t.Optional(t.Nullable(t.String({ maxLength: 50_000 }))),
|
custom_css: t.Optional(t.Nullable(t.String({ maxLength: 50_000 }))),
|
||||||
footer_text: t.Optional(t.Nullable(t.String({ maxLength: 5000 }))),
|
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 })),
|
auto_refresh_s: t.Optional(t.Number({ minimum: 10, maximum: 3600 })),
|
||||||
groups: t.Optional(t.Array(t.Object({
|
groups: t.Optional(t.Array(t.Object({
|
||||||
name: t.String({ minLength: 1, maxLength: 200 }),
|
name: t.String({ minLength: 1, maxLength: 200 }),
|
||||||
|
|
@ -128,18 +125,17 @@ export const statusPages = new Elysia({ prefix: "/pages" })
|
||||||
[row] = await sql`
|
[row] = await sql`
|
||||||
INSERT INTO status_pages (
|
INSERT INTO status_pages (
|
||||||
account_id, slug, title, description, theme, password_hash, index_search,
|
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,
|
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 (
|
VALUES (
|
||||||
${accountId}, ${body.slug}, ${body.title}, ${body.description ?? null},
|
${accountId}, ${body.slug}, ${body.title}, ${body.description ?? null},
|
||||||
${body.theme ?? 'auto'}, ${password_hash}, ${body.index_search ?? true},
|
${body.theme ?? 'auto'}, ${password_hash}, ${body.index_search ?? true},
|
||||||
${body.show_powered_by ?? true}, ${body.show_response_time ?? 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.bar_frequency ?? 'daily'}, ${body.bar_count ?? 90},
|
||||||
${body.custom_domain || null}, ${css}, ${body.footer_text ?? null}, ${body.og_image_url ?? null},
|
${body.custom_domain || null}, ${css}, ${body.footer_text ?? null},
|
||||||
${body.analytics_html ?? null}, ${body.auto_refresh_s ?? 60}
|
${body.auto_refresh_s ?? 60}
|
||||||
)
|
)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
@ -191,14 +187,11 @@ export const statusPages = new Elysia({ prefix: "/pages" })
|
||||||
index_search = COALESCE(${body.index_search ?? null}, index_search),
|
index_search = COALESCE(${body.index_search ?? null}, index_search),
|
||||||
show_powered_by = COALESCE(${body.show_powered_by ?? null}, show_powered_by),
|
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_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_frequency = COALESCE(${body.bar_frequency ?? null}, bar_frequency),
|
||||||
bar_count = COALESCE(${body.bar_count ?? null}, bar_count),
|
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_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,
|
custom_css = CASE WHEN ${body.custom_css !== undefined} THEN ${css} ELSE custom_css END,
|
||||||
footer_text = COALESCE(${body.footer_text ?? null}, footer_text),
|
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),
|
auto_refresh_s = COALESCE(${body.auto_refresh_s ?? null}, auto_refresh_s),
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = ${params.id} AND account_id = ${accountId}
|
WHERE id = ${params.id} AND account_id = ${accountId}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,6 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
||||||
});
|
});
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
let method = monitor.method.as_deref().unwrap_or("GET").to_uppercase();
|
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 timeout = std::time::Duration::from_millis(monitor.timeout_ms.unwrap_or(30000));
|
||||||
let is_https = monitor.url.starts_with("https://");
|
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 req_body = monitor.request_body.clone();
|
||||||
let max_redirects = monitor.max_redirects;
|
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 || {
|
std::thread::spawn(move || {
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
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)
|
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()))
|
.and_then(|r| r.map_err(|_| "check thread dropped".to_string()))
|
||||||
.unwrap_or_else(|e| Err(e));
|
.unwrap_or_else(|e| Err(e));
|
||||||
|
|
||||||
let latency_ms = start.elapsed().as_millis() as u64;
|
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Err(ref e) => {
|
Err(ref e) => {
|
||||||
|
let latency_ms = start.elapsed().as_millis() as u64;
|
||||||
debug!("{} check error: {e}", monitor.url);
|
debug!("{} check error: {e}", monitor.url);
|
||||||
PingResult {
|
PingResult {
|
||||||
monitor_id: monitor.id.clone(),
|
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()),
|
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_handle = if is_https {
|
||||||
let cert_url = monitor.url.clone();
|
let cert_url = monitor.url.clone();
|
||||||
|
|
@ -233,7 +231,7 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
||||||
status,
|
status,
|
||||||
body: body.clone(),
|
body: body.clone(),
|
||||||
headers: headers.clone(),
|
headers: headers.clone(),
|
||||||
latency_ms: Some(latency_ms),
|
latency_ms: Some(total_ms.saturating_sub(dns_ms)),
|
||||||
cert_expiry_days: None,
|
cert_expiry_days: None,
|
||||||
cert_issuer: None,
|
cert_issuer: None,
|
||||||
};
|
};
|
||||||
|
|
@ -253,14 +251,18 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let cert_expiry_days = cert_info.as_ref().map(|c| c.expiry_days);
|
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);
|
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!({
|
let meta = json!({
|
||||||
"headers": headers,
|
"headers": headers,
|
||||||
"body_preview": &body[..body.len().min(25_000)],
|
"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 {
|
PingResult {
|
||||||
monitor_id: monitor.id.clone(),
|
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(
|
fn run_check_blocking(
|
||||||
url: &str,
|
url: &str,
|
||||||
method: &str,
|
method: &str,
|
||||||
|
|
@ -289,7 +292,18 @@ fn run_check_blocking(
|
||||||
body: Option<&str>,
|
body: Option<&str>,
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
max_redirects: u32,
|
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 root_certs = ROOT_CERTS.with(|c| Arc::clone(c));
|
||||||
|
|
||||||
let tls = ureq::tls::TlsConfig::builder()
|
let tls = ureq::tls::TlsConfig::builder()
|
||||||
|
|
@ -306,6 +320,8 @@ fn run_check_blocking(
|
||||||
.build()
|
.build()
|
||||||
.new_agent();
|
.new_agent();
|
||||||
|
|
||||||
|
let request_start = Instant::now();
|
||||||
|
|
||||||
let mut builder = ureq::http::Request::builder()
|
let mut builder = ureq::http::Request::builder()
|
||||||
.method(method)
|
.method(method)
|
||||||
.uri(url);
|
.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 {
|
struct CertInfo {
|
||||||
expiry_days: i64,
|
expiry_days: i64,
|
||||||
issuer: String,
|
issuer: String,
|
||||||
|
tls_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_cert(url: &str) -> Result<Option<CertInfo>> {
|
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 server_name = ServerName::try_from(host.to_string())?;
|
||||||
|
|
||||||
let stream = TcpStream::connect(format!("{host}:{port}")).await?;
|
let stream = TcpStream::connect(format!("{host}:{port}")).await?;
|
||||||
|
let tls_start = Instant::now();
|
||||||
let tls_stream = connector.connect(server_name, stream).await?;
|
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 (_, conn) = tls_stream.get_ref();
|
||||||
let certs = conn.peer_certificates().unwrap_or(&[]);
|
let certs = conn.peer_certificates().unwrap_or(&[]);
|
||||||
|
|
@ -412,7 +432,7 @@ async fn check_cert(url: &str) -> Result<Option<CertInfo>> {
|
||||||
.as_secs() as i64;
|
.as_secs() as i64;
|
||||||
let days = (not_after - now) / 86400;
|
let days = (not_after - now) / 86400;
|
||||||
let issuer = cert.issuer().to_string();
|
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)
|
Ok(None)
|
||||||
|
|
|
||||||
|
|
@ -129,14 +129,11 @@ export async function migrate(sql: any) {
|
||||||
index_search BOOLEAN NOT NULL DEFAULT true,
|
index_search BOOLEAN NOT NULL DEFAULT true,
|
||||||
show_powered_by BOOLEAN NOT NULL DEFAULT true,
|
show_powered_by BOOLEAN NOT NULL DEFAULT true,
|
||||||
show_response_time 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_frequency TEXT NOT NULL DEFAULT 'daily',
|
||||||
bar_count INTEGER NOT NULL DEFAULT 90,
|
bar_count INTEGER NOT NULL DEFAULT 90,
|
||||||
custom_domain TEXT UNIQUE,
|
custom_domain TEXT UNIQUE,
|
||||||
custom_css TEXT,
|
custom_css TEXT,
|
||||||
footer_text TEXT,
|
footer_text TEXT,
|
||||||
og_image_url TEXT,
|
|
||||||
analytics_html TEXT,
|
|
||||||
auto_refresh_s INTEGER NOT NULL DEFAULT 60,
|
auto_refresh_s INTEGER NOT NULL DEFAULT 60,
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT now()
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,12 @@ export interface StatusPageRow {
|
||||||
index_search: boolean;
|
index_search: boolean;
|
||||||
show_powered_by: boolean;
|
show_powered_by: boolean;
|
||||||
show_response_time:boolean;
|
show_response_time:boolean;
|
||||||
show_cert_expiry: boolean;
|
|
||||||
bar_frequency: BucketType;
|
bar_frequency: BucketType;
|
||||||
bar_count: number;
|
bar_count: number;
|
||||||
custom_domain: string | null;
|
custom_domain: string | null;
|
||||||
custom_css: string | null;
|
custom_css: string | null;
|
||||||
footer_text: string | null;
|
footer_text: string | null;
|
||||||
og_image_url: string | null;
|
|
||||||
analytics_html: string | null;
|
|
||||||
auto_refresh_s: number;
|
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
|
// 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
|
// internal IDs and any field a public consumer doesn't need from the row
|
||||||
// types - see redactPageForPublic / redactGroupsAndMonitors below.
|
// 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 {
|
export interface PublicPageView {
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -511,13 +504,9 @@ export interface PublicPageView {
|
||||||
index_search: boolean;
|
index_search: boolean;
|
||||||
show_powered_by: boolean;
|
show_powered_by: boolean;
|
||||||
show_response_time: boolean;
|
show_response_time: boolean;
|
||||||
show_cert_expiry: boolean;
|
|
||||||
bar_frequency: BucketType;
|
bar_frequency: BucketType;
|
||||||
bar_count: number;
|
bar_count: number;
|
||||||
custom_css: string | null;
|
|
||||||
footer_text: string | null;
|
footer_text: string | null;
|
||||||
og_image_url: string | null;
|
|
||||||
analytics_html: string | null;
|
|
||||||
auto_refresh_s: number;
|
auto_refresh_s: number;
|
||||||
has_password: boolean;
|
has_password: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -549,13 +538,9 @@ function redactPageForPublic(p: StatusPageRow): PublicPageView {
|
||||||
index_search: p.index_search,
|
index_search: p.index_search,
|
||||||
show_powered_by: p.show_powered_by,
|
show_powered_by: p.show_powered_by,
|
||||||
show_response_time: p.show_response_time,
|
show_response_time: p.show_response_time,
|
||||||
show_cert_expiry: p.show_cert_expiry,
|
|
||||||
bar_frequency: p.bar_frequency,
|
bar_frequency: p.bar_frequency,
|
||||||
bar_count: p.bar_count,
|
bar_count: p.bar_count,
|
||||||
custom_css: p.custom_css,
|
|
||||||
footer_text: p.footer_text,
|
footer_text: p.footer_text,
|
||||||
og_image_url: p.og_image_url,
|
|
||||||
analytics_html: p.analytics_html,
|
|
||||||
auto_refresh_s: p.auto_refresh_s,
|
auto_refresh_s: p.auto_refresh_s,
|
||||||
has_password: !!p.password_hash,
|
has_password: !!p.password_hash,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -105,12 +105,11 @@
|
||||||
<% if (!page.index_search) { %><meta name="robots" content="noindex,nofollow"><% } %>
|
<% if (!page.index_search) { %><meta name="robots" content="noindex,nofollow"><% } %>
|
||||||
<meta property="og:title" content="<%= page.title %>">
|
<meta property="og:title" content="<%= page.title %>">
|
||||||
<% if (page.description) { %><meta property="og:description" content="<%= page.description %>"><% } %>
|
<% 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="alternate" type="application/rss+xml" title="<%= page.title %> incidents" href="/<%= page.slug %>.rss">
|
||||||
<link rel="manifest" href="/<%= page.slug %>/manifest.json">
|
<link rel="manifest" href="/<%= page.slug %>/manifest.json">
|
||||||
<link rel="stylesheet" href="/_static/app.css?v=<%= it.appCssHash %>">
|
<link rel="stylesheet" href="/_static/app.css?v=<%= it.appCssHash %>">
|
||||||
<% if (page.custom_css) { %><style><%~ page.custom_css %></style><% } %>
|
<% if (page.custom_css) { %><style><%~ page.custom_css %></style><% } %>
|
||||||
<% if (page.analytics_html) { %><%~ page.analytics_html %><% } %>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
|
||||||
|
|
@ -738,11 +738,11 @@ export const dashboard = new Elysia()
|
||||||
bar_frequency: b.bar_frequency || "daily",
|
bar_frequency: b.bar_frequency || "daily",
|
||||||
bar_count: Number(b.bar_count) || 90,
|
bar_count: Number(b.bar_count) || 90,
|
||||||
show_response_time: !!b.show_response_time,
|
show_response_time: !!b.show_response_time,
|
||||||
show_cert_expiry: !!b.show_cert_expiry,
|
|
||||||
show_powered_by: !!b.show_powered_by,
|
show_powered_by: !!b.show_powered_by,
|
||||||
index_search: !!b.index_search,
|
index_search: !!b.index_search,
|
||||||
auto_refresh_s: Number(b.auto_refresh_s) || 60,
|
auto_refresh_s: Number(b.auto_refresh_s) || 60,
|
||||||
og_image_url: b.og_image_url || null,
|
|
||||||
password: b.password || undefined,
|
password: b.password || undefined,
|
||||||
custom_domain: b.custom_domain || null,
|
custom_domain: b.custom_domain || null,
|
||||||
custom_css: b.custom_css || null,
|
custom_css: b.custom_css || null,
|
||||||
|
|
@ -771,11 +771,9 @@ export const dashboard = new Elysia()
|
||||||
bar_frequency: b.bar_frequency || "daily",
|
bar_frequency: b.bar_frequency || "daily",
|
||||||
bar_count: Number(b.bar_count) || 90,
|
bar_count: Number(b.bar_count) || 90,
|
||||||
show_response_time: !!b.show_response_time,
|
show_response_time: !!b.show_response_time,
|
||||||
show_cert_expiry: !!b.show_cert_expiry,
|
|
||||||
show_powered_by: !!b.show_powered_by,
|
show_powered_by: !!b.show_powered_by,
|
||||||
index_search: !!b.index_search,
|
index_search: !!b.index_search,
|
||||||
auto_refresh_s: Number(b.auto_refresh_s) || 60,
|
auto_refresh_s: Number(b.auto_refresh_s) || 60,
|
||||||
og_image_url: b.og_image_url || null,
|
|
||||||
custom_domain: b.custom_domain || null,
|
custom_domain: b.custom_domain || null,
|
||||||
custom_css: b.custom_css || null,
|
custom_css: b.custom_css || null,
|
||||||
footer_text: b.footer_text || null,
|
footer_text: b.footer_text || null,
|
||||||
|
|
|
||||||
|
|
@ -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_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>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>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>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_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_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>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>
|
<tr><td>monitors</td><td>array?</td><td>Monitors to display. See monitor fields below.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
|
|
@ -223,10 +223,6 @@
|
||||||
<input type="checkbox" name="show_response_time" value="1" <%= (p.show_response_time !== false) ? 'checked' : '' %> class="accent-blue-500">
|
<input type="checkbox" name="show_response_time" value="1" <%= (p.show_response_time !== false) ? 'checked' : '' %> class="accent-blue-500">
|
||||||
Show response time
|
Show response time
|
||||||
</label>
|
</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">
|
<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">
|
<input type="checkbox" name="show_powered_by" value="1" <%= (p.show_powered_by !== false) ? 'checked' : '' %> class="accent-blue-500">
|
||||||
Show "Powered by PingQL"
|
Show "Powered by PingQL"
|
||||||
|
|
@ -246,11 +242,6 @@
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<% const hasPassword = !it.isNew && p.password_hash; %>
|
<% const hasPassword = !it.isNew && p.password_hash; %>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue