feat: custom docs page at /docs, drop swagger
This commit is contained in:
parent
eb45152c29
commit
7b38ff192e
|
|
@ -7,9 +7,8 @@
|
||||||
"build": "bun build src/index.ts --outdir dist"
|
"build": "bun build src/index.ts --outdir dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"elysia": "^1.4.27",
|
|
||||||
"@elysiajs/cors": "^1.4.1",
|
"@elysiajs/cors": "^1.4.1",
|
||||||
"@elysiajs/swagger": "^1.3.1",
|
"elysia": "^1.4.27",
|
||||||
"postgres": "^3.4.8"
|
"postgres": "^3.4.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,208 +1,370 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="dark">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>PingQL — Query Language Docs</title>
|
<title>PingQL — Documentation</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', ui-monospace, monospace; background: #0a0a0a; }
|
body { font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', ui-monospace, monospace; background: #0a0a0a; }
|
||||||
pre, code { font-family: inherit; }
|
pre, code { font-family: inherit; }
|
||||||
.prose h2 { color: #f3f4f6; font-size: 1.125rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid #1f2937; }
|
|
||||||
.prose h3 { color: #d1d5db; font-size: 0.9rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
/* Sidebar nav */
|
||||||
.prose p { color: #9ca3af; font-size: 0.875rem; line-height: 1.7; margin-bottom: 0.75rem; }
|
.nav-link { display: block; padding: 0.3rem 0.75rem; font-size: 0.8rem; color: #6b7280; border-left: 2px solid transparent; transition: all 0.1s; }
|
||||||
.prose ul { color: #9ca3af; font-size: 0.875rem; margin-bottom: 0.75rem; padding-left: 1.25rem; list-style: disc; }
|
.nav-link:hover { color: #d1d5db; border-left-color: #374151; }
|
||||||
.prose li { margin-bottom: 0.25rem; line-height: 1.6; }
|
.nav-link.active { color: #93c5fd; border-left-color: #3b82f6; }
|
||||||
code.inline { background: #1f2937; color: #93c5fd; padding: 0.1em 0.4em; border-radius: 0.25rem; font-size: 0.8rem; }
|
.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; }
|
||||||
.codeblock { background: #0f172a; border: 1px solid #1e293b; border-radius: 0.5rem; padding: 1rem; margin: 0.75rem 0; overflow-x: auto; }
|
|
||||||
.codeblock pre { color: #e2e8f0; font-size: 0.8rem; line-height: 1.6; margin: 0; white-space: pre; }
|
/* Content */
|
||||||
.tag-op { color: #60a5fa; }
|
.section { padding-top: 3rem; padding-bottom: 1rem; border-top: 1px solid #111827; margin-top: 2rem; }
|
||||||
.tag-str { color: #34d399; }
|
.section:first-child { border-top: none; margin-top: 0; padding-top: 0; }
|
||||||
.tag-num { color: #f59e0b; }
|
h2 { font-size: 1.2rem; font-weight: 700; color: #f9fafb; margin-bottom: 0.5rem; }
|
||||||
.tag-key { color: #c084fc; }
|
h3 { font-size: 0.85rem; font-weight: 600; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.06em; margin: 1.5rem 0 0.5rem; }
|
||||||
.tag-cmt { color: #475569; }
|
p { font-size: 0.875rem; color: #6b7280; line-height: 1.7; margin-bottom: 0.75rem; }
|
||||||
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; margin: 0.75rem 0; }
|
p a { color: #93c5fd; }
|
||||||
th { text-align: left; color: #6b7280; font-weight: 500; padding: 0.4rem 0.75rem; border-bottom: 1px solid #1f2937; }
|
p a:hover { color: #bfdbfe; }
|
||||||
td { color: #9ca3af; padding: 0.4rem 0.75rem; border-bottom: 1px solid #111827; vertical-align: top; }
|
|
||||||
td:first-child { color: #93c5fd; white-space: nowrap; }
|
/* Code blocks */
|
||||||
|
.cb { background: #0f172a; border: 1px solid #1e293b; border-radius: 0.5rem; overflow: hidden; margin: 0.75rem 0 1.25rem; }
|
||||||
|
.cb-header { background: #0a1628; border-bottom: 1px solid #1e293b; padding: 0.4rem 0.85rem; display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.cb-lang { font-size: 0.7rem; color: #475569; }
|
||||||
|
.cb-copy { font-size: 0.7rem; color: #3b82f6; cursor: pointer; }
|
||||||
|
.cb-copy:hover { color: #93c5fd; }
|
||||||
|
.cb pre { padding: 0.85rem; font-size: 0.78rem; line-height: 1.65; color: #e2e8f0; overflow-x: auto; margin: 0; }
|
||||||
|
|
||||||
|
/* Syntax */
|
||||||
|
.k { color: #c084fc; } /* key */
|
||||||
|
.s { color: #34d399; } /* string */
|
||||||
|
.n { color: #f59e0b; } /* number */
|
||||||
|
.o { color: #60a5fa; } /* operator */
|
||||||
|
.c { color: #334155; } /* comment */
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; margin: 0.5rem 0 1.25rem; }
|
||||||
|
th { text-align: left; color: #4b5563; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.4rem 0.75rem; border-bottom: 1px solid #111827; }
|
||||||
|
td { color: #6b7280; padding: 0.45rem 0.75rem; border-bottom: 1px solid #0d1117; vertical-align: top; }
|
||||||
|
td:first-child { color: #93c5fd; white-space: nowrap; font-size: 0.78rem; }
|
||||||
|
td code { background: #1e293b; color: #94a3b8; padding: 0.1em 0.35em; border-radius: 0.2rem; font-size: 0.75rem; }
|
||||||
|
|
||||||
|
/* Endpoint blocks */
|
||||||
|
.endpoint { display: flex; align-items: baseline; gap: 0.75rem; margin-bottom: 0.5rem; }
|
||||||
|
.method { font-size: 0.7rem; font-weight: 700; padding: 0.15rem 0.45rem; border-radius: 0.2rem; }
|
||||||
|
.method.get { background: #052e16; color: #4ade80; }
|
||||||
|
.method.post { background: #172554; color: #93c5fd; }
|
||||||
|
.method.patch { background: #1c1917; color: #fb923c; }
|
||||||
|
.method.delete { background: #3f1215; color: #f87171; }
|
||||||
|
.path { color: #e2e8f0; font-size: 0.875rem; }
|
||||||
|
.endpoint-desc { font-size: 0.8rem; color: #4b5563; margin-bottom: 1rem; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-[#0a0a0a] text-gray-100 min-h-screen">
|
<body class="bg-[#0a0a0a] text-gray-100 min-h-screen">
|
||||||
|
|
||||||
<nav class="border-b border-gray-800 px-6 py-3 flex items-center justify-between sticky top-0 bg-[#0a0a0a]/95 backdrop-blur z-10">
|
<!-- Top bar -->
|
||||||
<a href="/dashboard/home" class="text-xl font-bold">Ping<span class="text-blue-400">QL</span></a>
|
<div class="border-b border-gray-900 px-6 py-3 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-6 text-sm text-gray-500">
|
<a href="/" class="text-lg font-bold">Ping<span class="text-blue-400">QL</span> <span class="text-gray-600 font-normal text-sm">docs</span></a>
|
||||||
<a href="/dashboard/home" class="hover:text-gray-300 transition-colors">Monitors</a>
|
<a href="/dashboard" class="text-sm text-gray-600 hover:text-gray-400 transition-colors">Dashboard →</a>
|
||||||
<a href="/docs" class="hover:text-gray-300 transition-colors">API</a>
|
|
||||||
<span class="text-blue-400">Query Docs</span>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="max-w-3xl mx-auto px-6 py-10 prose">
|
|
||||||
|
|
||||||
<h1 class="text-2xl font-bold text-white mb-1">Query Language</h1>
|
|
||||||
<p class="text-gray-500 text-sm mb-8">A MongoDB-style query language for defining exactly when a monitor is up or down.</p>
|
|
||||||
|
|
||||||
<!-- Overview -->
|
|
||||||
<h2>Overview</h2>
|
|
||||||
<p>A PingQL query is a JSON object that runs against every ping result. If the query evaluates to <code class="inline">true</code>, the monitor is considered <strong class="text-white">up</strong>. Use <code class="inline">$consider: "down"</code> to invert this.</p>
|
|
||||||
<p>By default (no query set), a monitor is up when the HTTP status code is below 400.</p>
|
|
||||||
|
|
||||||
<!-- Fields -->
|
|
||||||
<h2>Fields</h2>
|
|
||||||
<p>These are the values you can query against:</p>
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>status</td><td>number</td><td>HTTP status code (e.g. 200, 404)</td></tr>
|
|
||||||
<tr><td>body</td><td>string</td><td>Full response body as text</td></tr>
|
|
||||||
<tr><td>headers.<em>name</em></td><td>string</td><td>Response header value (e.g. <code class="inline">headers.content-type</code>)</td></tr>
|
|
||||||
<tr><td>$responseTime</td><td>number</td><td>Request latency in milliseconds</td></tr>
|
|
||||||
<tr><td>$certExpiry</td><td>number</td><td>Days until SSL certificate expires</td></tr>
|
|
||||||
<tr><td>$json</td><td>object</td><td>JSONPath expression against response body — see below</td></tr>
|
|
||||||
<tr><td>$select</td><td>object</td><td>CSS selector against response HTML — see below</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- Operators -->
|
|
||||||
<h2>Operators</h2>
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>Operator</th><th>Description</th><th>Types</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>$eq</td><td>Equal to</td><td>any</td></tr>
|
|
||||||
<tr><td>$ne</td><td>Not equal to</td><td>any</td></tr>
|
|
||||||
<tr><td>$gt</td><td>Greater than</td><td>number</td></tr>
|
|
||||||
<tr><td>$gte</td><td>Greater than or equal</td><td>number</td></tr>
|
|
||||||
<tr><td>$lt</td><td>Less than</td><td>number</td></tr>
|
|
||||||
<tr><td>$lte</td><td>Less than or equal</td><td>number</td></tr>
|
|
||||||
<tr><td>$contains</td><td>String contains substring</td><td>string</td></tr>
|
|
||||||
<tr><td>$startsWith</td><td>String starts with</td><td>string</td></tr>
|
|
||||||
<tr><td>$endsWith</td><td>String ends with</td><td>string</td></tr>
|
|
||||||
<tr><td>$regex</td><td>Matches regular expression</td><td>string</td></tr>
|
|
||||||
<tr><td>$exists</td><td>Field is present and non-null</td><td>any</td></tr>
|
|
||||||
<tr><td>$in</td><td>Value is in array</td><td>any</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- Basic examples -->
|
|
||||||
<h2>Basic Examples</h2>
|
|
||||||
|
|
||||||
<h3>Status code</h3>
|
|
||||||
<div class="codeblock"><pre><span class="tag-cmt">// Up if status is 200</span>
|
|
||||||
{ <span class="tag-key">"status"</span>: <span class="tag-num">200</span> }
|
|
||||||
|
|
||||||
<span class="tag-cmt">// Up if status is 2xx or 3xx</span>
|
|
||||||
{ <span class="tag-key">"status"</span>: { <span class="tag-op">"$lt"</span>: <span class="tag-num">400</span> } }</pre></div>
|
|
||||||
|
|
||||||
<h3>Response body</h3>
|
|
||||||
<div class="codeblock"><pre><span class="tag-cmt">// Up if body contains "healthy"</span>
|
|
||||||
{ <span class="tag-key">"body"</span>: { <span class="tag-op">"$contains"</span>: <span class="tag-str">"healthy"</span> } }
|
|
||||||
|
|
||||||
<span class="tag-cmt">// Up if body matches a regex</span>
|
|
||||||
{ <span class="tag-key">"body"</span>: { <span class="tag-op">"$regex"</span>: <span class="tag-str">"status.*(ok|healthy)"</span> } }</pre></div>
|
|
||||||
|
|
||||||
<h3>Headers</h3>
|
|
||||||
<div class="codeblock"><pre><span class="tag-cmt">// Up if response is JSON</span>
|
|
||||||
{ <span class="tag-key">"headers.content-type"</span>: { <span class="tag-op">"$contains"</span>: <span class="tag-str">"application/json"</span> } }</pre></div>
|
|
||||||
|
|
||||||
<h3>Response time</h3>
|
|
||||||
<div class="codeblock"><pre><span class="tag-cmt">// Up if response is under 500ms</span>
|
|
||||||
{ <span class="tag-key">"$responseTime"</span>: { <span class="tag-op">"$lt"</span>: <span class="tag-num">500</span> } }</pre></div>
|
|
||||||
|
|
||||||
<h3>SSL certificate expiry</h3>
|
|
||||||
<div class="codeblock"><pre><span class="tag-cmt">// Up if cert expires in more than 14 days</span>
|
|
||||||
{ <span class="tag-key">"$certExpiry"</span>: { <span class="tag-op">"$gt"</span>: <span class="tag-num">14</span> } }</pre></div>
|
|
||||||
|
|
||||||
<!-- JSONPath -->
|
|
||||||
<h2>JSON Body — $json</h2>
|
|
||||||
<p>Use <code class="inline">$json</code> to extract and compare a value from a JSON response body. The key is a dot-notation path, the value is a condition.</p>
|
|
||||||
<div class="codeblock"><pre><span class="tag-cmt">// Up if response JSON has { "status": "ok" }</span>
|
|
||||||
{ <span class="tag-key">"$json"</span>: { <span class="tag-str">"$.status"</span>: { <span class="tag-op">"$eq"</span>: <span class="tag-str">"ok"</span> } } }
|
|
||||||
|
|
||||||
<span class="tag-cmt">// Up if search index has >= 1000 entries</span>
|
|
||||||
{ <span class="tag-key">"$json"</span>: { <span class="tag-str">"$.data.count"</span>: { <span class="tag-op">"$gte"</span>: <span class="tag-num">1000</span> } } }
|
|
||||||
|
|
||||||
<span class="tag-cmt">// Nested path</span>
|
|
||||||
{ <span class="tag-key">"$json"</span>: { <span class="tag-str">"$.db.connections.active"</span>: { <span class="tag-op">"$lt"</span>: <span class="tag-num">100</span> } } }</pre></div>
|
|
||||||
|
|
||||||
<!-- CSS Selector -->
|
|
||||||
<h2>HTML Parsing — $select</h2>
|
|
||||||
<p>Use <code class="inline">$select</code> to extract text from an HTML response using a CSS selector. Useful for monitoring public-facing pages where there's no API.</p>
|
|
||||||
<div class="codeblock"><pre><span class="tag-cmt">// Up if h1 text is "Example Domain"</span>
|
|
||||||
{ <span class="tag-key">"$select"</span>: { <span class="tag-str">"h1"</span>: { <span class="tag-op">"$eq"</span>: <span class="tag-str">"Example Domain"</span> } } }
|
|
||||||
|
|
||||||
<span class="tag-cmt">// Up if status badge contains "operational"</span>
|
|
||||||
{ <span class="tag-key">"$select"</span>: { <span class="tag-str">".status-badge"</span>: { <span class="tag-op">"$contains"</span>: <span class="tag-str">"operational"</span> } } }</pre></div>
|
|
||||||
|
|
||||||
<!-- Logical -->
|
|
||||||
<h2>Logical Operators</h2>
|
|
||||||
<div class="codeblock"><pre><span class="tag-cmt">// Up if status is 200 AND body contains "ok"</span>
|
|
||||||
{
|
|
||||||
<span class="tag-op">"$and"</span>: [
|
|
||||||
{ <span class="tag-key">"status"</span>: <span class="tag-num">200</span> },
|
|
||||||
{ <span class="tag-key">"body"</span>: { <span class="tag-op">"$contains"</span>: <span class="tag-str">"ok"</span> } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
<span class="tag-cmt">// Up if status is 200 OR 204</span>
|
|
||||||
{
|
|
||||||
<span class="tag-op">"$or"</span>: [
|
|
||||||
{ <span class="tag-key">"status"</span>: <span class="tag-num">200</span> },
|
|
||||||
{ <span class="tag-key">"status"</span>: <span class="tag-num">204</span> }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
<span class="tag-cmt">// Up if status is NOT 500</span>
|
|
||||||
{ <span class="tag-op">"$not"</span>: { <span class="tag-key">"status"</span>: <span class="tag-num">500</span> } }</pre></div>
|
|
||||||
|
|
||||||
<!-- $consider -->
|
|
||||||
<h2>$consider — Inverting the Result</h2>
|
|
||||||
<p>By default, a query returning <code class="inline">true</code> means the monitor is <strong class="text-green-400">up</strong>. Set <code class="inline">$consider: "down"</code> to invert this — if the conditions match, the monitor is <strong class="text-red-400">down</strong>.</p>
|
|
||||||
<div class="codeblock"><pre><span class="tag-cmt">// DOWN if response time exceeds 2 seconds</span>
|
|
||||||
{
|
|
||||||
<span class="tag-op">"$consider"</span>: <span class="tag-str">"down"</span>,
|
|
||||||
<span class="tag-key">"$responseTime"</span>: { <span class="tag-op">"$gt"</span>: <span class="tag-num">2000</span> }
|
|
||||||
}
|
|
||||||
|
|
||||||
<span class="tag-cmt">// DOWN if cert expires in less than 7 days</span>
|
|
||||||
{
|
|
||||||
<span class="tag-op">"$consider"</span>: <span class="tag-str">"down"</span>,
|
|
||||||
<span class="tag-key">"$certExpiry"</span>: { <span class="tag-op">"$lt"</span>: <span class="tag-num">7</span> }
|
|
||||||
}
|
|
||||||
|
|
||||||
<span class="tag-cmt">// DOWN if any of these match</span>
|
|
||||||
{
|
|
||||||
<span class="tag-op">"$consider"</span>: <span class="tag-str">"down"</span>,
|
|
||||||
<span class="tag-op">"$or"</span>: [
|
|
||||||
{ <span class="tag-key">"status"</span>: { <span class="tag-op">"$gte"</span>: <span class="tag-num">500</span> } },
|
|
||||||
{ <span class="tag-key">"$responseTime"</span>: { <span class="tag-op">"$gt"</span>: <span class="tag-num">5000</span> } }
|
|
||||||
]
|
|
||||||
}</pre></div>
|
|
||||||
|
|
||||||
<!-- Combining -->
|
|
||||||
<h2>Combining Multiple Queries</h2>
|
|
||||||
<p>You can add multiple monitors pointing to the same URL with different queries — one for uptime, one for performance, one for content integrity.</p>
|
|
||||||
<div class="codeblock"><pre><span class="tag-cmt">// Monitor 1: basic uptime</span>
|
|
||||||
{ <span class="tag-key">"status"</span>: { <span class="tag-op">"$lt"</span>: <span class="tag-num">400</span> } }
|
|
||||||
|
|
||||||
<span class="tag-cmt">// Monitor 2: performance — down if slow</span>
|
|
||||||
{ <span class="tag-op">"$consider"</span>: <span class="tag-str">"down"</span>, <span class="tag-key">"$responseTime"</span>: { <span class="tag-op">"$gt"</span>: <span class="tag-num">1000</span> } }
|
|
||||||
|
|
||||||
<span class="tag-cmt">// Monitor 3: content — check API response shape</span>
|
|
||||||
{
|
|
||||||
<span class="tag-op">"$and"</span>: [
|
|
||||||
{ <span class="tag-key">"status"</span>: <span class="tag-num">200</span> },
|
|
||||||
{ <span class="tag-key">"$json"</span>: { <span class="tag-str">"$.ok"</span>: { <span class="tag-op">"$eq"</span>: <span class="tag-str">"true"</span> } } }
|
|
||||||
]
|
|
||||||
}</pre></div>
|
|
||||||
|
|
||||||
<div class="mt-12 pt-6 border-t border-gray-800 flex items-center justify-between">
|
|
||||||
<a href="/dashboard/home" class="text-sm text-gray-500 hover:text-gray-300 transition-colors">← Back to monitors</a>
|
|
||||||
<a href="/docs" class="text-sm text-blue-400 hover:text-blue-300 transition-colors">API Reference →</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/dashboard/app.js"></script>
|
<div class="flex max-w-6xl mx-auto">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="w-52 shrink-0 sticky top-0 h-screen overflow-y-auto py-6 hidden md:block">
|
||||||
|
<div class="nav-section">Getting Started</div>
|
||||||
|
<a href="#overview" class="nav-link">Overview</a>
|
||||||
|
<a href="#auth" class="nav-link">Authentication</a>
|
||||||
|
|
||||||
|
<div class="nav-section">API Reference</div>
|
||||||
|
<a href="#account" class="nav-link">Account</a>
|
||||||
|
<a href="#monitors" class="nav-link">Monitors</a>
|
||||||
|
|
||||||
|
<div class="nav-section">Query Language</div>
|
||||||
|
<a href="#ql-fields" class="nav-link">Fields</a>
|
||||||
|
<a href="#ql-operators" class="nav-link">Operators</a>
|
||||||
|
<a href="#ql-json" class="nav-link">$json</a>
|
||||||
|
<a href="#ql-select" class="nav-link">$select</a>
|
||||||
|
<a href="#ql-logical" class="nav-link">Logical</a>
|
||||||
|
<a href="#ql-consider" class="nav-link">$consider</a>
|
||||||
|
<a href="#ql-examples" class="nav-link">Examples</a>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="flex-1 px-10 py-8 max-w-3xl">
|
||||||
|
|
||||||
|
<!-- Overview -->
|
||||||
|
<div id="overview" class="section">
|
||||||
|
<h2>Overview</h2>
|
||||||
|
<p>PingQL is a developer-friendly uptime monitoring API. Monitors are defined with a URL, an interval, and an optional query that determines what "up" means for your service.</p>
|
||||||
|
<p>Base URL: <code style="color:#93c5fd;background:#0f172a;padding:0.15em 0.4em;border-radius:0.25rem;font-size:0.8rem">https://api.pingql.com</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth -->
|
||||||
|
<div id="auth" class="section">
|
||||||
|
<h2>Authentication</h2>
|
||||||
|
<p>All API requests require an account key passed as a Bearer token:</p>
|
||||||
|
<div class="cb">
|
||||||
|
<div class="cb-header"><span class="cb-lang">http</span></div>
|
||||||
|
<pre>Authorization: Bearer XXXX-XXXX-XXXX-XXXX</pre>
|
||||||
|
</div>
|
||||||
|
<p>Create an account at <a href="/dashboard">/dashboard</a> or via the API. Keys are 16-character hex strings formatted as four groups.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Account -->
|
||||||
|
<div id="account" class="section">
|
||||||
|
<h2>Account</h2>
|
||||||
|
|
||||||
|
<h3>Register</h3>
|
||||||
|
<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 — used only for recovery and alerts.</p>
|
||||||
|
<div class="cb">
|
||||||
|
<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>
|
||||||
|
<pre>{ <span class="k">"key"</span>: <span class="s">"B8AE-9621-A963-F652"</span>, <span class="k">"email_registered"</span>: <span class="n">true</span> }</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Update Email</h3>
|
||||||
|
<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>
|
||||||
|
<pre>{ <span class="k">"email"</span>: <span class="s">"you@example.com"</span> }</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Monitors -->
|
||||||
|
<div id="monitors" class="section">
|
||||||
|
<h2>Monitors</h2>
|
||||||
|
|
||||||
|
<h3>List</h3>
|
||||||
|
<div class="endpoint"><span class="method get">GET</span><span class="path">/monitors/</span></div>
|
||||||
|
<p class="endpoint-desc">Returns all monitors for the authenticated account.</p>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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: 10)</span>
|
||||||
|
<span class="k">"query"</span>: { ... } <span class="c">// optional — see Query Language below</span>
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Get</h3>
|
||||||
|
<div class="endpoint"><span class="method get">GET</span><span class="path">/monitors/:id</span></div>
|
||||||
|
<p class="endpoint-desc">Returns a monitor including its most recent ping results.</p>
|
||||||
|
|
||||||
|
<h3>Update</h3>
|
||||||
|
<div class="endpoint"><span class="method patch">PATCH</span><span class="path">/monitors/:id</span></div>
|
||||||
|
<p class="endpoint-desc">Update any field. All fields are optional.</p>
|
||||||
|
|
||||||
|
<h3>Delete</h3>
|
||||||
|
<div class="endpoint"><span class="method delete">DELETE</span><span class="path">/monitors/:id</span></div>
|
||||||
|
|
||||||
|
<h3>Toggle</h3>
|
||||||
|
<div class="endpoint"><span class="method post">POST</span><span class="path">/monitors/:id/toggle</span></div>
|
||||||
|
<p class="endpoint-desc">Enable or disable a monitor without deleting it.</p>
|
||||||
|
|
||||||
|
<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.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- QL Fields -->
|
||||||
|
<div id="ql-fields" class="section">
|
||||||
|
<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 when status < 400.</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>status</td><td>number</td><td>HTTP status code</td></tr>
|
||||||
|
<tr><td>body</td><td>string</td><td>Full response body as text</td></tr>
|
||||||
|
<tr><td>headers.<em>name</em></td><td>string</td><td>Response header, e.g. <code>headers.content-type</code></td></tr>
|
||||||
|
<tr><td>$responseTime</td><td>number</td><td>Request latency in milliseconds</td></tr>
|
||||||
|
<tr><td>$certExpiry</td><td>number</td><td>Days until SSL certificate expires</td></tr>
|
||||||
|
<tr><td>$json</td><td>object</td><td>JSONPath expression against response body</td></tr>
|
||||||
|
<tr><td>$select</td><td>object</td><td>CSS selector against response HTML</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- QL Operators -->
|
||||||
|
<div id="ql-operators" class="section">
|
||||||
|
<h2>Query Language — Operators</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Operator</th><th>Description</th><th>Types</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>$eq</td><td>Equal to</td><td>any</td></tr>
|
||||||
|
<tr><td>$ne</td><td>Not equal to</td><td>any</td></tr>
|
||||||
|
<tr><td>$gt / $gte</td><td>Greater than / or equal</td><td>number</td></tr>
|
||||||
|
<tr><td>$lt / $lte</td><td>Less than / or equal</td><td>number</td></tr>
|
||||||
|
<tr><td>$contains</td><td>String contains substring</td><td>string</td></tr>
|
||||||
|
<tr><td>$startsWith</td><td>String starts with</td><td>string</td></tr>
|
||||||
|
<tr><td>$endsWith</td><td>String ends with</td><td>string</td></tr>
|
||||||
|
<tr><td>$regex</td><td>Matches regular expression</td><td>string</td></tr>
|
||||||
|
<tr><td>$exists</td><td>Field is present and non-null</td><td>any</td></tr>
|
||||||
|
<tr><td>$in</td><td>Value is in array</td><td>any</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="cb">
|
||||||
|
<div class="cb-header"><span class="cb-lang">json</span></div>
|
||||||
|
<pre><span class="c">// simple equality shorthand</span>
|
||||||
|
{ <span class="k">"status"</span>: <span class="n">200</span> }
|
||||||
|
|
||||||
|
<span class="c">// operator form</span>
|
||||||
|
{ <span class="k">"status"</span>: { <span class="o">"$lt"</span>: <span class="n">400</span> } }
|
||||||
|
{ <span class="k">"body"</span>: { <span class="o">"$contains"</span>: <span class="s">"healthy"</span> } }
|
||||||
|
{ <span class="k">"headers.content-type"</span>: { <span class="o">"$contains"</span>: <span class="s">"application/json"</span> } }</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- $json -->
|
||||||
|
<div id="ql-json" class="section">
|
||||||
|
<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>
|
||||||
|
<pre><span class="c">// response body: { "status": "ok", "db": { "connections": 12 } }</span>
|
||||||
|
|
||||||
|
{ <span class="o">"$json"</span>: { <span class="s">"$.status"</span>: { <span class="o">"$eq"</span>: <span class="s">"ok"</span> } } }
|
||||||
|
{ <span class="o">"$json"</span>: { <span class="s">"$.db.connections"</span>: { <span class="o">"$lt"</span>: <span class="n">100</span> } } }</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- $select -->
|
||||||
|
<div id="ql-select" class="section">
|
||||||
|
<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>
|
||||||
|
<pre><span class="c">// matches if <h1> text is exactly "Example Domain"</span>
|
||||||
|
{ <span class="o">"$select"</span>: { <span class="s">"h1"</span>: { <span class="o">"$eq"</span>: <span class="s">"Example Domain"</span> } } }
|
||||||
|
|
||||||
|
<span class="c">// matches if status badge contains "operational"</span>
|
||||||
|
{ <span class="o">"$select"</span>: { <span class="s">".status-badge"</span>: { <span class="o">"$contains"</span>: <span class="s">"operational"</span> } } }</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logical -->
|
||||||
|
<div id="ql-logical" class="section">
|
||||||
|
<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>
|
||||||
|
{ <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="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="o">"$not"</span>: { <span class="k">"status"</span>: <span class="n">500</span> } }</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- $consider -->
|
||||||
|
<div id="ql-consider" class="section">
|
||||||
|
<h2>$consider</h2>
|
||||||
|
<p>By default, matching conditions mean the monitor is <strong style="color:#4ade80">up</strong>. Set <code style="color:#93c5fd;background:#0f172a;padding:0.1em 0.35em;border-radius:0.2rem;font-size:0.78rem">"$consider": "down"</code> to flip this — if the conditions match, the monitor is <strong style="color:#f87171">down</strong>.</p>
|
||||||
|
<div class="cb">
|
||||||
|
<div class="cb-header"><span class="cb-lang">json</span></div>
|
||||||
|
<pre><span class="c">// down if response time exceeds 2 seconds</span>
|
||||||
|
{ <span class="o">"$consider"</span>: <span class="s">"down"</span>, <span class="k">"$responseTime"</span>: { <span class="o">"$gt"</span>: <span class="n">2000</span> } }
|
||||||
|
|
||||||
|
<span class="c">// down if cert expires in less than 7 days</span>
|
||||||
|
{ <span class="o">"$consider"</span>: <span class="s">"down"</span>, <span class="k">"$certExpiry"</span>: { <span class="o">"$lt"</span>: <span class="n">7</span> } }
|
||||||
|
|
||||||
|
<span class="c">// down if any of these match</span>
|
||||||
|
{
|
||||||
|
<span class="o">"$consider"</span>: <span class="s">"down"</span>,
|
||||||
|
<span class="o">"$or"</span>: [
|
||||||
|
{ <span class="k">"status"</span>: { <span class="o">"$gte"</span>: <span class="n">500</span> } },
|
||||||
|
{ <span class="k">"$responseTime"</span>: { <span class="o">"$gt"</span>: <span class="n">5000</span> } }
|
||||||
|
]
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Examples -->
|
||||||
|
<div id="ql-examples" class="section">
|
||||||
|
<h2>Examples</h2>
|
||||||
|
|
||||||
|
<h3>Basic health endpoint</h3>
|
||||||
|
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||||
|
<pre>{ <span class="k">"status"</span>: <span class="n">200</span>, <span class="k">"body"</span>: { <span class="o">"$contains"</span>: <span class="s">"healthy"</span> } }</pre></div>
|
||||||
|
|
||||||
|
<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> } } }
|
||||||
|
]
|
||||||
|
}</pre></div>
|
||||||
|
|
||||||
|
<h3>Performance monitor (mark down if slow)</h3>
|
||||||
|
<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>, <span class="k">"$responseTime"</span>: { <span class="o">"$gt"</span>: <span class="n">1000</span> } }</pre></div>
|
||||||
|
|
||||||
|
<h3>Cert expiry alert</h3>
|
||||||
|
<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>, <span class="k">"$certExpiry"</span>: { <span class="o">"$lt"</span>: <span class="n">14</span> } }</pre></div>
|
||||||
|
|
||||||
|
<h3>Status page (HTML)</h3>
|
||||||
|
<div class="cb"><div class="cb-header"><span class="cb-lang">json</span></div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12 pt-6 border-t border-gray-900 text-sm text-gray-600">
|
||||||
|
PingQL · <a href="/dashboard" class="hover:text-gray-400 transition-colors">Dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Highlight active nav link on scroll
|
||||||
|
const sections = document.querySelectorAll('[id]');
|
||||||
|
const navLinks = document.querySelectorAll('.nav-link');
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
navLinks.forEach(l => l.classList.remove('active'));
|
||||||
|
const link = document.querySelector(`.nav-link[href="#${entry.target.id}"]`);
|
||||||
|
if (link) link.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { rootMargin: '-20% 0px -70% 0px' });
|
||||||
|
sections.forEach(s => observer.observe(s));
|
||||||
|
|
||||||
|
// Copy buttons
|
||||||
|
document.querySelectorAll('.cb').forEach(cb => {
|
||||||
|
const btn = cb.querySelector('.cb-copy');
|
||||||
|
if (!btn) return;
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const text = cb.querySelector('pre').innerText;
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
btn.textContent = 'Copied!';
|
||||||
|
setTimeout(() => btn.textContent = 'Copy', 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Add copy buttons to all codeblocks
|
||||||
|
document.querySelectorAll('.cb-header').forEach(h => {
|
||||||
|
if (!h.querySelector('.cb-copy')) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'cb-copy';
|
||||||
|
btn.textContent = 'Copy';
|
||||||
|
h.appendChild(btn);
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const text = h.nextElementSibling.innerText;
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
btn.textContent = 'Copied!';
|
||||||
|
setTimeout(() => btn.textContent = 'Copy', 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@
|
||||||
<nav class="border-b border-gray-800 px-6 py-4 flex items-center justify-between">
|
<nav class="border-b border-gray-800 px-6 py-4 flex items-center justify-between">
|
||||||
<a href="/dashboard/home" class="text-xl font-bold tracking-tight">Ping<span class="text-blue-400">QL</span></a>
|
<a href="/dashboard/home" class="text-xl font-bold tracking-tight">Ping<span class="text-blue-400">QL</span></a>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href="/dashboard/docs" class="text-gray-500 hover:text-gray-300 text-sm transition-colors">Query Docs</a>
|
|
||||||
<a href="/docs" class="text-gray-500 hover:text-gray-300 text-sm transition-colors">API</a>
|
|
||||||
<a href="/dashboard/monitors/new" class="bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors">+ New Monitor</a>
|
<a href="/dashboard/monitors/new" class="bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors">+ New Monitor</a>
|
||||||
<button onclick="logout()" class="text-gray-500 hover:text-gray-300 text-sm transition-colors">Logout</button>
|
<button onclick="logout()" class="text-gray-500 hover:text-gray-300 text-sm transition-colors">Logout</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Elysia } from "elysia";
|
import { Elysia } from "elysia";
|
||||||
import { cors } from "@elysiajs/cors";
|
import { cors } from "@elysiajs/cors";
|
||||||
import { swagger } from "@elysiajs/swagger";
|
|
||||||
import { ingest } from "./routes/pings";
|
import { ingest } from "./routes/pings";
|
||||||
import { monitors } from "./routes/monitors";
|
import { monitors } from "./routes/monitors";
|
||||||
import { account } from "./routes/auth";
|
import { account } from "./routes/auth";
|
||||||
|
|
@ -12,100 +11,7 @@ await migrate();
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.use(cors())
|
.use(cors())
|
||||||
.use(swagger({
|
.get("/", () => ({ name: "PingQL", version: "0.1.0", docs: "/docs", dashboard: "/dashboard" }))
|
||||||
path: "/docs",
|
|
||||||
documentation: {
|
|
||||||
info: {
|
|
||||||
title: "PingQL API",
|
|
||||||
version: "0.1.0",
|
|
||||||
description: `
|
|
||||||
## Query Language
|
|
||||||
|
|
||||||
A MongoDB-style query language for defining when a monitor is **up** or **down**.
|
|
||||||
By default (no query), a monitor is up when HTTP status < 400.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Fields
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| \`status\` | number | HTTP status code |
|
|
||||||
| \`body\` | string | Response body as text |
|
|
||||||
| \`headers.<name>\` | string | Response header (e.g. \`headers.content-type\`) |
|
|
||||||
| \`$responseTime\` | number | Latency in milliseconds |
|
|
||||||
| \`$certExpiry\` | number | Days until SSL cert expires |
|
|
||||||
| \`$json\` | object | JSONPath against response body |
|
|
||||||
| \`$select\` | object | CSS selector against response HTML |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Operators
|
|
||||||
|
|
||||||
| Operator | Description |
|
|
||||||
|---|---|
|
|
||||||
| \`$eq\` \`$ne\` | Equal / not equal |
|
|
||||||
| \`$gt\` \`$gte\` \`$lt\` \`$lte\` | Numeric comparison |
|
|
||||||
| \`$contains\` \`$startsWith\` \`$endsWith\` | String matching |
|
|
||||||
| \`$regex\` | Regular expression |
|
|
||||||
| \`$exists\` | Field is present and non-null |
|
|
||||||
| \`$in\` | Value is in array |
|
|
||||||
| \`$and\` \`$or\` \`$not\` | Logical operators |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
**Status code:**
|
|
||||||
\`\`\`json
|
|
||||||
{ "status": { "$lt": 400 } }
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Body content:**
|
|
||||||
\`\`\`json
|
|
||||||
{ "body": { "$contains": "healthy" } }
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**JSON response field:**
|
|
||||||
\`\`\`json
|
|
||||||
{ "$json": { "$.data.status": { "$eq": "ok" } } }
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**CSS selector (HTML pages):**
|
|
||||||
\`\`\`json
|
|
||||||
{ "$select": { ".status-badge": { "$contains": "operational" } } }
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Response time:**
|
|
||||||
\`\`\`json
|
|
||||||
{ "$responseTime": { "$lt": 500 } }
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**SSL cert expiry:**
|
|
||||||
\`\`\`json
|
|
||||||
{ "$certExpiry": { "$gt": 14 } }
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Logical:**
|
|
||||||
\`\`\`json
|
|
||||||
{ "$and": [{ "status": 200 }, { "body": { "$contains": "ok" } }] }
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**$consider — mark DOWN when conditions match:**
|
|
||||||
\`\`\`json
|
|
||||||
{ "$consider": "down", "$responseTime": { "$gt": 2000 } }
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Full docs: [/dashboard/docs](/dashboard/docs)
|
|
||||||
`.trim(),
|
|
||||||
},
|
|
||||||
tags: [
|
|
||||||
{ name: "account", description: "Account registration and settings" },
|
|
||||||
{ name: "monitors", description: "Create and manage monitors" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.get("/", () => ({ name: "PingQL", version: "0.1.0", docs: "/docs", dashboard: "/dashboard" }), { detail: { hide: true } })
|
|
||||||
.use(dashboard)
|
.use(dashboard)
|
||||||
.use(account)
|
.use(account)
|
||||||
.use(monitors)
|
.use(monitors)
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,4 @@ export const dashboard = new Elysia()
|
||||||
.get("/dashboard/home", () => Bun.file(`${dir}/home.html`), hide)
|
.get("/dashboard/home", () => Bun.file(`${dir}/home.html`), hide)
|
||||||
.get("/dashboard/monitors/new", () => Bun.file(`${dir}/new.html`), hide)
|
.get("/dashboard/monitors/new", () => Bun.file(`${dir}/new.html`), hide)
|
||||||
.get("/dashboard/monitors/:id", () => Bun.file(`${dir}/detail.html`), hide)
|
.get("/dashboard/monitors/:id", () => Bun.file(`${dir}/detail.html`), hide)
|
||||||
.get("/dashboard/docs", () => Bun.file(`${dir}/docs.html`), hide);
|
.get("/docs", () => Bun.file(`${dir}/docs.html`), hide);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue