fix various issues
This commit is contained in:
@@ -426,25 +426,25 @@ export const dashboard = new Elysia()
|
||||
const resolved = await getAccountId(cookie, headers);
|
||||
const accountId = resolved?.accountId ?? null;
|
||||
if (!accountId) return redirect("/dashboard");
|
||||
const channels = await sql`
|
||||
SELECT id, name, kind FROM notification_channels
|
||||
WHERE account_id = ${accountId} AND enabled = true
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
return html("new", { nav: "monitors", plan: resolved?.plan || "free", channels });
|
||||
return html("new", { nav: "monitors", plan: resolved?.plan || "free" });
|
||||
})
|
||||
|
||||
.get("/dashboard/notifications", async ({ cookie, headers, query }) => {
|
||||
const resolved = await getAccountId(cookie, headers);
|
||||
if (!resolved?.accountId) return redirect("/dashboard");
|
||||
const channels = await sql`
|
||||
SELECT id, name, kind, config, enabled, created_at
|
||||
SELECT id, name, kind, config, enabled, monitor_ids, created_at
|
||||
FROM notification_channels
|
||||
WHERE account_id = ${resolved.accountId}
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
const monitors = await sql`
|
||||
SELECT id, name FROM monitors
|
||||
WHERE account_id = ${resolved.accountId}
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
const testResult = query.test === "ok" ? { ok: true } : query.test_error ? { ok: false, error: String(query.test_error) } : null;
|
||||
return html("notifications", { nav: "notifications", channels, testResult });
|
||||
return html("notifications", { nav: "notifications", channels, monitors, testResult });
|
||||
})
|
||||
|
||||
.post("/dashboard/notifications/new", async ({ cookie, headers, body }) => {
|
||||
@@ -457,13 +457,31 @@ export const dashboard = new Elysia()
|
||||
config.url = (b.url || "").trim();
|
||||
if (b.secret) config.secret = b.secret;
|
||||
}
|
||||
const monitorIds = Array.isArray(b.monitor_ids) ? b.monitor_ids : (b.monitor_ids ? [b.monitor_ids] : []);
|
||||
try {
|
||||
const apiUrl = process.env.API_URL || "https://api.pingql.com";
|
||||
const key = cookie?.pingql_key?.value;
|
||||
await fetch(`${apiUrl}/notifications/channels/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
|
||||
body: JSON.stringify({ name: (b.name || "").trim(), kind, config }),
|
||||
body: JSON.stringify({ name: (b.name || "").trim(), kind, config, monitor_ids: monitorIds }),
|
||||
});
|
||||
} catch {}
|
||||
return redirect("/dashboard/notifications");
|
||||
})
|
||||
|
||||
.post("/dashboard/notifications/:id/edit", async ({ cookie, headers, params, body }) => {
|
||||
const resolved = await getAccountId(cookie, headers);
|
||||
if (!resolved?.accountId) return redirect("/dashboard");
|
||||
const b = body as any;
|
||||
const monitorIds = Array.isArray(b.monitor_ids) ? b.monitor_ids : (b.monitor_ids ? [b.monitor_ids] : []);
|
||||
try {
|
||||
const apiUrl = process.env.API_URL || "https://api.pingql.com";
|
||||
const key = cookie?.pingql_key?.value;
|
||||
await fetch(`${apiUrl}/notifications/channels/${params.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
|
||||
body: JSON.stringify({ monitor_ids: monitorIds }),
|
||||
});
|
||||
} catch {}
|
||||
return redirect("/dashboard/notifications");
|
||||
@@ -530,14 +548,8 @@ export const dashboard = new Elysia()
|
||||
SELECT * FROM pings WHERE monitor_id = ${params.id}
|
||||
ORDER BY checked_at DESC LIMIT 200
|
||||
`;
|
||||
const channels = await sql`
|
||||
SELECT id, name, kind FROM notification_channels
|
||||
WHERE account_id = ${accountId} AND enabled = true
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
// channel_ids is already on the monitor row as a UUID[] column
|
||||
|
||||
return html("detail", { nav: "monitors", monitor, pings, plan: resolved?.plan || "free", channels });
|
||||
return html("detail", { nav: "monitors", monitor, pings, plan: resolved?.plan || "free" });
|
||||
})
|
||||
|
||||
.get("/dashboard/monitors/:id/chart", async ({ cookie, headers, params }) => {
|
||||
@@ -613,7 +625,6 @@ export const dashboard = new Elysia()
|
||||
resend_interval: Number(b.resend_interval) || 0,
|
||||
cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 0,
|
||||
max_redirects: b.max_redirects != null ? Number(b.max_redirects) : 1,
|
||||
channel_ids: Array.isArray(b.channel_ids) ? b.channel_ids : (b.channel_ids ? [b.channel_ids] : []),
|
||||
regions,
|
||||
request_headers: Object.keys(requestHeaders).length ? requestHeaders : null,
|
||||
request_body: b.request_body || null,
|
||||
@@ -656,7 +667,6 @@ export const dashboard = new Elysia()
|
||||
resend_interval: Number(b.resend_interval) || 0,
|
||||
cert_alert_days: b.cert_alert_days != null ? Number(b.cert_alert_days) : 0,
|
||||
max_redirects: b.max_redirects != null ? Number(b.max_redirects) : 1,
|
||||
channel_ids: Array.isArray(b.channel_ids) ? b.channel_ids : (b.channel_ids ? [b.channel_ids] : []),
|
||||
regions,
|
||||
request_headers: Object.keys(requestHeaders).length ? requestHeaders : null,
|
||||
request_body: b.request_body || null,
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
<!-- Edit form -->
|
||||
<div class="card-static p-6">
|
||||
<h3 class="text-sm text-gray-400 mb-4">Edit Monitor</h3>
|
||||
<%~ include('./partials/monitor-form', { _form: { monitor: m, isEdit: true, prefix: 'edit-', bg: 'bg-gray-800/50', border: 'border-border-subtle' }, plan: it.plan, regions: it.regions, channels: it.channels }) %>
|
||||
<%~ include('./partials/monitor-form', { _form: { monitor: m, isEdit: true, prefix: 'edit-', bg: 'bg-gray-800/50', border: 'border-border-subtle' }, plan: it.plan, regions: it.regions }) %>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+106
-106
@@ -134,21 +134,20 @@
|
||||
<div class="cb">
|
||||
<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">"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">"max_redirects"</span>: <span class="n">1</span>, <span class="c">// optional - follow up to N redirects (0-4). Default: 1</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">"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">"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">"max_redirects"</span>: <span class="n">1</span>, <span class="c">// optional - follow up to N redirects (0-4). Default: 1</span>
|
||||
<span class="k">"query"</span>: { ... } <span class="c">// optional - see Query Language below</span>
|
||||
}</pre>
|
||||
</div>
|
||||
<table>
|
||||
@@ -167,7 +166,6 @@
|
||||
<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>max_redirects</td><td>number</td><td>Follow up to N redirects (0-4). 0 = don't follow. Default: 1.</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>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -204,7 +202,7 @@
|
||||
<!-- Notifications -->
|
||||
<div id="notifications" class="section">
|
||||
<h2>Notifications</h2>
|
||||
<p>Notification channels are reusable destinations attached to monitors. When an important beat fires (DOWN, recovery, or cert), each attached channel is dispatched. PingQL ships with a <strong>webhook</strong> provider; more (Discord, Slack, Email) are designed as drop-ins.</p>
|
||||
<p>Each notification channel holds a list of monitors it dispatches for. When an important beat fires (DOWN, recovery, or cert) on one of its attached monitors, the channel is dispatched. PingQL ships with a <strong>webhook</strong> provider; more (Discord, Slack, Email) are designed as drop-ins.</p>
|
||||
|
||||
<h3>List channels</h3>
|
||||
<div class="endpoint"><span class="method get">GET</span><span class="path">/notifications/channels</span></div>
|
||||
@@ -215,14 +213,15 @@
|
||||
<div class="cb">
|
||||
<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">"enabled"</span>: <span class="n">true</span> <span class="c">// optional - default true</span>
|
||||
<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">"monitor_ids"</span>: [<span class="s">"<monitor-id>"</span>], <span class="c">// optional - monitors to dispatch for</span>
|
||||
<span class="k">"enabled"</span>: <span class="n">true</span> <span class="c">// optional - default true</span>
|
||||
}</pre>
|
||||
</div>
|
||||
<table>
|
||||
@@ -232,12 +231,13 @@
|
||||
<tr><td>kind</td><td>string</td><td>Provider type. Currently only <code>webhook</code>.</td></tr>
|
||||
<tr><td>config</td><td>object</td><td>Provider-specific config. For <code>webhook</code>, requires <code>url</code> (http/https). Optional <code>headers</code> object and <code>secret</code> for HMAC signing.</td></tr>
|
||||
<tr><td>enabled</td><td>boolean</td><td>Disabled channels are skipped during dispatch but remain attached.</td></tr>
|
||||
<tr><td>monitor_ids</td><td>string[]</td><td>Monitor IDs this channel dispatches for. See <a href="#notifications">Attaching monitors</a> below.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Update channel</h3>
|
||||
<div class="endpoint"><span class="method patch">PATCH</span><span class="path">/notifications/channels/:id</span></div>
|
||||
<p class="endpoint-desc">All fields optional. Provide a partial body to change name, config, or enabled state.</p>
|
||||
<p class="endpoint-desc">All fields optional. Provide a partial body to change name, config, enabled state, or <code>monitor_ids</code>.</p>
|
||||
|
||||
<h3>Delete channel</h3>
|
||||
<div class="endpoint"><span class="method delete">DELETE</span><span class="path">/notifications/channels/:id</span></div>
|
||||
@@ -247,15 +247,15 @@
|
||||
<div class="endpoint"><span class="method post">POST</span><span class="path">/notifications/channels/:id/test</span></div>
|
||||
<p class="endpoint-desc">Sends a synthetic <code>test</code> event through the channel and returns whether the provider accepted it. Useful to verify the URL and HMAC are wired correctly without waiting for a real outage.</p>
|
||||
|
||||
<h3>Attaching channels to monitors</h3>
|
||||
<p>Pass <code>channel_ids</code> as an array of channel UUIDs when creating or patching a monitor. The PATCH replaces the full set; pass an empty array to detach all channels. Channels can only be attached to monitors in the same account.</p>
|
||||
<h3>Attaching monitors to channels</h3>
|
||||
<p>Pass <code>monitor_ids</code> as an array of monitor IDs when creating or patching a channel. The PATCH replaces the full set; pass an empty array to detach all monitors. Monitors can only be attached to channels in the same account.</p>
|
||||
<div class="cb">
|
||||
<div class="cb-header"><span class="cb-lang">http</span></div>
|
||||
<pre>PATCH /monitors/abc123def456
|
||||
<pre>PATCH /notifications/channels/5fb1c0bf-…
|
||||
Authorization: Bearer <key>
|
||||
Content-Type: application/json
|
||||
|
||||
{ <span class="k">"channel_ids"</span>: [<span class="s">"5fb1c0bf-…"</span>, <span class="s">"a72e0d91-…"</span>] }</pre>
|
||||
{ <span class="k">"monitor_ids"</span>: [<span class="s">"abc123def456"</span>, <span class="s">"f0e1d2c3b4a59687"</span>] }</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -273,16 +273,16 @@ Content-Type: application/json
|
||||
<div class="cb">
|
||||
<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>,
|
||||
<span class="k">"groups"</span>: [
|
||||
{ <span class="k">"name"</span>: <span class="s">"Core"</span>, <span class="k">"position"</span>: <span class="n">0</span> },
|
||||
{ <span class="k">"name"</span>: <span class="s">"Integrations"</span>, <span class="k">"position"</span>: <span class="n">1</span> }
|
||||
],
|
||||
<span class="k">"monitors"</span>: [
|
||||
{ <span class="k">"monitor_id"</span>: <span class="s">"a1b2c3d4e5f67890"</span>, <span class="k">"group_index"</span>: <span class="n">0</span> },
|
||||
{ <span class="k">"monitor_id"</span>: <span class="s">"f0e1d2c3b4a59687"</span>, <span class="k">"group_index"</span>: <span class="n">1</span>, <span class="k">"display_name"</span>: <span class="s">"Stripe"</span> }
|
||||
]
|
||||
<span class="k">"slug"</span>: <span class="s">"my-app"</span>,
|
||||
<span class="k">"title"</span>: <span class="s">"My App Status"</span>,
|
||||
<span class="k">"groups"</span>: [
|
||||
{ <span class="k">"name"</span>: <span class="s">"Core"</span>, <span class="k">"position"</span>: <span class="n">0</span> },
|
||||
{ <span class="k">"name"</span>: <span class="s">"Integrations"</span>, <span class="k">"position"</span>: <span class="n">1</span> }
|
||||
],
|
||||
<span class="k">"monitors"</span>: [
|
||||
{ <span class="k">"monitor_id"</span>: <span class="s">"a1b2c3d4e5f67890"</span>, <span class="k">"group_index"</span>: <span class="n">0</span> },
|
||||
{ <span class="k">"monitor_id"</span>: <span class="s">"f0e1d2c3b4a59687"</span>, <span class="k">"group_index"</span>: <span class="n">1</span>, <span class="k">"display_name"</span>: <span class="s">"Stripe"</span> }
|
||||
]
|
||||
}</pre>
|
||||
</div>
|
||||
<table>
|
||||
@@ -361,14 +361,14 @@ Content-Type: application/json
|
||||
<div class="cb">
|
||||
<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>,
|
||||
<span class="k">"severity"</span>: <span class="s">"major"</span>,
|
||||
<span class="k">"monitor_ids"</span>: [<span class="s">"a1b2c3d4e5f67890"</span>],
|
||||
<span class="k">"status_page_ids"</span>: [<span class="s">"f8c1a2b3-..."</span>],
|
||||
<span class="k">"initial_update"</span>: {
|
||||
<span class="k">"body"</span>: <span class="s">"We're seeing elevated error rates on the API. Investigating now."</span>
|
||||
}
|
||||
<span class="k">"title"</span>: <span class="s">"API returning 503s"</span>,
|
||||
<span class="k">"status"</span>: <span class="s">"investigating"</span>,
|
||||
<span class="k">"severity"</span>: <span class="s">"major"</span>,
|
||||
<span class="k">"monitor_ids"</span>: [<span class="s">"a1b2c3d4e5f67890"</span>],
|
||||
<span class="k">"status_page_ids"</span>: [<span class="s">"f8c1a2b3-..."</span>],
|
||||
<span class="k">"initial_update"</span>: {
|
||||
<span class="k">"body"</span>: <span class="s">"We're seeing elevated error rates on the API. Investigating now."</span>
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
<table>
|
||||
@@ -397,8 +397,8 @@ Content-Type: application/json
|
||||
<div class="cb">
|
||||
<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">"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>
|
||||
}</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>
|
||||
@@ -504,13 +504,13 @@ Content-Type: application/json
|
||||
<div class="cb">
|
||||
<div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{
|
||||
<span class="o">"$and"</span>: [
|
||||
{ <span class="k">"status"</span>: <span class="n">200</span> },
|
||||
{ <span class="o">"$or"</span>: [
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.region"</span>: { <span class="o">"$eq"</span>: <span class="s">"us"</span> } } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.region"</span>: { <span class="o">"$eq"</span>: <span class="s">"eu"</span> } } }
|
||||
] }
|
||||
]
|
||||
<span class="o">"$and"</span>: [
|
||||
{ <span class="k">"status"</span>: <span class="n">200</span> },
|
||||
{ <span class="o">"$or"</span>: [
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.region"</span>: { <span class="o">"$eq"</span>: <span class="s">"us"</span> } } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.region"</span>: { <span class="o">"$eq"</span>: <span class="s">"eu"</span> } } }
|
||||
] }
|
||||
]
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,10 +526,10 @@ Content-Type: application/json
|
||||
<h3>JSON API response shape</h3>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{
|
||||
<span class="o">"$and"</span>: [
|
||||
{ <span class="k">"status"</span>: <span class="n">200</span> },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.ok"</span>: { <span class="o">"$eq"</span>: <span class="n">true</span> } } }
|
||||
]
|
||||
<span class="o">"$and"</span>: [
|
||||
{ <span class="k">"status"</span>: <span class="n">200</span> },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.ok"</span>: { <span class="o">"$eq"</span>: <span class="n">true</span> } } }
|
||||
]
|
||||
}</pre></div>
|
||||
|
||||
<h3>Performance monitor (down if slow)</h3>
|
||||
@@ -553,41 +553,41 @@ Content-Type: application/json
|
||||
<p>Wrap <code>$or</code> in <code>$not</code> to mark down when any condition matches.</p>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{ <span class="o">"$not"</span>: {
|
||||
<span class="o">"$or"</span>: [
|
||||
{ <span class="k">"status"</span>: { <span class="o">"$ge"</span>: <span class="n">500</span> } },
|
||||
{ <span class="k">"$time"</span>: { <span class="o">"$gt"</span>: <span class="n">3000</span> } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.healthy"</span>: { <span class="o">"$eq"</span>: <span class="n">false</span> } } },
|
||||
{ <span class="o">"$html"</span>: { <span class="s">".error-banner"</span>: { <span class="o">"$exists"</span>: <span class="n">true</span> } } }
|
||||
]
|
||||
<span class="o">"$or"</span>: [
|
||||
{ <span class="k">"status"</span>: { <span class="o">"$ge"</span>: <span class="n">500</span> } },
|
||||
{ <span class="k">"$time"</span>: { <span class="o">"$gt"</span>: <span class="n">3000</span> } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.healthy"</span>: { <span class="o">"$eq"</span>: <span class="n">false</span> } } },
|
||||
{ <span class="o">"$html"</span>: { <span class="s">".error-banner"</span>: { <span class="o">"$exists"</span>: <span class="n">true</span> } } }
|
||||
]
|
||||
} }</pre></div>
|
||||
|
||||
<h3>Up only when everything matches</h3>
|
||||
<p>Combine <code>$and</code> with header, body, and JSON checks for a strict definition of healthy.</p>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{
|
||||
<span class="o">"$and"</span>: [
|
||||
{ <span class="k">"status"</span>: <span class="n">200</span> },
|
||||
{ <span class="k">"headers.content-type"</span>: { <span class="o">"$co"</span>: <span class="s">"application/json"</span> } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.version"</span>: { <span class="o">"$sw"</span>: <span class="s">"v2"</span> } } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.db.connections"</span>: { <span class="o">"$lt"</span>: <span class="n">100</span> } } }
|
||||
]
|
||||
<span class="o">"$and"</span>: [
|
||||
{ <span class="k">"status"</span>: <span class="n">200</span> },
|
||||
{ <span class="k">"headers.content-type"</span>: { <span class="o">"$co"</span>: <span class="s">"application/json"</span> } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.version"</span>: { <span class="o">"$sw"</span>: <span class="s">"v2"</span> } } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.db.connections"</span>: { <span class="o">"$lt"</span>: <span class="n">100</span> } } }
|
||||
]
|
||||
}</pre></div>
|
||||
|
||||
<h3>Nested logic</h3>
|
||||
<p><code>$and</code> and <code>$or</code> nest freely inside each other, as deep as you need.</p>
|
||||
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{
|
||||
<span class="o">"$and"</span>: [
|
||||
{ <span class="k">"status"</span>: { <span class="o">"$lt"</span>: <span class="n">400</span> } },
|
||||
{ <span class="o">"$or"</span>: [
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.env"</span>: { <span class="o">"$eq"</span>: <span class="s">"production"</span> } } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.env"</span>: { <span class="o">"$eq"</span>: <span class="s">"staging"</span> } } }
|
||||
] },
|
||||
{ <span class="o">"$or"</span>: [
|
||||
{ <span class="k">"$time"</span>: { <span class="o">"$lt"</span>: <span class="n">2000</span> } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.cache"</span>: { <span class="o">"$eq"</span>: <span class="s">"hit"</span> } } }
|
||||
] }
|
||||
]
|
||||
<span class="o">"$and"</span>: [
|
||||
{ <span class="k">"status"</span>: { <span class="o">"$lt"</span>: <span class="n">400</span> } },
|
||||
{ <span class="o">"$or"</span>: [
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.env"</span>: { <span class="o">"$eq"</span>: <span class="s">"production"</span> } } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.env"</span>: { <span class="o">"$eq"</span>: <span class="s">"staging"</span> } } }
|
||||
] },
|
||||
{ <span class="o">"$or"</span>: [
|
||||
{ <span class="k">"$time"</span>: { <span class="o">"$lt"</span>: <span class="n">2000</span> } },
|
||||
{ <span class="o">"$json"</span>: { <span class="s">"$.cache"</span>: { <span class="o">"$eq"</span>: <span class="s">"hit"</span> } } }
|
||||
] }
|
||||
]
|
||||
}</pre></div>
|
||||
</div>
|
||||
|
||||
@@ -633,8 +633,8 @@ Content-Type: application/json
|
||||
<div class="cb">
|
||||
<div class="cb-header"><span class="cb-lang">json</span></div>
|
||||
<pre>{
|
||||
<span class="k">"channel"</span>: { <span class="k">"id"</span>: <span class="s">"<uuid>"</span>, <span class="k">"name"</span>: <span class="s">"On-call webhook"</span> },
|
||||
<span class="k">"event"</span>: { <span class="k">"kind"</span>: <span class="s">"down"</span> | <span class="s">"up"</span> | <span class="s">"cert"</span> | <span class="s">"test"</span>, ... }
|
||||
<span class="k">"channel"</span>: { <span class="k">"id"</span>: <span class="s">"<uuid>"</span>, <span class="k">"name"</span>: <span class="s">"On-call webhook"</span> },
|
||||
<span class="k">"event"</span>: { <span class="k">"kind"</span>: <span class="s">"down"</span> | <span class="s">"up"</span> | <span class="s">"cert"</span> | <span class="s">"test"</span>, ... }
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
@@ -642,19 +642,19 @@ Content-Type: application/json
|
||||
<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">"ping"</span>: {
|
||||
<span class="k">"status_code"</span>: <span class="n">503</span>,
|
||||
<span class="k">"latency_ms"</span>: <span class="n">412</span>,
|
||||
<span class="k">"error"</span>: <span class="n">null</span>,
|
||||
<span class="k">"checked_at"</span>: <span class="s">"2026-04-08T14:23:00.000Z"</span>
|
||||
}
|
||||
<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">"ping"</span>: {
|
||||
<span class="k">"status_code"</span>: <span class="n">503</span>,
|
||||
<span class="k">"latency_ms"</span>: <span class="n">412</span>,
|
||||
<span class="k">"error"</span>: <span class="n">null</span>,
|
||||
<span class="k">"checked_at"</span>: <span class="s">"2026-04-08T14:23:00.000Z"</span>
|
||||
}
|
||||
}</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>
|
||||
@@ -662,16 +662,16 @@ Content-Type: application/json
|
||||
<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>
|
||||
<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>
|
||||
<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> }
|
||||
<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> }
|
||||
}</pre></div>
|
||||
|
||||
<h3>Verifying the signature</h3>
|
||||
@@ -680,8 +680,8 @@ Content-Type: application/json
|
||||
<pre><span class="k">import</span> { createHmac, timingSafeEqual } <span class="k">from</span> <span class="s">"crypto"</span>;
|
||||
|
||||
<span class="k">function</span> verify(rawBody, headerSig, secret) {
|
||||
<span class="k">const</span> expected = createHmac(<span class="s">"sha256"</span>, secret).update(rawBody).digest(<span class="s">"hex"</span>);
|
||||
<span class="k">return</span> timingSafeEqual(Buffer.from(expected), Buffer.from(headerSig));
|
||||
<span class="k">const</span> expected = createHmac(<span class="s">"sha256"</span>, secret).update(rawBody).digest(<span class="s">"hex"</span>);
|
||||
<span class="k">return</span> timingSafeEqual(Buffer.from(expected), Buffer.from(headerSig));
|
||||
}</pre>
|
||||
</div>
|
||||
<p>Always verify against the <em>raw</em> request body before parsing JSON.</p>
|
||||
|
||||
@@ -163,26 +163,26 @@
|
||||
</head>
|
||||
<body class="bg-[#0a0a0a] text-gray-100 font-sans antialiased grid-bg">
|
||||
|
||||
<!-- ─── DEV WARNING ─── -->
|
||||
<div class="fixed top-0 left-0 right-0 z-[60] bg-yellow-500/10 border-b border-yellow-500/20 text-center py-1.5 px-4">
|
||||
<p class="text-xs text-yellow-400">PingQL is in active development. Please don't rely on the service until this notice is removed.</p>
|
||||
</div>
|
||||
|
||||
<!-- ─── HEADER ─── -->
|
||||
<header class="fixed top-[34px] left-0 right-0 z-50 border-b border-border-subtle bg-[#0a0a0a]/70 backdrop-blur-md">
|
||||
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<a href="/" class="font-mono text-lg font-bold tracking-tight group">Ping<span class="text-blue-400 transition-all group-hover:drop-shadow-[0_0_8px_rgba(59,130,246,0.4)]">QL</span></a>
|
||||
<nav class="hidden md:flex items-center gap-7 text-sm text-gray-400">
|
||||
<a href="/docs" class="hover:text-gray-200 transition-colors">Docs</a>
|
||||
<a href="/privacy" class="hover:text-gray-200 transition-colors">Privacy</a>
|
||||
<a href="#pricing" class="hover:text-gray-200 transition-colors">Pricing</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/dashboard" class="text-sm text-gray-400 hover:text-gray-200 transition-colors">Sign in</a>
|
||||
<a href="/dashboard" class="text-sm text-white px-4 py-2 rounded-lg font-medium transition-all" style="background:#2563eb;box-shadow:0 1px 3px rgba(0,0,0,0.3),0 0 12px rgba(59,130,246,0.15)">Get started</a>
|
||||
</div>
|
||||
<!-- ─── TOP BAR (dev warning + header, flush) ─── -->
|
||||
<div class="fixed top-0 left-0 right-0 z-50">
|
||||
<div class="bg-yellow-500/10 backdrop-blur-md border-b border-yellow-500/20 text-center py-1.5 px-4">
|
||||
<p class="text-xs text-yellow-400">PingQL is in active development. Please don't rely on the service until this notice is removed.</p>
|
||||
</div>
|
||||
</header>
|
||||
<header class="border-b border-border-subtle bg-[#0a0a0a]/70 backdrop-blur-md">
|
||||
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<a href="/" class="font-mono text-lg font-bold tracking-tight group">Ping<span class="text-blue-400 transition-all group-hover:drop-shadow-[0_0_8px_rgba(59,130,246,0.4)]">QL</span></a>
|
||||
<nav class="hidden md:flex items-center gap-7 text-sm text-gray-400">
|
||||
<a href="/docs" class="hover:text-gray-200 transition-colors">Docs</a>
|
||||
<a href="/privacy" class="hover:text-gray-200 transition-colors">Privacy</a>
|
||||
<a href="#pricing" class="hover:text-gray-200 transition-colors">Pricing</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/dashboard" class="text-sm text-gray-400 hover:text-gray-200 transition-colors">Sign in</a>
|
||||
<a href="/dashboard" class="text-sm text-white px-4 py-2 rounded-lg font-medium transition-all" style="background:#2563eb;box-shadow:0 1px 3px rgba(0,0,0,0.3),0 0 12px rgba(59,130,246,0.15)">Get started</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<!-- ─── HERO ─── -->
|
||||
<section class="relative min-h-screen flex items-center hero-glow pt-16">
|
||||
@@ -221,11 +221,11 @@
|
||||
</div>
|
||||
<div class="terminal-body">
|
||||
<pre><span class="syn-brace">{</span>
|
||||
<span class="syn-key">"$and"</span><span class="syn-brace">:</span> <span class="syn-brace">[</span>
|
||||
<span class="syn-brace">{</span> <span class="syn-key">"status"</span><span class="syn-brace">:</span> <span class="syn-brace">{</span> <span class="syn-op">"$lt"</span><span class="syn-brace">:</span> <span class="syn-num">400</span> <span class="syn-brace">}</span> <span class="syn-brace">}</span>,
|
||||
<span class="syn-brace">{</span> <span class="syn-op">"$json"</span><span class="syn-brace">:</span> <span class="syn-brace">{</span> <span class="syn-str">"$.db.status"</span><span class="syn-brace">:</span> <span class="syn-brace">{</span> <span class="syn-op">"$eq"</span><span class="syn-brace">:</span> <span class="syn-str">"ok"</span> <span class="syn-brace">}</span> <span class="syn-brace">}</span> <span class="syn-brace">}</span>,
|
||||
<span class="syn-brace">{</span> <span class="syn-op">"$certExpiry"</span><span class="syn-brace">:</span> <span class="syn-brace">{</span> <span class="syn-op">"$gt"</span><span class="syn-brace">:</span> <span class="syn-num">14</span> <span class="syn-brace">}</span> <span class="syn-brace">}</span>
|
||||
<span class="syn-brace">]</span>
|
||||
<span class="syn-key">"$and"</span><span class="syn-brace">:</span> <span class="syn-brace">[</span>
|
||||
<span class="syn-brace">{</span> <span class="syn-key">"status"</span><span class="syn-brace">:</span> <span class="syn-brace">{</span> <span class="syn-op">"$lt"</span><span class="syn-brace">:</span> <span class="syn-num">400</span> <span class="syn-brace">}</span> <span class="syn-brace">}</span>,
|
||||
<span class="syn-brace">{</span> <span class="syn-op">"$json"</span><span class="syn-brace">:</span> <span class="syn-brace">{</span> <span class="syn-str">"$.db.status"</span><span class="syn-brace">:</span> <span class="syn-brace">{</span> <span class="syn-op">"$eq"</span><span class="syn-brace">:</span> <span class="syn-str">"ok"</span> <span class="syn-brace">}</span> <span class="syn-brace">}</span> <span class="syn-brace">}</span>,
|
||||
<span class="syn-brace">{</span> <span class="syn-op">"$certExpiry"</span><span class="syn-brace">:</span> <span class="syn-brace">{</span> <span class="syn-op">"$gt"</span><span class="syn-brace">:</span> <span class="syn-num">14</span> <span class="syn-brace">}</span> <span class="syn-brace">}</span>
|
||||
<span class="syn-brace">]</span>
|
||||
<span class="syn-brace">}</span></pre>
|
||||
<div class="mt-4 pt-4 border-t border-border-subtle text-xs text-gray-500">
|
||||
<span class="syn-comment">// status < 400 AND $.db.status = "ok" AND $certExpiry > 14 days</span>
|
||||
@@ -562,20 +562,20 @@
|
||||
</div>
|
||||
<div class="terminal-body text-xs sm:text-[13px]">
|
||||
<pre><span class="text-gray-500">$</span> <span class="text-white">curl</span> -X POST https://pingql.com/api/pages \
|
||||
-H <span class="syn-str">"X-Key: abcd-1234-efgh-5678"</span> \
|
||||
-d <span class="syn-str">'{
|
||||
"slug": "my-app",
|
||||
"title": "My App Status",
|
||||
"monitors": [
|
||||
{ "monitor_id": "a1b2c3d4e5f67890" }
|
||||
]
|
||||
}'</span>
|
||||
-H <span class="syn-str">"X-Key: abcd-1234-efgh-5678"</span> \
|
||||
-d <span class="syn-str">'{
|
||||
"slug": "my-app",
|
||||
"title": "My App Status",
|
||||
"monitors": [
|
||||
{ "monitor_id": "a1b2c3d4e5f67890" }
|
||||
]
|
||||
}'</span>
|
||||
|
||||
<span class="syn-brace">{</span>
|
||||
<span class="syn-key">"id"</span>: <span class="syn-str">"f8c1a2b3-..."</span>,
|
||||
<span class="syn-key">"slug"</span>: <span class="syn-str">"my-app"</span>,
|
||||
<span class="syn-key">"title"</span>: <span class="syn-str">"My App Status"</span>,
|
||||
<span class="syn-key">"theme"</span>: <span class="syn-str">"auto"</span>
|
||||
<span class="syn-key">"id"</span>: <span class="syn-str">"f8c1a2b3-..."</span>,
|
||||
<span class="syn-key">"slug"</span>: <span class="syn-str">"my-app"</span>,
|
||||
<span class="syn-key">"title"</span>: <span class="syn-str">"My App Status"</span>,
|
||||
<span class="syn-key">"theme"</span>: <span class="syn-str">"auto"</span>
|
||||
<span class="syn-brace">}</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -590,22 +590,22 @@
|
||||
</div>
|
||||
<div class="terminal-body text-xs sm:text-[13px]">
|
||||
<pre><span class="text-gray-500">$</span> <span class="text-white">curl</span> -X POST https://pingql.com/api/monitors \
|
||||
-H <span class="syn-str">"X-Key: abcd-1234-efgh-5678"</span> \
|
||||
-d <span class="syn-str">'{
|
||||
"name": "Production API",
|
||||
"url": "https://api.example.com/health",
|
||||
"interval_s": 60,
|
||||
"query": {
|
||||
"status": { "$lt": 400 },
|
||||
"$json": { "$.ok": { "$eq": true } }
|
||||
}
|
||||
}'</span>
|
||||
-H <span class="syn-str">"X-Key: abcd-1234-efgh-5678"</span> \
|
||||
-d <span class="syn-str">'{
|
||||
"name": "Production API",
|
||||
"url": "https://api.example.com/health",
|
||||
"interval_s": 60,
|
||||
"query": {
|
||||
"status": { "$lt": 400 },
|
||||
"$json": { "$.ok": { "$eq": true } }
|
||||
}
|
||||
}'</span>
|
||||
|
||||
<span class="syn-brace">{</span>
|
||||
<span class="syn-key">"id"</span>: <span class="syn-str">"a1b2c3d4e5f67890"</span>,
|
||||
<span class="syn-key">"name"</span>: <span class="syn-str">"Production API"</span>,
|
||||
<span class="syn-key">"url"</span>: <span class="syn-str">"https://api.example.com/health"</span>,
|
||||
<span class="syn-key">"enabled"</span>: <span class="syn-num">true</span>
|
||||
<span class="syn-key">"id"</span>: <span class="syn-str">"a1b2c3d4e5f67890"</span>,
|
||||
<span class="syn-key">"name"</span>: <span class="syn-str">"Production API"</span>,
|
||||
<span class="syn-key">"url"</span>: <span class="syn-str">"https://api.example.com/health"</span>,
|
||||
<span class="syn-key">"enabled"</span>: <span class="syn-num">true</span>
|
||||
<span class="syn-brace">}</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h2 class="text-lg font-semibold text-gray-200 mt-2">Create Monitor</h2>
|
||||
</div>
|
||||
|
||||
<%~ include('./partials/monitor-form', { _form: { monitor: {}, isEdit: false, prefix: '', bg: 'bg-surface-solid', border: 'border-border-subtle' }, plan: it.plan, regions: it.regions, channels: it.channels }) %>
|
||||
<%~ include('./partials/monitor-form', { _form: { monitor: {}, isEdit: false, prefix: '', bg: 'bg-surface-solid', border: 'border-border-subtle' }, plan: it.plan, regions: it.regions }) %>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -9,17 +9,19 @@
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500 leading-relaxed">
|
||||
Channels are dispatched on status transitions (the "important" beats). Webhooks POST a JSON payload to your URL.
|
||||
Channels are dispatched on status transitions (the "important" beats) for every monitor you attach below. Webhooks POST a JSON payload to your URL.
|
||||
More providers (Discord, Slack, Email, Telegram) will land here as drop-ins.
|
||||
</p>
|
||||
|
||||
<% const monitors = it.monitors || []; %>
|
||||
|
||||
<% if (!it.channels || it.channels.length === 0) { %>
|
||||
<section class="card-static p-6 text-sm text-gray-500">
|
||||
No channels yet. Create one below to start receiving alerts.
|
||||
</section>
|
||||
<% } else { %>
|
||||
<% it.channels.forEach(function(c) { %>
|
||||
<section class="card-static p-6">
|
||||
<section class="card-static p-6 space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
@@ -42,6 +44,26 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="/dashboard/notifications/<%= c.id %>/edit" method="POST" class="space-y-3 pt-4 border-t border-border-subtle">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-2">Attached monitors</label>
|
||||
<% if (monitors.length === 0) { %>
|
||||
<p class="text-xs text-gray-600">No monitors yet. <a href="/dashboard/monitors/new" class="text-blue-400 hover:text-blue-300">Create one</a> to attach it to this channel.</p>
|
||||
<% } else { %>
|
||||
<% const attached = (c.monitor_ids || []); %>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<% monitors.forEach(function(m) { %>
|
||||
<label class="flex items-center gap-2 bg-gray-900 border border-gray-800 hover:border-gray-600 rounded-lg px-3 py-2 cursor-pointer transition-colors">
|
||||
<input type="checkbox" name="monitor_ids" value="<%= m.id %>" class="accent-blue-500" <%= attached.includes(m.id) ? 'checked' : '' %>>
|
||||
<span class="text-sm text-gray-300 truncate max-w-[16rem]"><%= m.name %></span>
|
||||
</label>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-1.5 rounded-lg border border-border-subtle text-gray-300 hover:text-white hover:border-gray-600 text-xs transition-colors">Save attachments</button>
|
||||
</form>
|
||||
</section>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
@@ -71,6 +93,21 @@
|
||||
<input name="secret" type="text" placeholder="Used to sign payloads as X-PingQL-Signature"
|
||||
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-4 py-2.5 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 font-mono text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">Monitors <span class="text-gray-600">(optional - attach now or later)</span></label>
|
||||
<% if (monitors.length === 0) { %>
|
||||
<p class="text-xs text-gray-600">No monitors yet. <a href="/dashboard/monitors/new" class="text-blue-400 hover:text-blue-300">Create one</a> first, then attach it here.</p>
|
||||
<% } else { %>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<% monitors.forEach(function(m) { %>
|
||||
<label class="flex items-center gap-2 bg-gray-900 border border-gray-800 hover:border-gray-600 rounded-lg px-3 py-2 cursor-pointer transition-colors">
|
||||
<input type="checkbox" name="monitor_ids" value="<%= m.id %>" class="accent-blue-500">
|
||||
<span class="text-sm text-gray-300 truncate max-w-[16rem]"><%= m.name %></span>
|
||||
</label>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary px-6 py-2.5 text-sm">Create channel</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
cert_alert_days: Number(document.getElementById(prefix + 'cert-alert-days').value),
|
||||
max_redirects: Number(document.getElementById(prefix + 'max-redirects-follow').value),
|
||||
};
|
||||
body.channel_ids = [...document.querySelectorAll('.' + prefix + 'channel-check:checked')].map(el => el.value);
|
||||
if (Object.keys(headers).length) body.request_headers = headers;
|
||||
else body.request_headers = null;
|
||||
const rb = document.getElementById(prefix + 'request-body').value.trim();
|
||||
|
||||
@@ -146,27 +146,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<%
|
||||
const channels = it.channels || [];
|
||||
const attached = (monitor.channel_ids && monitor.channel_ids.length) ? monitor.channel_ids : [];
|
||||
%>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">Notification channels <span class="text-gray-600">(optional)</span></label>
|
||||
<% if (channels.length === 0) { %>
|
||||
<p class="text-xs text-gray-600">No channels yet. <a href="/dashboard/notifications" class="text-blue-400 hover:text-blue-300">Create one</a> to get alerted on transitions.</p>
|
||||
<% } else { %>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<% channels.forEach(function(c) { %>
|
||||
<label class="flex items-center gap-2 <%= bg %> border <%= border %> hover:border-gray-600 rounded-lg px-3 py-2 cursor-pointer transition-colors">
|
||||
<input type="checkbox" name="channel_ids" value="<%= c.id %>" class="<%= prefix %>channel-check accent-blue-500" <%= attached.includes(c.id) ? 'checked' : '' %>>
|
||||
<span class="text-sm text-gray-300"><%= c.name %></span>
|
||||
<span class="text-xs text-gray-600 font-mono"><%= c.kind %></span>
|
||||
</label>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<%
|
||||
// Default to all regions if none selected
|
||||
const selectedRegions = (monitor.regions && monitor.regions.length) ? monitor.regions : regions.map(r => r[0]);
|
||||
|
||||
Reference in New Issue
Block a user