remove em dashes
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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">"<uuid>"</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">"<uuid>"</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 & 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> }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user