clean up
This commit is contained in:
+8
-80
@@ -4,7 +4,6 @@
|
||||
|
||||
import sql from "./db";
|
||||
|
||||
export type Window = "24h" | "7d" | "30d" | "90d";
|
||||
export type BucketType = "hourly" | "daily";
|
||||
|
||||
export interface StatusPageRow {
|
||||
@@ -19,7 +18,6 @@ export interface StatusPageRow {
|
||||
show_powered_by: boolean;
|
||||
show_response_time:boolean;
|
||||
show_cert_expiry: boolean;
|
||||
display_mode: "compact" | "expanded";
|
||||
bar_frequency: BucketType;
|
||||
bar_count: number;
|
||||
custom_css: string | null;
|
||||
@@ -29,13 +27,6 @@ export interface StatusPageRow {
|
||||
auto_refresh_s: number;
|
||||
}
|
||||
|
||||
export interface MultiWindowUptime {
|
||||
d24: number | null;
|
||||
d7: number | null;
|
||||
d30: number | null;
|
||||
d90: number | null;
|
||||
}
|
||||
|
||||
export interface MonitorRow {
|
||||
id: string;
|
||||
display_name: string;
|
||||
@@ -49,14 +40,12 @@ export interface MonitorRow {
|
||||
// looks up groups by this token.
|
||||
group_id: string | null;
|
||||
position: number;
|
||||
display_mode: "compact" | "expanded"; // resolved (per-monitor override → page default → 'expanded')
|
||||
// 'paused' means the monitor was disabled in the dashboard - the runner has
|
||||
// stopped checking it, and the public page should treat it as planned
|
||||
// maintenance rather than an outage.
|
||||
current_state: "up" | "down" | "unknown" | "paused";
|
||||
region_states: Array<{ region: string; state: "up" | "down" | "unknown"; updated_at: string | null }>;
|
||||
uptime_pct: number | null;
|
||||
uptime: MultiWindowUptime; // 24h / 7d / 30d / 90d row
|
||||
buckets: Array<{ start: string; total: number; up: number; avg_latency: number | null }>; // bar chart input
|
||||
avg_latency: number | null;
|
||||
latency_history: Array<{ region: string; latency_ms: number | null; ts: string }>;
|
||||
@@ -107,8 +96,6 @@ export async function loadGroups(pageId: string): Promise<GroupRow[]> {
|
||||
|
||||
export async function loadMonitors(
|
||||
pageId: string,
|
||||
window: Window,
|
||||
pageDisplayMode: "compact" | "expanded" = "expanded",
|
||||
barFrequency: BucketType = "daily",
|
||||
barCount: number = 90,
|
||||
): Promise<MonitorRow[]> {
|
||||
@@ -126,7 +113,6 @@ export async function loadMonitors(
|
||||
m.enabled AS enabled,
|
||||
spm.group_id,
|
||||
spm.position,
|
||||
spm.display_mode AS spm_display_mode
|
||||
FROM status_page_monitors spm
|
||||
JOIN monitors m ON m.id = spm.monitor_id
|
||||
WHERE spm.status_page_id = ${pageId}
|
||||
@@ -230,33 +216,14 @@ export async function loadMonitors(
|
||||
// barIndexed[mid][isoStart] → cross-region {total, up} for bar coloring (only rows of barFrequency)
|
||||
// barRegionLat[mid][region] → weighted latency over the bar window for picking fastest region
|
||||
// barRegionBucketLat[mid][region][iso] → per-bucket latency in the fastest region (only rows of barFrequency)
|
||||
// windowTotals[mid][windowKey] → {up, total} per uptime window (24h/7d/30d/90d)
|
||||
// latByMonitor[mid][] → 30h hourly latency sparkline rows
|
||||
const barIndexed: Record<string, Record<string, { total: number; up: number }>> = {};
|
||||
const barRegionLat: Record<string, Record<string, { sum: number; n: number }>> = {};
|
||||
const barRegionBucketLat: Record<string, Record<string, Record<string, number>>> = {};
|
||||
|
||||
type WindowKey = "d24" | "d7" | "d30" | "d90";
|
||||
const windowTotals: Record<string, Record<WindowKey, { up: number; total: number }>> = {};
|
||||
const initWindowTotals = (mid: string) => {
|
||||
if (!windowTotals[mid]) {
|
||||
windowTotals[mid] = {
|
||||
d24: { up: 0, total: 0 },
|
||||
d7: { up: 0, total: 0 },
|
||||
d30: { up: 0, total: 0 },
|
||||
d90: { up: 0, total: 0 },
|
||||
};
|
||||
}
|
||||
return windowTotals[mid]!;
|
||||
};
|
||||
|
||||
const latByMonitor: Record<string, MonitorRow["latency_history"]> = {};
|
||||
|
||||
const nowMs = Date.now();
|
||||
const ms24h = 24 * 3600_000;
|
||||
const ms7d = 7 * 86_400_000;
|
||||
const ms30d = 30 * 86_400_000;
|
||||
const ms90d = 90 * 86_400_000;
|
||||
const ms30h = 30 * 3600_000;
|
||||
|
||||
for (const r of rollupRows) {
|
||||
@@ -290,20 +257,6 @@ export async function loadMonitors(
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-window uptime accumulators. 24h uses hourly buckets; 7d/30d/90d
|
||||
// use daily buckets - same as the old loadMultiWindowUptime SQL did.
|
||||
// Strict `<` to match the old SQL's `bucket_start > now() - interval`.
|
||||
const wt = initWindowTotals(mid);
|
||||
if (bt === "hourly" && nowMs - startMs < ms24h) {
|
||||
wt.d24.up += up; wt.d24.total += total;
|
||||
}
|
||||
if (bt === "daily") {
|
||||
const age = nowMs - startMs;
|
||||
if (age < ms7d) { wt.d7.up += up; wt.d7.total += total; }
|
||||
if (age < ms30d) { wt.d30.up += up; wt.d30.total += total; }
|
||||
if (age < ms90d) { wt.d90.up += up; wt.d90.total += total; }
|
||||
}
|
||||
|
||||
// 30h hourly latency sparkline.
|
||||
if (bt === "hourly" && nowMs - startMs < ms30h) {
|
||||
if (!latByMonitor[mid]) latByMonitor[mid] = [];
|
||||
@@ -370,21 +323,6 @@ export async function loadMonitors(
|
||||
});
|
||||
}
|
||||
|
||||
// Multi-window uptime is a straight read from the windowTotals accumulator.
|
||||
// We deliberately do NOT round to 2 decimals here - the formatter on the
|
||||
// public page truncates (not rounds) to 2 decimals so a value like 99.9999%
|
||||
// doesn't visually round up to "100.00%". Pre-rounding here would erase that
|
||||
// information before the formatter ever sees it.
|
||||
const multiWindow: Record<string, MultiWindowUptime> = {};
|
||||
const toPct = (up: number, total: number): number | null =>
|
||||
total > 0 ? (100 * up / total) : null;
|
||||
for (const id of ids) {
|
||||
const wt = windowTotals[id];
|
||||
multiWindow[id] = wt
|
||||
? { d24: toPct(wt.d24.up, wt.d24.total), d7: toPct(wt.d7.up, wt.d7.total), d30: toPct(wt.d30.up, wt.d30.total), d90: toPct(wt.d90.up, wt.d90.total) }
|
||||
: { d24: null, d7: null, d30: null, d90: null };
|
||||
}
|
||||
|
||||
const latencyByMonitorList = latByMonitor;
|
||||
|
||||
return monitorRows.map((m) => {
|
||||
@@ -409,20 +347,14 @@ export async function loadMonitors(
|
||||
uptime_pct = tot > 0 ? (100 * upT / tot) : null;
|
||||
}
|
||||
const avg_latency = fastestLatency[m.id] ?? null;
|
||||
// Per-monitor display mode override → page default → 'expanded'.
|
||||
const display_mode = (m.spm_display_mode === 'compact' || m.spm_display_mode === 'expanded')
|
||||
? m.spm_display_mode
|
||||
: pageDisplayMode;
|
||||
return {
|
||||
id: m.id,
|
||||
display_name: m.display_name,
|
||||
group_id: m.group_id,
|
||||
position: m.position,
|
||||
display_mode,
|
||||
current_state,
|
||||
region_states,
|
||||
uptime_pct,
|
||||
uptime: multiWindow[m.id] ?? { d24: null, d7: null, d30: null, d90: null },
|
||||
buckets,
|
||||
avg_latency,
|
||||
latency_history: latencyByMonitorList[m.id] ?? [],
|
||||
@@ -483,7 +415,7 @@ export interface MonitorDetailPayload {
|
||||
generated_at: string;
|
||||
}
|
||||
|
||||
export async function loadMonitorDetail(slug: string, monitorId: string, window?: Window): Promise<MonitorDetailPayload | null> {
|
||||
export async function loadMonitorDetail(slug: string, monitorId: string): Promise<MonitorDetailPayload | null> {
|
||||
const page = await loadStatusPage(slug);
|
||||
if (!page) return null;
|
||||
// Existence check only - confirm the monitor is actually attached to this
|
||||
@@ -497,14 +429,13 @@ export async function loadMonitorDetail(slug: string, monitorId: string, window?
|
||||
`;
|
||||
if (!link) return null;
|
||||
|
||||
const win = (window ?? '24h') as Window;
|
||||
// Reuse the bulk loader with a single-monitor list - keeps the bucket/state
|
||||
// logic in one place. Cheap because we're querying for one ID. We also need
|
||||
// the page's groups so we can redact the monitor's group_id (UUID → public
|
||||
// position-as-string token), matching what /:slug.json emits.
|
||||
const [allGroups, allMonitors] = await Promise.all([
|
||||
loadGroups(page.id),
|
||||
loadMonitors(page.id, win, page.display_mode, page.bar_frequency, page.bar_count),
|
||||
loadMonitors(page.id, page.bar_frequency, page.bar_count),
|
||||
]);
|
||||
const { monitors } = redactGroupsAndMonitors(allGroups, allMonitors);
|
||||
const m = monitors.find((x) => x.id === monitorId);
|
||||
@@ -570,7 +501,6 @@ export interface PublicPageView {
|
||||
show_powered_by: boolean;
|
||||
show_response_time: boolean;
|
||||
show_cert_expiry: boolean;
|
||||
display_mode: "compact" | "expanded";
|
||||
bar_frequency: BucketType;
|
||||
bar_count: number;
|
||||
custom_css: string | null;
|
||||
@@ -609,7 +539,6 @@ function redactPageForPublic(p: StatusPageRow): PublicPageView {
|
||||
show_powered_by: p.show_powered_by,
|
||||
show_response_time: p.show_response_time,
|
||||
show_cert_expiry: p.show_cert_expiry,
|
||||
display_mode: p.display_mode,
|
||||
bar_frequency: p.bar_frequency,
|
||||
bar_count: p.bar_count,
|
||||
custom_css: p.custom_css,
|
||||
@@ -635,20 +564,19 @@ function redactGroupsAndMonitors(
|
||||
name: g.name,
|
||||
position: g.position,
|
||||
}));
|
||||
const publicMonitors = monitors.map((m) => {
|
||||
const { uptime, ...rest } = m;
|
||||
return { ...rest, group_id: m.group_id ? (idMap.get(m.group_id) ?? null) : null };
|
||||
});
|
||||
const publicMonitors = monitors.map((m) => ({
|
||||
...m,
|
||||
group_id: m.group_id ? (idMap.get(m.group_id) ?? null) : null,
|
||||
}));
|
||||
return { groups: publicGroups, monitors: publicMonitors };
|
||||
}
|
||||
|
||||
export async function loadPagePayload(slug: string, window?: Window): Promise<PagePayload | null> {
|
||||
export async function loadPagePayload(slug: string): Promise<PagePayload | null> {
|
||||
const page = await loadStatusPage(slug);
|
||||
if (!page) return null;
|
||||
const win = (window ?? '24h') as Window;
|
||||
const [rawGroups, rawMonitors, incidents] = await Promise.all([
|
||||
loadGroups(page.id),
|
||||
loadMonitors(page.id, win, page.display_mode, page.bar_frequency, page.bar_count),
|
||||
loadMonitors(page.id, page.bar_frequency, page.bar_count),
|
||||
loadIncidents(page.id),
|
||||
]);
|
||||
const { groups, monitors } = redactGroupsAndMonitors(rawGroups, rawMonitors);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Eta } from "eta";
|
||||
import { resolve } from "path";
|
||||
import { createHash } from "crypto";
|
||||
import sql from "./db";
|
||||
import { loadStatusPage, loadPagePayload, loadMonitorDetail, type Window } from "./data";
|
||||
import { loadStatusPage, loadPagePayload, loadMonitorDetail } from "./data";
|
||||
import { renderRss } from "./render/rss";
|
||||
import { renderBadge, badgeFromState } from "./render/badge";
|
||||
import { cached } from "./cache";
|
||||
@@ -132,7 +132,7 @@ async function renderHtml(slug: string, request: Request): Promise<Response> {
|
||||
return new Response(html, { headers });
|
||||
}
|
||||
|
||||
async function renderJson(slug: string, request: Request, win?: Window): Promise<Response> {
|
||||
async function renderJson(slug: string, request: Request): Promise<Response> {
|
||||
// Same as renderHtml: never cache the page row. The auth check has to see
|
||||
// the live password_hash, otherwise rotating a password leaves a 15s
|
||||
// window where old cookies still validate against the stale row.
|
||||
@@ -143,8 +143,7 @@ async function renderJson(slug: string, request: Request, win?: Window): Promise
|
||||
// fall back to HTML scraping (which gets the password form, also a 401-ish
|
||||
// signal but more expensive to parse and less stable).
|
||||
if (!page || !isAuthorised(page, request)) return jsonNotFound();
|
||||
const cacheKey = `payload:${slug}:${win ?? '24h'}`;
|
||||
const payload = await cached(cacheKey, 15, () => loadPagePayload(slug, win));
|
||||
const payload = await cached(`payload:${slug}`, 15, () => loadPagePayload(slug));
|
||||
if (!payload) return jsonNotFound();
|
||||
// Password-protected JSON must be private - same reasoning as renderHtml.
|
||||
const cacheControl = page.password_hash
|
||||
@@ -208,7 +207,7 @@ const app = new Elysia()
|
||||
.get("/:slug", async ({ params, request, query }) => {
|
||||
const { slug, format } = splitSlugAndFormat(params.slug);
|
||||
if (!allow(slug, clientIp(request))) return rateLimited();
|
||||
if (format === "json") return renderJson(slug, request, (query as any)?.window as Window | undefined);
|
||||
if (format === "json") return renderJson(slug, request);
|
||||
if (format === "rss") return renderRssResp(slug, request);
|
||||
return renderHtml(slug, request);
|
||||
})
|
||||
@@ -253,9 +252,8 @@ const app = new Elysia()
|
||||
if (!page || !isAuthorised(page, request)) return jsonNotFound();
|
||||
const idWithExt = params.idWithExt;
|
||||
const monitorId = idWithExt.endsWith(".json") ? idWithExt.slice(0, -5) : idWithExt;
|
||||
const win = (query as any)?.window as Window | undefined;
|
||||
const cacheKey = `monitor:${params.slug}:${monitorId}:${win ?? ''}`;
|
||||
const payload = await cached(cacheKey, 15, () => loadMonitorDetail(params.slug, monitorId, win));
|
||||
const cacheKey = `monitor:${params.slug}:${monitorId}`;
|
||||
const payload = await cached(cacheKey, 15, () => loadMonitorDetail(params.slug, monitorId));
|
||||
if (!payload) return jsonNotFound();
|
||||
const cacheControl = page.password_hash
|
||||
? "private, no-store, must-revalidate"
|
||||
|
||||
@@ -55,10 +55,6 @@ h1 { font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem; }
|
||||
.group-body { display: none; padding: 0 0.75rem 0.75rem; }
|
||||
.group-toggle:checked ~ .group-body { display: block; }
|
||||
.group-body .monitors { gap: 0.35rem; }
|
||||
/* Compact monitor inside a group - no collapse, no uptime cells */
|
||||
.monitor-compact { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 0.75rem 1rem; }
|
||||
.monitor-compact-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 0.5rem; }
|
||||
.monitor-compact .bars { height: 24px; }
|
||||
.page-components { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.monitors { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
/* All monitors share one structure: a clickable header row + a collapsible
|
||||
@@ -94,22 +90,8 @@ h1 { font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem; }
|
||||
#bar-tooltip .pct.warn { color: var(--bar-partial); }
|
||||
#bar-tooltip .pct.bad { color: var(--bar-down); }
|
||||
/* Multi-window uptime row: four labelled cells side by side. */
|
||||
.uptime-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.5rem; margin-top: 0.75rem; }
|
||||
.uptime-cell { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.65rem; }
|
||||
.uptime-cell .label { font-size: 0.65rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.2rem; }
|
||||
.uptime-cell .value { font-variant-numeric: tabular-nums; font-weight: 600; font-size: 0.95rem; }
|
||||
.uptime-cell.good .value { color: var(--bar-up); }
|
||||
.uptime-cell.warn .value { color: var(--bar-partial); }
|
||||
.uptime-cell.bad .value { color: var(--bar-down); }
|
||||
.uptime-cell.empty .value { color: var(--muted); }
|
||||
/* Header row is always clickable; detail panel collapses/expands via hidden checkbox. */
|
||||
.monitor-toggle { display: none; }
|
||||
.monitor-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem; cursor: pointer; background: transparent; border: none; color: inherit; width: 100%; text-align: left; font-family: inherit; font-size: inherit; }
|
||||
.monitor-row:hover { background: rgba(255,255,255,0.02); }
|
||||
.monitor-row .chev { color: var(--muted); transition: transform 0.15s; flex-shrink: 0; }
|
||||
.monitor-toggle:checked ~ .monitor-row .chev { transform: rotate(90deg); }
|
||||
.monitor-detail { padding: 0 1.25rem 1rem; border-top: 1px solid var(--border); display: none; }
|
||||
.monitor-toggle:checked ~ .monitor-detail { display: block; padding-top: 1rem; }
|
||||
.monitor-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem 0; }
|
||||
.monitor .bars { padding: 0.5rem 1.25rem 1rem; }
|
||||
.regions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; font-size: 0.75rem; }
|
||||
.region { padding: 0.15rem 0.5rem; border-radius: 999px; border: 1px solid var(--border); }
|
||||
.region.up { color: var(--green); border-color: rgba(16,185,129,0.3); }
|
||||
@@ -160,7 +142,6 @@ h1 { font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem; }
|
||||
.past-update-body p { display: inline; margin: 0; }
|
||||
.past-update-body code { background: var(--bg); padding: 0.05em 0.3em; border-radius: 3px; font-size: 0.85em; }
|
||||
.past-update-time { color: var(--muted); font-size: 0.7rem; margin-top: 0.15rem; }
|
||||
.page-uptime { margin-top: 2rem; }
|
||||
footer { padding-top: 2rem; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.8rem; text-align: center; }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
@@ -95,19 +95,6 @@
|
||||
: overall === 'degraded' ? 'rgba(245,158,11,0.4)'
|
||||
: 'rgba(239,68,68,0.4)';
|
||||
|
||||
// Page-wide aggregate uptime - average across active monitors
|
||||
const pageUptimeAcc = { d24: { sum: 0, n: 0 }, d7: { sum: 0, n: 0 }, d30: { sum: 0, n: 0 }, d90: { sum: 0, n: 0 } };
|
||||
for (const m of monitors) {
|
||||
if (m.current_state === 'paused') continue;
|
||||
const u = m.uptime || {};
|
||||
for (const key of ['d24', 'd7', 'd30', 'd90']) {
|
||||
if (u[key] != null) { pageUptimeAcc[key].sum += u[key]; pageUptimeAcc[key].n++; }
|
||||
}
|
||||
}
|
||||
function pageUptimePct(key) {
|
||||
const a = pageUptimeAcc[key];
|
||||
return a.n > 0 ? (a.sum / a.n) : null;
|
||||
}
|
||||
%><!DOCTYPE html>
|
||||
<html lang="en" class="<%= themeClass %>">
|
||||
<head>
|
||||
@@ -182,11 +169,6 @@
|
||||
<% } %>
|
||||
|
||||
<%
|
||||
// Per-monitor mode now lives on each MonitorRow.display_mode (already
|
||||
// resolved against the page-level fallback by the data layer). The
|
||||
// page-level isCompact is kept only as a hint for whether to emit the
|
||||
// expand JS at all.
|
||||
const anyCompact = monitors.some(m => m.display_mode === 'compact');
|
||||
const windowLabel = page.bar_frequency === 'hourly'
|
||||
? ('Last ' + page.bar_count + ' hour' + (page.bar_count === 1 ? '' : 's'))
|
||||
: ('Last ' + page.bar_count + ' day' + (page.bar_count === 1 ? '' : 's'));
|
||||
@@ -253,35 +235,9 @@
|
||||
<div class="monitors">
|
||||
<% list.forEach(function(m) {
|
||||
const buckets = m.buckets || [];
|
||||
const hasData = buckets.some(b => b.total > 0);
|
||||
const startsOpen = m.display_mode !== 'compact';
|
||||
%>
|
||||
<% if (inGroup) { %>
|
||||
<div class="monitor-compact" data-monitor-id="<%= m.id %>">
|
||||
<div class="monitor-compact-header">
|
||||
<div class="monitor-name">
|
||||
<span class="dot" style="background: <%= statusColor(m.current_state) %>;"></span>
|
||||
<span class="name"><%= m.display_name %></span>
|
||||
</div>
|
||||
<div class="monitor-meta">
|
||||
<% if (m.current_state === 'paused') { %>
|
||||
<span class="maintenance-pill" title="This service is paused for maintenance - checks are not running.">Maintenance</span>
|
||||
<% } else { %>
|
||||
<% if (page.show_response_time && m.avg_latency != null) { %><span><%= m.avg_latency %>ms</span><% } %>
|
||||
<span class="uptime-pct <%= uptimeBand(m.uptime_pct) %>"><%= fmtUptime(m.uptime_pct) %></span>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bars" aria-label="<%= statusLabel(m.current_state) %>">
|
||||
<% buckets.forEach(function(b) { %>
|
||||
<div class="bar" style="background: <%= bucketColor(b) %>;" data-start="<%= b.start %>" data-total="<%= b.total %>" data-up="<%= b.up %>"<% if (b.avg_latency != null) { %> data-latency="<%= b.avg_latency %>"<% } %>></div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="monitor" data-monitor-id="<%= m.id %>">
|
||||
<input type="checkbox" class="monitor-toggle" id="m-<%= m.id %>" <%= startsOpen ? 'checked' : '' %>>
|
||||
<label class="monitor-row" for="m-<%= m.id %>">
|
||||
<div class="monitor-header">
|
||||
<div class="monitor-name">
|
||||
<span class="dot" style="background: <%= statusColor(m.current_state) %>;"></span>
|
||||
<span class="name"><%= m.display_name %></span>
|
||||
@@ -293,22 +249,14 @@
|
||||
<% if (page.show_response_time && m.avg_latency != null) { %><span><%= m.avg_latency %>ms</span><% } %>
|
||||
<span class="uptime-pct <%= uptimeBand(m.uptime_pct) %>"><%= fmtUptime(m.uptime_pct) %></span>
|
||||
<% } %>
|
||||
<svg class="chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</div>
|
||||
</label>
|
||||
<div class="monitor-detail">
|
||||
<div class="bars" aria-label="<%= statusLabel(m.current_state) %>">
|
||||
<% buckets.forEach(function(b) { %>
|
||||
<div class="bar" style="background: <%= bucketColor(b) %>;" data-start="<%= b.start %>" data-total="<%= b.total %>" data-up="<%= b.up %>"<% if (b.avg_latency != null) { %> data-latency="<%= b.avg_latency %>"<% } %>></div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<div class="bars-meta">
|
||||
<span><%= windowLabel %></span>
|
||||
<span><%= hasData ? 'uptime over window' : 'awaiting data' %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bars" aria-label="<%= statusLabel(m.current_state) %>">
|
||||
<% buckets.forEach(function(b) { %>
|
||||
<div class="bar" style="background: <%= bucketColor(b) %>;" data-start="<%= b.start %>" data-total="<%= b.total %>" data-up="<%= b.up %>"<% if (b.avg_latency != null) { %> data-latency="<%= b.avg_latency %>"<% } %>></div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% if (groupName) { %>
|
||||
@@ -362,15 +310,6 @@
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="page-uptime">
|
||||
<div class="uptime-row">
|
||||
<div class="uptime-cell <%= uptimeBand(pageUptimePct('d24')) %>"><div class="label">24h</div><div class="value"><%= fmtUptime(pageUptimePct('d24')) %></div></div>
|
||||
<div class="uptime-cell <%= uptimeBand(pageUptimePct('d7')) %>"><div class="label">7d</div><div class="value"><%= fmtUptime(pageUptimePct('d7')) %></div></div>
|
||||
<div class="uptime-cell <%= uptimeBand(pageUptimePct('d30')) %>"><div class="label">30d</div><div class="value"><%= fmtUptime(pageUptimePct('d30')) %></div></div>
|
||||
<div class="uptime-cell <%= uptimeBand(pageUptimePct('d90')) %>"><div class="label">90d</div><div class="value"><%= fmtUptime(pageUptimePct('d90')) %></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<% if (page.footer_text) { %><div><%= page.footer_text %></div><% } %>
|
||||
<% if (page.show_powered_by) { %><div>Status powered by <a href="https://pingql.com">PingQL</a></div><% } %>
|
||||
|
||||
Reference in New Issue
Block a user