remove em dashes

This commit is contained in:
2026-04-09 21:07:28 +04:00
parent 5f20b41e91
commit 79ba63d86b
31 changed files with 154 additions and 154 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ function watchAccount(onPing) {
// section is only present when an existing page already has a password set;
// the helper is a no-op on every other page. Lives here (rather than inline
// in the .eta template) because Eta's parser chokes on stray `%` characters
// inside <script> blocks see feedback_eta_no_inline_js.
// inside <script> blocks - see feedback_eta_no_inline_js.
function initPasswordSection() {
var section = document.querySelector('[data-password-section]');
if (!section) return;
+1 -1
View File
@@ -333,7 +333,7 @@ class QueryBuilder {
// Standard 2-space JSON prettifier with one twist: any subtree whose single-line
// form fits within MAX_INLINE chars at its current column gets inlined. Breaks
// always use clean 2-space indent never key-aligned.
// always use clean 2-space indent - never key-aligned.
const MAX_INLINE = 60;
function formatQueryJson(value, indent = 0, startCol = indent) {
+7 -7
View File
@@ -147,10 +147,10 @@ async function getAccountId(cookie: any, headers: any): Promise<{ accountId: str
}
// Parse the status page edit form's monitor list. The form posts:
// monitor_order full list of monitor IDs in DOM order (every row, not just checked)
// monitor_ids only the *checked* IDs, also in DOM order
// display_name[<id>] optional per-page name override
// display_mode[<id>] '', 'compact', or 'expanded'
// monitor_order - full list of monitor IDs in DOM order (every row, not just checked)
// monitor_ids - only the *checked* IDs, also in DOM order
// display_name[<id>] - optional per-page name override
// display_mode[<id>] - '', 'compact', or 'expanded'
// Bun's body parser surfaces bracket-keyed fields either as nested objects
// (`b.display_name = { id: value }`) or as flat string keys
// (`b['display_name[id]'] = value`) depending on parser version, so handle both.
@@ -225,7 +225,7 @@ export const dashboard = new Elysia()
if (key) {
const resolved = await resolveKey(key);
if (resolved) return redirect("/dashboard/home");
// Invalid/stale key clear it and show login
// Invalid/stale key - clear it and show login
cookie.pingql_key?.remove();
}
return html("login", {});
@@ -253,7 +253,7 @@ export const dashboard = new Elysia()
`;
// One simple indexed query per monitor, run in parallel. Each is an index
// seek on (monitor_id, checked_at DESC) microseconds. Trivially scalable
// seek on (monitor_id, checked_at DESC) - microseconds. Trivially scalable
// and easy to reason about.
const pingResults = await Promise.all(
monitors.map((m: any) => sql`
@@ -286,7 +286,7 @@ export const dashboard = new Elysia()
const isSubKey = !!keyId;
const loginKey = isSubKey ? null : (cookie?.pingql_key?.value ?? null);
// All four reads are independent fan them out in parallel instead of
// All four reads are independent - fan them out in parallel instead of
// serializing four round-trips. Each individual query is fast (PK seek or
// small indexed scan); the win is just halving the wall-clock by not
// waiting on each one in turn.
+2 -2
View File
@@ -99,7 +99,7 @@
<label class="block text-sm text-gray-400 mb-2">How many months?</label>
<select id="months-select" name="months" class="input-base px-4 py-2.5 text-gray-100">
<% for (let i = 1; i <= 12; i++) { %>
<option value="<%= i %>" data-base="<%= i * 12 %>"><%= i %> month<%= i > 1 ? 's' : '' %> $<%= i * 12 %></option>
<option value="<%= i %>" data-base="<%= i * 12 %>"><%= i %> month<%= i > 1 ? 's' : '' %> - $<%= i * 12 %></option>
<% } %>
</select>
</div>
@@ -315,7 +315,7 @@
const mult = multipliers[plan] || 1;
for (const opt of sel.options) {
const base = parseInt(opt.dataset.base);
opt.textContent = opt.value + ' month' + (opt.value > 1 ? 's' : '') + ' $' + (base * mult);
opt.textContent = opt.value + ' month' + (opt.value > 1 ? 's' : '') + ' - $' + (base * mult);
}
}
</script>
+18 -18
View File
@@ -56,19 +56,19 @@
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
<div class="stat-card rounded-xl p-4">
<div class="text-xs text-gray-500 mb-1">Status</div>
<div id="stat-status" class="text-lg font-semibold"><%~ lastPing ? (lastPing.up ? '<span class="text-green-400">Up</span>' : '<span class="text-red-400">Down</span>') : '<span class="text-gray-500"></span>' %></div>
<div id="stat-status" class="text-lg font-semibold"><%~ lastPing ? (lastPing.up ? '<span class="text-green-400">Up</span>' : '<span class="text-red-400">Down</span>') : '<span class="text-gray-500">-</span>' %></div>
</div>
<div class="stat-card rounded-xl p-4">
<div class="text-xs text-gray-500 mb-1">Avg Latency</div>
<div id="stat-latency" class="text-lg font-semibold text-gray-200"><%= avgLatency != null ? avgLatency + 'ms' : '' %></div>
<div id="stat-latency" class="text-lg font-semibold text-gray-200"><%= avgLatency != null ? avgLatency + 'ms' : '-' %></div>
</div>
<div class="stat-card rounded-xl p-4">
<div class="text-xs text-gray-500 mb-1">Uptime</div>
<div id="stat-uptime" class="text-lg font-semibold text-gray-200"><%= uptime != null ? uptime + '%' : '' %></div>
<div id="stat-uptime" class="text-lg font-semibold text-gray-200"><%= uptime != null ? uptime + '%' : '-' %></div>
</div>
<div class="stat-card rounded-xl p-4">
<div class="text-xs text-gray-500 mb-1">Last Ping</div>
<div id="stat-last" class="text-lg font-semibold text-gray-200"><%~ lastPing ? it.timeAgoSSR(lastPing.checked_at) : '' %></div>
<div id="stat-last" class="text-lg font-semibold text-gray-200"><%~ lastPing ? it.timeAgoSSR(lastPing.checked_at) : '-' %></div>
</div>
</div>
@@ -139,10 +139,10 @@
%>
<tr class="table-row-alt cursor-pointer hover:bg-white/[0.02]" data-ping="<%~ pingJson %>" data-up="<%= c.up ? '1' : '0' %>">
<td class="px-4 py-2"><%~ c.up ? '<span class="text-green-400">Up</span>' : '<span class="text-red-400">Down</span>' %></td>
<td class="px-4 py-2 text-gray-300"><%= c.status_code != null ? c.status_code : '' %></td>
<td class="px-4 py-2 text-gray-300"><%= c.latency_ms != null ? c.latency_ms + 'ms' : '' %></td>
<td class="px-4 py-2 text-gray-500 text-sm"><%= c.region || '' %></td>
<td class="px-4 py-2 text-gray-600 font-mono text-xs"><%= c.run_id || '' %></td>
<td class="px-4 py-2 text-gray-300"><%= c.status_code != null ? c.status_code : '-' %></td>
<td class="px-4 py-2 text-gray-300"><%= c.latency_ms != null ? c.latency_ms + 'ms' : '-' %></td>
<td class="px-4 py-2 text-gray-500 text-sm"><%= c.region || '-' %></td>
<td class="px-4 py-2 text-gray-600 font-mono text-xs"><%= c.run_id || '-' %></td>
<td class="px-4 py-2 text-gray-500"><%~ it.timeAgoSSR(c.checked_at) %><% if (c.jitter_ms != null) { %> <span class="text-gray-600 text-xs">(+<%= c.jitter_ms %>ms)</span><% } %></td>
<td class="px-4 py-2 text-red-400/70 text-xs truncate max-w-[200px]"><%= c.error ? c.error : '' %></td>
</tr>
@@ -393,7 +393,7 @@
html += '</div></div>';
}
// Response body loaded on demand
// Response body - loaded on demand
const contentType = getContentType(headers);
const ctLabel = contentType ? ` <span class="text-gray-600">(${escapeHtml(contentType.split(';')[0].trim())})</span>` : '';
html += '<div id="ping-body-section">';
@@ -600,7 +600,7 @@
ctx.beginPath();
ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
// Gentle Catmull-Rom spline low tension to avoid overshoot on noisy data
// Gentle Catmull-Rom spline - low tension to avoid overshoot on noisy data
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[Math.max(i - 1, 0)];
@@ -851,7 +851,7 @@
// Last ping
document.getElementById('stat-last').innerHTML = timeAgo(ping.checked_at);
// Status bar group by run_id, cap at 60
// Status bar - group by run_id, cap at 60
const bar = document.getElementById('status-bar');
if (bar) {
const rid = ping.run_id || ping.checked_at;
@@ -884,13 +884,13 @@
updateStatusTooltip();
}
// Pings table prepend row if filter matches
// Pings table - prepend row if filter matches
const tbody = document.getElementById('pings-table');
if (tbody) {
const shouldShow = _pingFilter === 'all'
|| (_pingFilter === 'up' && ping.up)
|| (_pingFilter === 'down' && !ping.up);
// For events filter, always prepend it's a state change if it differs from current status
// For events filter, always prepend - it's a state change if it differs from current status
const isEvent = _pingFilter === 'events';
if (shouldShow || isEvent) {
@@ -912,7 +912,7 @@
}
}
// Chart push new ping, trim oldest runs, re-render
// Chart - push new ping, trim oldest runs, re-render
chartPings.push({
latency_ms: ping.latency_ms, region: ping.region || '__none__',
checked_at: ping.checked_at, up: ping.up, run_id: ping.run_id || null,
@@ -930,10 +930,10 @@
function pingRowHtml(p) {
const up = p.up;
const statusHtml = up ? '<span class="text-green-400">Up</span>' : '<span class="text-red-400">Down</span>';
const code = p.status_code != null ? p.status_code : '';
const latency = p.latency_ms != null ? p.latency_ms + 'ms' : '';
const region = p.region || '';
const runId = p.run_id || '';
const code = p.status_code != null ? p.status_code : '-';
const latency = p.latency_ms != null ? p.latency_ms + 'ms' : '-';
const region = p.region || '-';
const runId = p.run_id || '-';
const time = timeAgo(p.checked_at);
const jitter = p.jitter_ms != null ? ` <span class="text-gray-600 text-xs">(+${p.jitter_ms}ms)</span>` : '';
const error = p.error ? escapeHtml(p.error) : '';
+43 -43
View File
@@ -10,7 +10,7 @@
.nav-link.active { color: #93c5fd; border-left-color: #3b82f6; }
.nav-section { font-size: 0.7rem; font-weight: 600; color: #374151; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.75rem 0.75rem 0.25rem; margin-top: 0.5rem; }
/* Content scoped to .docs */
/* Content - scoped to .docs */
.docs .section { padding-top: 3rem; padding-bottom: 1rem; border-top: 1px solid #111827; margin-top: 2rem; }
.docs .section:first-child { border-top: none; margin-top: 0; padding-top: 0; }
.docs h2 { font-size: 1.2rem; font-weight: 700; color: #f9fafb; margin-bottom: 0.5rem; }
@@ -105,11 +105,11 @@
<div class="endpoint"><span class="method post">POST</span><span class="path">/account/register</span></div>
<p class="endpoint-desc">Create a new account. Email is optional and used only for account recovery.</p>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json request body</span></div>
<div class="cb-header"><span class="cb-lang">json - request body</span></div>
<pre>{ <span class="k">"email"</span>: <span class="s">"you@example.com"</span> } <span class="c">// optional</span></pre>
</div>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json response</span></div>
<div class="cb-header"><span class="cb-lang">json - response</span></div>
<pre>{ <span class="k">"key"</span>: <span class="s">"5bf5311b56d09254c8a1f0e3..."</span>, <span class="k">"email_registered"</span>: <span class="n">true</span> }</pre>
</div>
@@ -117,7 +117,7 @@
<div class="endpoint"><span class="method post">POST</span><span class="path">/account/email</span></div>
<p class="endpoint-desc">Set or update the recovery email for an existing account.</p>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json request body</span></div>
<div class="cb-header"><span class="cb-lang">json - request body</span></div>
<pre>{ <span class="k">"email"</span>: <span class="s">"you@example.com"</span> }</pre>
</div>
</div>
@@ -133,22 +133,22 @@
<h3>Create</h3>
<div class="endpoint"><span class="method post">POST</span><span class="path">/monitors/</span></div>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json request body</span></div>
<div class="cb-header"><span class="cb-lang">json - request body</span></div>
<pre>{
<span class="k">"name"</span>: <span class="s">"My API"</span>,
<span class="k">"url"</span>: <span class="s">"https://api.example.com/health"</span>,
<span class="k">"interval_s"</span>: <span class="n">60</span>, <span class="c">// check every 60 seconds (min: 2)</span>
<span class="k">"method"</span>: <span class="s">"POST"</span>, <span class="c">// optional default: GET</span>
<span class="k">"method"</span>: <span class="s">"POST"</span>, <span class="c">// optional - default: GET</span>
<span class="k">"request_headers"</span>: { <span class="s">"X-Api-Key"</span>: <span class="s">"secret"</span> }, <span class="c">// optional</span>
<span class="k">"request_body"</span>: <span class="s">"{\"ping\": true}"</span>, <span class="c">// optional Content-Type defaults to application/json</span>
<span class="k">"regions"</span>: [<span class="s">"eu-central"</span>, <span class="s">"us-west"</span>], <span class="c">// optional default: all regions</span>
<span class="k">"timeout_ms"</span>: <span class="n">10000</span>, <span class="c">// optional default: 10000</span>
<span class="k">"max_retries"</span>: <span class="n">2</span>, <span class="c">// optional retry N times before declaring DOWN. Default: 0</span>
<span class="k">"retry_interval_s"</span>: <span class="n">30</span>, <span class="c">// optional seconds between retries. Default: 30</span>
<span class="k">"resend_interval"</span>: <span class="n">10</span>, <span class="c">// optional re-alert every Nth consecutive DOWN beat. 0 = never. Default: 0</span>
<span class="k">"cert_alert_days"</span>: <span class="n">0</span>, <span class="c">// optional alert when TLS cert is within N days of expiry. 0 disables. Default: 0 (disabled)</span>
<span class="k">"channel_ids"</span>: [<span class="s">"&lt;uuid&gt;"</span>], <span class="c">// optional notification channels to attach</span>
<span class="k">"query"</span>: { ... } <span class="c">// optional see Query Language below</span>
<span class="k">"request_body"</span>: <span class="s">"{\"ping\": true}"</span>, <span class="c">// optional - Content-Type defaults to application/json</span>
<span class="k">"regions"</span>: [<span class="s">"eu-central"</span>, <span class="s">"us-west"</span>], <span class="c">// optional - default: all regions</span>
<span class="k">"timeout_ms"</span>: <span class="n">10000</span>, <span class="c">// optional - default: 10000</span>
<span class="k">"max_retries"</span>: <span class="n">2</span>, <span class="c">// optional - retry N times before declaring DOWN. Default: 0</span>
<span class="k">"retry_interval_s"</span>: <span class="n">30</span>, <span class="c">// optional - seconds between retries. Default: 30</span>
<span class="k">"resend_interval"</span>: <span class="n">10</span>, <span class="c">// optional - re-alert every Nth consecutive DOWN beat. 0 = never. Default: 0</span>
<span class="k">"cert_alert_days"</span>: <span class="n">0</span>, <span class="c">// optional - alert when TLS cert is within N days of expiry. 0 disables. Default: 0 (disabled)</span>
<span class="k">"channel_ids"</span>: [<span class="s">"&lt;uuid&gt;"</span>], <span class="c">// optional - notification channels to attach</span>
<span class="k">"query"</span>: { ... } <span class="c">// optional - see Query Language below</span>
}</pre>
</div>
<table>
@@ -157,7 +157,7 @@
<tr><td>name</td><td>string</td><td>Display name for the monitor</td></tr>
<tr><td>url</td><td>string</td><td>URL to monitor</td></tr>
<tr><td>interval_s</td><td>number</td><td>Check interval in seconds (min: 30 free, 2 pro)</td></tr>
<tr><td>method</td><td>string</td><td>HTTP method GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS</td></tr>
<tr><td>method</td><td>string</td><td>HTTP method - GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS</td></tr>
<tr><td>request_headers</td><td>object</td><td>Custom headers as key-value pairs</td></tr>
<tr><td>request_body</td><td>string</td><td>Request body (Content-Type defaults to application/json)</td></tr>
<tr><td>regions</td><td>string[]</td><td>Regions to ping from: <code>eu-central</code>, <code>us-west</code>. Default: all</td></tr>
@@ -167,7 +167,7 @@
<tr><td>resend_interval</td><td>number</td><td>If a monitor stays DOWN, re-fire a notification every Nth consecutive down beat. 0 disables resend. Default: 0.</td></tr>
<tr><td>cert_alert_days</td><td>number</td><td>Fire a separate <code>cert</code> notification when the TLS certificate is within N days of expiring. 0 disables. Default: 0 (disabled).</td></tr>
<tr><td>channel_ids</td><td>string[]</td><td>Notification channel IDs to attach. See <a href="#notifications">Notifications</a>.</td></tr>
<tr><td>query</td><td>object</td><td>Query conditions see below</td></tr>
<tr><td>query</td><td>object</td><td>Query conditions - see below</td></tr>
</tbody>
</table>
@@ -188,7 +188,7 @@
<h3>Ping History</h3>
<div class="endpoint"><span class="method get">GET</span><span class="path">/monitors/:id/pings?limit=100</span></div>
<p class="endpoint-desc">Returns recent ping results for a monitor. Max 1000. Each ping carries an <code>important</code> boolean true on status transitions and resend ticks (the beats that triggered notifications).</p>
<p class="endpoint-desc">Returns recent ping results for a monitor. Max 1000. Each ping carries an <code>important</code> boolean - true on status transitions and resend ticks (the beats that triggered notifications).</p>
</div>
<!-- Notifications -->
@@ -203,16 +203,16 @@
<h3>Create channel</h3>
<div class="endpoint"><span class="method post">POST</span><span class="path">/notifications/channels</span></div>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json request body</span></div>
<div class="cb-header"><span class="cb-lang">json - request body</span></div>
<pre>{
<span class="k">"name"</span>: <span class="s">"On-call webhook"</span>,
<span class="k">"kind"</span>: <span class="s">"webhook"</span>,
<span class="k">"config"</span>: {
<span class="k">"url"</span>: <span class="s">"https://hooks.example.com/pingql"</span>,
<span class="k">"headers"</span>: { <span class="s">"X-Team"</span>: <span class="s">"infra"</span> }, <span class="c">// optional</span>
<span class="k">"secret"</span>: <span class="s">"shared-hmac-secret"</span> <span class="c">// optional signs payloads</span>
<span class="k">"secret"</span>: <span class="s">"shared-hmac-secret"</span> <span class="c">// optional - signs payloads</span>
},
<span class="k">"enabled"</span>: <span class="n">true</span> <span class="c">// optional default true</span>
<span class="k">"enabled"</span>: <span class="n">true</span> <span class="c">// optional - default true</span>
}</pre>
</div>
<table>
@@ -261,7 +261,7 @@ Content-Type: application/json
<h3>Create page</h3>
<div class="endpoint"><span class="method post">POST</span><span class="path">/status-pages</span></div>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json request body</span></div>
<div class="cb-header"><span class="cb-lang">json - request body</span></div>
<pre>{
<span class="k">"slug"</span>: <span class="s">"my-app"</span>,
<span class="k">"title"</span>: <span class="s">"My App Status"</span>,
@@ -353,7 +353,7 @@ Content-Type: application/json
<h3>Create incident</h3>
<div class="endpoint"><span class="method post">POST</span><span class="path">/incidents</span></div>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json request body</span></div>
<div class="cb-header"><span class="cb-lang">json - request body</span></div>
<pre>{
<span class="k">"title"</span>: <span class="s">"API returning 503s"</span>,
<span class="k">"status"</span>: <span class="s">"investigating"</span>,
@@ -389,10 +389,10 @@ Content-Type: application/json
<h3>Post update</h3>
<div class="endpoint"><span class="method post">POST</span><span class="path">/incidents/:id/updates</span></div>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json request body</span></div>
<div class="cb-header"><span class="cb-lang">json - request body</span></div>
<pre>{
<span class="k">"status"</span>: <span class="s">"identified"</span>,
<span class="k">"body"</span>: <span class="s">"Root cause identified a bad deploy at 14:02 UTC. Rolling back now."</span>
<span class="k">"body"</span>: <span class="s">"Root cause identified - a bad deploy at 14:02 UTC. Rolling back now."</span>
}</pre>
</div>
<p>The <code>body</code> field supports basic markdown: <code>**bold**</code>, <code>*italic*</code>, <code>`code`</code>, and <code>[links](url)</code>. The incident's status is automatically synced to the latest update's status.</p>
@@ -404,7 +404,7 @@ Content-Type: application/json
<!-- QL Fields -->
<div id="ql-fields" class="section">
<h2>Query Language Fields</h2>
<h2>Query Language - Fields</h2>
<p>A PingQL query is a JSON object evaluated against each ping. If it returns <code style="color:#4ade80;background:#052e16;padding:0.1em 0.35em;border-radius:0.2rem;font-size:0.78rem">true</code>, the monitor is <strong style="color:#4ade80">up</strong>. Default (no query): up only on a <strong>2xx</strong> status. Redirects and errors all count as DOWN.</p>
<table>
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
@@ -422,7 +422,7 @@ Content-Type: application/json
<!-- QL Operators -->
<div id="ql-operators" class="section">
<h2>Query Language Operators</h2>
<h2>Query Language - Operators</h2>
<table>
<thead><tr><th>Operator</th><th>Description</th><th>Types</th></tr></thead>
<tbody>
@@ -452,7 +452,7 @@ Content-Type: application/json
<!-- $json -->
<div id="ql-json" class="section">
<h2>$json JSONPath</h2>
<h2>$json - JSONPath</h2>
<p>Extract and compare a value from a JSON response body. The key is a dot-notation path starting with <code style="color:#93c5fd;background:#0f172a;padding:0.1em 0.35em;border-radius:0.2rem;font-size:0.78rem">$.</code></p>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json</span></div>
@@ -465,7 +465,7 @@ Content-Type: application/json
<!-- $select -->
<div id="ql-select" class="section">
<h2>$select CSS Selector</h2>
<h2>$select - CSS Selector</h2>
<p>Extract text content from an HTML response using a CSS selector. Useful for monitoring public pages without an API.</p>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json</span></div>
@@ -482,13 +482,13 @@ Content-Type: application/json
<h2>Logical Operators</h2>
<div class="cb">
<div class="cb-header"><span class="cb-lang">json</span></div>
<pre><span class="c">// $and all conditions must match</span>
<pre><span class="c">// $and - all conditions must match</span>
{ <span class="o">"$and"</span>: [{ <span class="k">"status"</span>: <span class="n">200</span> }, { <span class="k">"body"</span>: { <span class="o">"$contains"</span>: <span class="s">"ok"</span> } }] }
<span class="c">// $or any condition must match</span>
<span class="c">// $or - any condition must match</span>
{ <span class="o">"$or"</span>: [{ <span class="k">"status"</span>: <span class="n">200</span> }, { <span class="k">"status"</span>: <span class="n">204</span> }] }
<span class="c">// $not invert a condition</span>
<span class="c">// $not - invert a condition</span>
{ <span class="o">"$not"</span>: { <span class="k">"status"</span>: <span class="n">500</span> } }</pre>
</div>
</div>
@@ -546,7 +546,7 @@ Content-Type: application/json
<pre>{ <span class="o">"$select"</span>: { <span class="s">".status-indicator"</span>: { <span class="o">"$eq"</span>: <span class="s">"All systems operational"</span> } } }</pre></div>
<h3>Page down if a JSON queue is backed up</h3>
<p>The killer demo: alert when a JSON field crosses a threshold. Uptime Kuma's keyword/json-query checks can't compose this you'd need a script. Here it's one expression.</p>
<p>The killer demo: alert when a JSON field crosses a threshold. Uptime Kuma's keyword/json-query checks can't compose this - you'd need a script. Here it's one expression.</p>
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
<pre>{
<span class="o">"$consider"</span>: <span class="s">"down"</span>,
@@ -588,7 +588,7 @@ Content-Type: application/json
<p>If a check fails and <code>max_retries</code> is greater than zero, the runner waits <code>retry_interval_s</code> seconds and retries up to that many times <em>before</em> recording a DOWN result. A successful retry posts a single UP ping with <code>meta.retries</code> noting how many attempts it took. This kills almost all flapping caused by transient TCP resets, brief 5xx blips, or network jitter.</p>
<h3>Important beats &amp; transitions</h3>
<p>Every check is recorded, but the <code>important</code> flag on a ping is only set when the monitor's state changes (UP↔DOWN) <em>for that region</em>. Notifications fire on important beats only never on every routine check. State is tracked independently per region: if <code>us-west</code> goes DOWN, only a subsequent <code>us-west</code> UP clears it. <code>eu-central</code> being healthy will not silence a <code>us-west</code> outage.</p>
<p>Every check is recorded, but the <code>important</code> flag on a ping is only set when the monitor's state changes (UP↔DOWN) <em>for that region</em>. Notifications fire on important beats only - never on every routine check. State is tracked independently per region: if <code>us-west</code> goes DOWN, only a subsequent <code>us-west</code> UP clears it. <code>eu-central</code> being healthy will not silence a <code>us-west</code> outage.</p>
<h3>Resend interval</h3>
<p>For long outages, set <code>resend_interval</code> to re-fire the notification every Nth consecutive DOWN beat. With <code>resend_interval: 10</code>, a still-broken monitor produces an extra alert every 10 down checks. <code>0</code> (the default) means: alert once on the transition, then stay quiet until recovery.</p>
@@ -603,7 +603,7 @@ Content-Type: application/json
<!-- Webhook payload -->
<div id="webhook-payload" class="section">
<h2>Webhook payload</h2>
<p>Webhook channels POST a JSON body to the configured URL on every event. The HTTP method is <code>POST</code>, the request times out after 5 seconds, and PingQL does not retry the next important beat is the retry. Failures are logged but never block ingest.</p>
<p>Webhook channels POST a JSON body to the configured URL on every event. The HTTP method is <code>POST</code>, the request times out after 5 seconds, and PingQL does not retry - the next important beat is the retry. Failures are logged but never block ingest.</p>
<h3>Headers</h3>
<table>
@@ -627,15 +627,15 @@ Content-Type: application/json
</div>
<h3>Event types</h3>
<p><code>down</code> fired on the first DOWN important beat for a region, and again every <code>resend_interval</code>th consecutive down if configured.</p>
<div class="cb"><div class="cb-header"><span class="cb-lang">json event</span></div>
<p><code>down</code> - fired on the first DOWN important beat for a region, and again every <code>resend_interval</code>th consecutive down if configured.</p>
<div class="cb"><div class="cb-header"><span class="cb-lang">json - event</span></div>
<pre>{
<span class="k">"kind"</span>: <span class="s">"down"</span>,
<span class="k">"monitor"</span>: {
<span class="k">"id"</span>: <span class="s">"abc123def456"</span>,
<span class="k">"name"</span>: <span class="s">"My API"</span>,
<span class="k">"url"</span>: <span class="s">"https://api.example.com/health"</span>,
<span class="k">"region"</span>: <span class="s">"us-west"</span> <span class="c">// always present runners default to "default" if REGION env var is unset</span>
<span class="k">"region"</span>: <span class="s">"us-west"</span> <span class="c">// always present - runners default to "default" if REGION env var is unset</span>
},
<span class="k">"ping"</span>: {
<span class="k">"status_code"</span>: <span class="n">503</span>,
@@ -645,18 +645,18 @@ Content-Type: application/json
}
}</pre></div>
<p><code>up</code> fired on recovery, only when <em>that same region</em> transitions back from DOWN. Same shape as <code>down</code>.</p>
<p><code>up</code> - fired on recovery, only when <em>that same region</em> transitions back from DOWN. Same shape as <code>down</code>.</p>
<p><code>cert</code> fired once per renewal cycle when the TLS leaf cert drops at or below <code>cert_alert_days</code> for a region.</p>
<div class="cb"><div class="cb-header"><span class="cb-lang">json event</span></div>
<p><code>cert</code> - fired once per renewal cycle when the TLS leaf cert drops at or below <code>cert_alert_days</code> for a region.</p>
<div class="cb"><div class="cb-header"><span class="cb-lang">json - event</span></div>
<pre>{
<span class="k">"kind"</span>: <span class="s">"cert"</span>,
<span class="k">"monitor"</span>: { <span class="k">"id"</span>: <span class="s">"…"</span>, <span class="k">"name"</span>: <span class="s">"…"</span>, <span class="k">"url"</span>: <span class="s">"…"</span>, <span class="k">"region"</span>: <span class="s">"us-west"</span> },
<span class="k">"days"</span>: <span class="n">9</span> <span class="c">// days until certificate expires</span>
}</pre></div>
<p><code>test</code> synthetic event from <code>POST /notifications/channels/:id/test</code>. The <code>monitor</code> object is a placeholder.</p>
<div class="cb"><div class="cb-header"><span class="cb-lang">json event</span></div>
<p><code>test</code> - synthetic event from <code>POST /notifications/channels/:id/test</code>. The <code>monitor</code> object is a placeholder.</p>
<div class="cb"><div class="cb-header"><span class="cb-lang">json - event</span></div>
<pre>{
<span class="k">"kind"</span>: <span class="s">"test"</span>,
<span class="k">"monitor"</span>: { <span class="k">"id"</span>: <span class="s">"test"</span>, <span class="k">"name"</span>: <span class="s">"Test event"</span>, <span class="k">"url"</span>: <span class="s">"https://example.com"</span>, <span class="k">"region"</span>: <span class="s">""</span> }
+1 -1
View File
@@ -44,7 +44,7 @@
<div class="flex items-center gap-6 shrink-0 ml-4">
<div class="hidden sm:block stat-sparkline" style="width:120px;height:32px"><%~ it.sparklineSSR(pingsForSparkline) %></div>
<div class="text-right" style="width:72px">
<div class="text-sm text-gray-300 stat-latency"><%= avgLatency != null ? avgLatency + 'ms' : '' %></div>
<div class="text-sm text-gray-300 stat-latency"><%= avgLatency != null ? avgLatency + 'ms' : '-' %></div>
<div class="text-xs text-gray-500 stat-last"><%~ m.last_ping ? it.timeAgoSSR(m.last_ping.checked_at) : 'no pings' %></div>
</div>
<div class="text-xs px-2 py-1 rounded <%= m.enabled ? 'bg-gray-800/50 text-gray-400 border border-border-subtle' : 'bg-yellow-900/20 text-yellow-500 border border-yellow-800/30' %>"><%= m.enabled ? m.interval_s + 's' : 'paused' %></div>
+2 -2
View File
@@ -451,7 +451,7 @@
</div>
</div>
<div class="flex gap-[0.1rem] h-5">
<div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-yellow-500/70 demo-bar"><div class="demo-tip"><div class="tip-head">Mar 28, 14:00 15:00</div><div class="tip-row"><span>Checks</span><span>120</span></div><div class="tip-row"><span>Successful</span><span>118</span></div><div class="tip-row"><span>Uptime</span><span class="tip-warn">98.33%</span></div><div class="tip-row"><span>Avg ping</span><span>52ms</span></div></div></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div>
<div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-yellow-500/70 demo-bar"><div class="demo-tip"><div class="tip-head">Mar 28, 14:00 - 15:00</div><div class="tip-row"><span>Checks</span><span>120</span></div><div class="tip-row"><span>Successful</span><span>118</span></div><div class="tip-row"><span>Uptime</span><span class="tip-warn">98.33%</span></div><div class="tip-row"><span>Avg ping</span><span>52ms</span></div></div></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div>
</div>
</div>
@@ -483,7 +483,7 @@
</div>
</div>
<div class="flex gap-[0.1rem] h-5">
<div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-red-500/70 demo-bar"><div class="demo-tip"><div class="tip-head">Mar 22, 03:00 04:00</div><div class="tip-row"><span>Checks</span><span>120</span></div><div class="tip-row"><span>Successful</span><span>0</span></div><div class="tip-row"><span>Uptime</span><span class="tip-bad">0.00%</span></div><div class="tip-row"><span>Avg ping</span><span></span></div></div></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div>
<div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-red-500/70 demo-bar"><div class="demo-tip"><div class="tip-head">Mar 22, 03:00 - 04:00</div><div class="tip-row"><span>Checks</span><span>120</span></div><div class="tip-row"><span>Successful</span><span>0</span></div><div class="tip-row"><span>Uptime</span><span class="tip-bad">0.00%</span></div><div class="tip-row"><span>Avg ping</span><span>-</span></div></div></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div><div class="flex-1 rounded-sm bg-green-500/70"></div>
</div>
</div>
</div>
+2 -2
View File
@@ -20,7 +20,7 @@
<div class="card-static p-6" style="box-shadow:0 0 60px rgba(59,130,246,0.08)">
<!-- Sign in form works with or without JS -->
<!-- Sign in form - works with or without JS -->
<div id="screen-login">
<form id="login-form" action="/account/login" method="POST">
<input type="hidden" name="_form" value="1">
@@ -109,7 +109,7 @@
} catch { showError('Connection error'); }
});
// Register JS-enhanced (overrides form POST for inline key display)
// Register - JS-enhanced (overrides form POST for inline key display)
document.getElementById('register-form').addEventListener('submit', async (e) => {
e.preventDefault();
try {
+1 -1
View File
@@ -204,7 +204,7 @@
<script>
// JS enhancements clipboard copy (progressive, not required)
// JS enhancements - clipboard copy (progressive, not required)
function copyLoginKey() {
const val = document.getElementById('login-key-display').textContent;
navigator.clipboard.writeText(val);
+3 -3
View File
@@ -72,7 +72,7 @@
<div class="flex-1">
<label class="block text-sm text-gray-400 mb-1.5">Display mode</label>
<select name="display_mode" class="w-full bg-surface-solid border border-border-subtle rounded-lg px-4 py-2.5 text-gray-100 focus:outline-none focus:border-blue-500">
<% [['expanded','Expanded show full detail per monitor'],['compact','Compact one line per monitor, click to expand']].forEach(function([v, label]) { %>
<% [['expanded','Expanded - show full detail per monitor'],['compact','Compact - one line per monitor, click to expand']].forEach(function([v, label]) { %>
<option value="<%= v %>" <%= (p.display_mode || 'expanded') === v ? 'selected' : '' %>><%= label %></option>
<% }) %>
</select>
@@ -131,7 +131,7 @@
</div>
<script>
// Tiny vanilla drag-and-drop reorder. The DOM order at submit time is
// the canonical order each row carries a hidden "monitor_order" input
// the canonical order - each row carries a hidden "monitor_order" input
// that gets posted in DOM order naturally.
(function() {
const list = document.getElementById('monitor-list');
@@ -195,7 +195,7 @@
<div data-password-input-wrap class="hidden mt-2">
<input data-password-input name="password" type="password" placeholder="New password"
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">
<button type="button" data-password-cancel class="mt-1 text-xs text-gray-500 hover:text-gray-300">Cancel keep current password</button>
<button type="button" data-password-cancel class="mt-1 text-xs text-gray-500 hover:text-gray-300">Cancel - keep current password</button>
</div>
<div data-password-removing class="hidden mt-2 text-xs text-red-400">
Password will be removed when you save.