@getjack/jack 0.1.34 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/commands/down.ts +39 -7
- package/src/commands/link.ts +2 -4
- package/src/commands/logs.ts +2 -4
- package/src/commands/mcp.ts +12 -10
- package/src/commands/services.ts +4 -2
- package/src/commands/sync.ts +5 -6
- package/src/commands/update.ts +1 -0
- package/src/index.ts +8 -0
- package/src/lib/auth/client.ts +5 -2
- package/src/lib/binding-validator.ts +39 -3
- package/src/lib/build-helper.ts +18 -19
- package/src/lib/control-plane.ts +45 -0
- package/src/lib/do-config.ts +110 -0
- package/src/lib/do-export-validator.ts +26 -0
- package/src/lib/jsonc-edit.ts +292 -0
- package/src/lib/managed-deploy.ts +36 -1
- package/src/lib/project-link.ts +37 -0
- package/src/lib/project-operations.ts +31 -66
- package/src/lib/resources.ts +4 -5
- package/src/lib/schema.ts +8 -12
- package/src/lib/services/db-create.ts +2 -2
- package/src/lib/services/db-execute.ts +9 -6
- package/src/lib/services/db-list.ts +6 -4
- package/src/lib/services/endpoint-test.ts +275 -0
- package/src/lib/services/project-delete.ts +190 -0
- package/src/lib/services/project-environment.ts +579 -0
- package/src/lib/services/storage-config.ts +7 -309
- package/src/lib/services/storage-create.ts +2 -1
- package/src/lib/services/storage-delete.ts +3 -2
- package/src/lib/services/storage-info.ts +2 -1
- package/src/lib/services/storage-list.ts +6 -3
- package/src/lib/services/vectorize-config.ts +7 -264
- package/src/lib/services/vectorize-create.ts +2 -1
- package/src/lib/services/vectorize-delete.ts +6 -4
- package/src/lib/services/vectorize-list.ts +6 -3
- package/src/lib/storage/index.ts +21 -23
- package/src/lib/telemetry.ts +1 -0
- package/src/lib/wrangler-config.ts +43 -312
- package/src/lib/zip-packager.ts +28 -0
- package/src/mcp/test-utils.ts +31 -0
- package/src/mcp/tools/index.ts +280 -2
- package/src/templates/index.ts +5 -0
- package/src/templates/types.ts +4 -0
- package/templates/AI-BINDINGS.md +34 -76
- package/templates/CLAUDE.md +1 -1
- package/templates/ai-chat/src/index.ts +7 -14
- package/templates/ai-chat/src/jack-ai.ts +0 -6
- package/templates/chat/.jack.json +45 -0
- package/templates/chat/bun.lock +1584 -0
- package/templates/chat/components.json +23 -0
- package/templates/chat/index.html +12 -0
- package/templates/chat/package.json +41 -0
- package/templates/chat/src/chat-agent.ts +63 -0
- package/templates/chat/src/client/app.tsx +189 -0
- package/templates/chat/src/client/chat.tsx +222 -0
- package/templates/chat/src/client/components/prompt-kit/chat-container.tsx +47 -0
- package/templates/chat/src/client/components/prompt-kit/loader.tsx +33 -0
- package/templates/chat/src/client/components/prompt-kit/markdown.tsx +84 -0
- package/templates/chat/src/client/components/prompt-kit/message.tsx +54 -0
- package/templates/chat/src/client/components/prompt-kit/prompt-suggestion.tsx +20 -0
- package/templates/chat/src/client/components/prompt-kit/reasoning.tsx +134 -0
- package/templates/chat/src/client/components/prompt-kit/scroll-button.tsx +28 -0
- package/templates/chat/src/client/components/ui/button.tsx +38 -0
- package/templates/chat/src/client/lib/utils.ts +6 -0
- package/templates/chat/src/client/main.tsx +11 -0
- package/templates/chat/src/client/styles.css +125 -0
- package/templates/chat/src/index.ts +25 -0
- package/templates/chat/src/jack-ai.ts +94 -0
- package/templates/chat/tsconfig.json +18 -0
- package/templates/chat/vite.config.ts +14 -0
- package/templates/chat/wrangler.jsonc +18 -0
- package/templates/cron/.jack.json +18 -28
- package/templates/cron/schema.sql +10 -20
- package/templates/cron/src/admin.ts +321 -0
- package/templates/cron/src/index.ts +151 -81
- package/templates/cron/src/monitor.ts +124 -0
- package/templates/semantic-search/src/index.ts +5 -43
- package/templates/semantic-search/src/jack-ai.ts +0 -6
- package/templates/telegram-bot/.jack.json +56 -0
- package/templates/telegram-bot/bun.lock +41 -0
- package/templates/telegram-bot/package.json +16 -0
- package/templates/telegram-bot/src/index.ts +236 -0
- package/templates/telegram-bot/src/jack-ai.ts +100 -0
- package/templates/telegram-bot/tsconfig.json +11 -0
- package/templates/telegram-bot/wrangler.jsonc +8 -0
- package/templates/cron/src/jobs.ts +0 -139
- package/templates/cron/src/webhooks.ts +0 -95
- package/templates/semantic-search/src/jack-vectorize.ts +0 -169
|
@@ -1,42 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cron",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Uptime monitor with cron checks, latency tracking, and live dashboard",
|
|
4
4
|
"secrets": [],
|
|
5
5
|
"capabilities": ["db"],
|
|
6
6
|
"requires": ["DB", "CRON"],
|
|
7
|
+
"crons": ["*/15 * * * *"],
|
|
7
8
|
"intent": {
|
|
8
9
|
"keywords": [
|
|
9
10
|
"cron",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"webhook",
|
|
11
|
+
"uptime",
|
|
12
|
+
"monitor",
|
|
13
13
|
"scheduled",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
14
|
+
"health",
|
|
15
|
+
"ping",
|
|
16
|
+
"status"
|
|
17
17
|
],
|
|
18
18
|
"examples": [
|
|
19
|
-
"
|
|
20
|
-
"scheduled
|
|
21
|
-
"
|
|
22
|
-
"job queue"
|
|
19
|
+
"uptime monitor",
|
|
20
|
+
"scheduled health checks",
|
|
21
|
+
"status page"
|
|
23
22
|
]
|
|
24
23
|
},
|
|
25
24
|
"agentContext": {
|
|
26
|
-
"summary": "
|
|
27
|
-
"full_text": "## Project Structure\n\n- `src/index.ts` - Hono API with cron handler,
|
|
25
|
+
"summary": "An uptime monitor that checks URLs every 15 minutes, tracks latency and uptime percentage, with a live dashboard.",
|
|
26
|
+
"full_text": "## Project Structure\n\n- `src/index.ts` - Hono API with routes, cron handler, admin page\n- `src/monitor.ts` - URL checking with groups, fetch with timeout, D1 storage\n- `src/admin.ts` - Inline HTML/CSS/JS dashboard (no external deps)\n- `schema.sql` - D1 schema (checks table with group_name)\n\n## Routes\n\n| Method | Path | Purpose |\n|--------|------|---------|\n| GET / | Live dashboard (HTML) |\n| GET /api/status | Per-group uptime stats (URLs hidden) |\n| GET /api/checks | Recent 100 checks (URLs hidden) |\n| POST /api/trigger | Manually trigger checks |\n| POST /__scheduled | Cron handler (called every 15 min) |\n| GET /health | Health check |\n\n## How It Works\n\nEvery 15 minutes the cron checks all monitored URLs grouped by name. URLs are never exposed publicly — only group names appear on the dashboard and in API responses.\n\nBy default it monitors `https://1.1.1.1` (Cloudflare DNS) in the \"Default\" group.\n\n## Configuring Monitor Groups\n\nSet the `MONITOR_URLS` secret with named groups separated by semicolons:\n```\nProduction=https://api.example.com,https://app.example.com;Staging=https://staging.example.com/health\n```\n\nPlain URLs without a group name go into \"Default\":\n```\nhttps://example.com,https://api.example.com\n```\n\n## Dashboard\n\nThe dashboard auto-refreshes every 10 seconds and shows:\n- Status banner (all systems operational / issues detected)\n- Per-group cards with endpoint count, uptime %, avg latency\n- Recent checks table with group name and source (cron/manual)\n\nActual URLs are never shown — only group names are visible publicly.\n\n## Resources\n\n- [Hono Documentation](https://hono.dev)"
|
|
28
27
|
},
|
|
29
28
|
"hooks": {
|
|
30
|
-
"preCreate": [
|
|
31
|
-
{
|
|
32
|
-
"action": "require",
|
|
33
|
-
"source": "secret",
|
|
34
|
-
"key": "WEBHOOK_SECRET",
|
|
35
|
-
"message": "Generating webhook signing secret...",
|
|
36
|
-
"onMissing": "generate",
|
|
37
|
-
"generateCommand": "openssl rand -hex 32"
|
|
38
|
-
}
|
|
39
|
-
],
|
|
40
29
|
"postDeploy": [
|
|
41
30
|
{
|
|
42
31
|
"action": "clipboard",
|
|
@@ -50,15 +39,16 @@
|
|
|
50
39
|
},
|
|
51
40
|
{
|
|
52
41
|
"action": "box",
|
|
53
|
-
"title": "
|
|
42
|
+
"title": "Uptime monitor live: {{name}}",
|
|
54
43
|
"lines": [
|
|
55
44
|
"{{url}}",
|
|
56
45
|
"",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
" GET {{url}}/jobs — job queue status",
|
|
46
|
+
"Dashboard: {{url}}/",
|
|
47
|
+
"API: {{url}}/api/status",
|
|
60
48
|
"",
|
|
61
|
-
"
|
|
49
|
+
"Checks every 15 minutes automatically",
|
|
50
|
+
"Configure: set MONITOR_URLS secret",
|
|
51
|
+
" Format: Group=url1,url2;Other=url3"
|
|
62
52
|
]
|
|
63
53
|
}
|
|
64
54
|
]
|
|
@@ -1,24 +1,14 @@
|
|
|
1
|
-
CREATE TABLE IF NOT EXISTS
|
|
1
|
+
CREATE TABLE IF NOT EXISTS checks (
|
|
2
2
|
id TEXT PRIMARY KEY,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
11
|
-
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
CREATE TABLE IF NOT EXISTS webhook_events (
|
|
15
|
-
id TEXT PRIMARY KEY,
|
|
16
|
-
source TEXT NOT NULL DEFAULT 'unknown',
|
|
17
|
-
event_type TEXT,
|
|
18
|
-
payload TEXT NOT NULL,
|
|
19
|
-
status TEXT NOT NULL DEFAULT 'received',
|
|
3
|
+
url TEXT NOT NULL,
|
|
4
|
+
group_name TEXT NOT NULL DEFAULT 'Default',
|
|
5
|
+
status_code INTEGER,
|
|
6
|
+
latency_ms INTEGER NOT NULL,
|
|
7
|
+
ok INTEGER NOT NULL DEFAULT 1,
|
|
8
|
+
source TEXT NOT NULL DEFAULT 'cron',
|
|
9
|
+
error TEXT,
|
|
20
10
|
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
21
11
|
);
|
|
22
12
|
|
|
23
|
-
CREATE INDEX IF NOT EXISTS
|
|
24
|
-
CREATE INDEX IF NOT EXISTS
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_checks_group_created ON checks(group_name, created_at);
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_checks_created ON checks(created_at);
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
export function adminHTML(): string {
|
|
2
|
+
return `<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>Uptime Monitor</title>
|
|
8
|
+
<style>
|
|
9
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: #0d1117; color: #e6edf3; }
|
|
11
|
+
.container { max-width: 960px; margin: 0 auto; padding: 48px 24px; }
|
|
12
|
+
|
|
13
|
+
header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px; }
|
|
14
|
+
h1 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.02em; }
|
|
15
|
+
button { font-family: inherit; font-size: 0.8125rem; cursor: pointer; transition: all 0.15s; }
|
|
16
|
+
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: 6px; font-weight: 500; }
|
|
17
|
+
.btn-primary { background: #238636; color: #fff; border: 1px solid #2ea043; }
|
|
18
|
+
.btn-primary:hover { background: #2ea043; }
|
|
19
|
+
|
|
20
|
+
/* Global status banner */
|
|
21
|
+
.banner { display: flex; align-items: center; gap: 10px; padding: 14px 18px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; margin-bottom: 28px; }
|
|
22
|
+
.banner .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
23
|
+
.banner .dot.up { background: #3fb950; box-shadow: 0 0 8px rgba(63,185,80,0.4); }
|
|
24
|
+
.banner .dot.down { background: #f85149; box-shadow: 0 0 8px rgba(248,81,73,0.4); }
|
|
25
|
+
.banner .dot.unknown { background: #484f58; }
|
|
26
|
+
.banner-text { font-size: 0.875rem; font-weight: 500; }
|
|
27
|
+
.banner-sub { font-size: 0.75rem; color: #8b949e; margin-left: auto; }
|
|
28
|
+
|
|
29
|
+
/* Monitor group card */
|
|
30
|
+
.group-card { background: #161b22; border: 1px solid #30363d; border-radius: 10px; margin-bottom: 20px; overflow: hidden; }
|
|
31
|
+
.group-header { display: flex; align-items: center; gap: 10px; padding: 18px 20px 0; }
|
|
32
|
+
.group-name { font-size: 1rem; font-weight: 600; }
|
|
33
|
+
.group-endpoints { font-size: 0.75rem; color: #8b949e; margin-left: auto; }
|
|
34
|
+
.group-badge { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 2px 8px; border-radius: 9999px; }
|
|
35
|
+
.group-badge.up { background: rgba(63,185,80,0.15); color: #3fb950; border: 1px solid rgba(63,185,80,0.3); }
|
|
36
|
+
.group-badge.down { background: rgba(248,81,73,0.15); color: #f85149; border: 1px solid rgba(248,81,73,0.3); }
|
|
37
|
+
.group-streak { padding: 4px 20px 16px; font-size: 0.75rem; color: #8b949e; }
|
|
38
|
+
|
|
39
|
+
/* Status bar (24h checks) */
|
|
40
|
+
.status-bar-section { padding: 0 20px 16px; }
|
|
41
|
+
.status-bar-label { display: flex; justify-content: space-between; font-size: 0.75rem; color: #8b949e; margin-bottom: 6px; }
|
|
42
|
+
.status-bar { display: flex; gap: 2px; height: 28px; align-items: flex-end; }
|
|
43
|
+
.status-block { flex: 1; min-width: 3px; border-radius: 2px; transition: opacity 0.15s; }
|
|
44
|
+
.status-block.up { background: #238636; }
|
|
45
|
+
.status-block.down { background: #f85149; }
|
|
46
|
+
.status-block:hover { opacity: 0.7; }
|
|
47
|
+
|
|
48
|
+
/* Period cards row */
|
|
49
|
+
.periods { display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid #30363d; }
|
|
50
|
+
.period { padding: 16px 20px; }
|
|
51
|
+
.period:not(:last-child) { border-right: 1px solid #30363d; }
|
|
52
|
+
.period-label { font-size: 0.6875rem; color: #8b949e; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; }
|
|
53
|
+
.period-value { font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
54
|
+
.period-detail { font-size: 0.6875rem; color: #8b949e; margin-top: 2px; }
|
|
55
|
+
.pct-good { color: #3fb950; }
|
|
56
|
+
.pct-warn { color: #d29922; }
|
|
57
|
+
.pct-bad { color: #f85149; }
|
|
58
|
+
.pct-none { color: #484f58; }
|
|
59
|
+
|
|
60
|
+
/* Response time section */
|
|
61
|
+
.response-section { border-top: 1px solid #30363d; padding: 16px 20px; }
|
|
62
|
+
.response-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
|
63
|
+
.response-title { font-size: 0.8125rem; font-weight: 500; }
|
|
64
|
+
.response-stats { display: flex; gap: 16px; font-size: 0.6875rem; color: #8b949e; }
|
|
65
|
+
.response-stats span { font-variant-numeric: tabular-nums; }
|
|
66
|
+
.response-stats .val { color: #e6edf3; font-weight: 600; }
|
|
67
|
+
.sparkline { display: flex; align-items: flex-end; gap: 1px; height: 40px; }
|
|
68
|
+
.spark-bar { flex: 1; min-width: 2px; border-radius: 1px 1px 0 0; background: #238636; transition: opacity 0.15s; }
|
|
69
|
+
.spark-bar.slow { background: #d29922; }
|
|
70
|
+
.spark-bar.timeout { background: #f85149; }
|
|
71
|
+
.spark-bar:hover { opacity: 0.7; }
|
|
72
|
+
|
|
73
|
+
/* Recent checks table */
|
|
74
|
+
.section { margin-top: 28px; }
|
|
75
|
+
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
|
76
|
+
.section-header h2 { font-size: 0.875rem; font-weight: 500; }
|
|
77
|
+
.section-header .count { font-size: 0.75rem; color: #8b949e; }
|
|
78
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 10px; overflow: hidden; }
|
|
79
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
|
|
80
|
+
th { text-align: left; padding: 10px 16px; font-weight: 500; font-size: 0.75rem; color: #8b949e; border-bottom: 1px solid #30363d; }
|
|
81
|
+
td { padding: 10px 16px; border-bottom: 1px solid #21262d; color: #c9d1d9; }
|
|
82
|
+
tr:last-child td { border-bottom: none; }
|
|
83
|
+
tr:hover td { background: #1c2129; }
|
|
84
|
+
.mono { font-family: 'SF Mono', SFMono-Regular, ui-monospace, monospace; font-size: 0.75rem; }
|
|
85
|
+
.ok { color: #3fb950; }
|
|
86
|
+
.fail { color: #f85149; }
|
|
87
|
+
|
|
88
|
+
.empty-state { padding: 48px 24px; text-align: center; }
|
|
89
|
+
.empty-state p { color: #8b949e; font-size: 0.875rem; margin-bottom: 4px; }
|
|
90
|
+
.empty-state .hint { color: #484f58; font-size: 0.8125rem; }
|
|
91
|
+
|
|
92
|
+
.toast { position: fixed; bottom: 24px; right: 24px; background: #e6edf3; color: #0d1117; padding: 10px 20px; border-radius: 8px; font-size: 0.8125rem; font-weight: 500; opacity: 0; transition: opacity 0.2s; pointer-events: none; }
|
|
93
|
+
.toast.show { opacity: 1; }
|
|
94
|
+
.toast.error { background: #f85149; color: #fff; }
|
|
95
|
+
|
|
96
|
+
@media (max-width: 640px) {
|
|
97
|
+
.periods { grid-template-columns: 1fr; }
|
|
98
|
+
.period:not(:last-child) { border-right: none; border-bottom: 1px solid #30363d; }
|
|
99
|
+
.container { padding: 24px 16px; }
|
|
100
|
+
header { flex-direction: column; align-items: flex-start; gap: 12px; }
|
|
101
|
+
.response-stats { flex-wrap: wrap; gap: 8px; }
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
104
|
+
</head>
|
|
105
|
+
<body>
|
|
106
|
+
<div class="container">
|
|
107
|
+
|
|
108
|
+
<header>
|
|
109
|
+
<h1>Uptime Monitor</h1>
|
|
110
|
+
<button class="btn btn-primary" onclick="trigger()">Check now</button>
|
|
111
|
+
</header>
|
|
112
|
+
|
|
113
|
+
<div class="banner" id="banner">
|
|
114
|
+
<div class="dot unknown" id="status-dot"></div>
|
|
115
|
+
<span class="banner-text" id="status-text">Loading...</span>
|
|
116
|
+
<span class="banner-sub">Checked every 15 min</span>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div id="monitors"></div>
|
|
120
|
+
|
|
121
|
+
<div class="section">
|
|
122
|
+
<div class="section-header">
|
|
123
|
+
<h2>Recent Checks</h2>
|
|
124
|
+
<span class="count" id="checks-count"></span>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="card">
|
|
127
|
+
<table>
|
|
128
|
+
<thead><tr><th>Time</th><th>Group</th><th>Status</th><th>Latency</th><th>Source</th></tr></thead>
|
|
129
|
+
<tbody id="checks"></tbody>
|
|
130
|
+
</table>
|
|
131
|
+
<div class="empty-state" id="checks-empty">
|
|
132
|
+
<p>No checks yet</p>
|
|
133
|
+
<p class="hint">Click "Check now" or wait for the first automatic check.</p>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="toast" id="toast"></div>
|
|
141
|
+
|
|
142
|
+
<script>
|
|
143
|
+
function toast(msg, isError) {
|
|
144
|
+
var el = document.getElementById('toast');
|
|
145
|
+
el.textContent = msg;
|
|
146
|
+
el.className = 'toast show' + (isError ? ' error' : '');
|
|
147
|
+
setTimeout(function() { el.className = 'toast'; }, 2500);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function timeAgo(unix) {
|
|
151
|
+
var diff = Math.floor(Date.now() / 1000) - unix;
|
|
152
|
+
if (diff < 5) return 'just now';
|
|
153
|
+
if (diff < 60) return diff + 's ago';
|
|
154
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
155
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
156
|
+
return Math.floor(diff / 86400) + 'd ago';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function duration(seconds) {
|
|
160
|
+
if (seconds < 60) return seconds + 's';
|
|
161
|
+
if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
|
|
162
|
+
if (seconds < 86400) {
|
|
163
|
+
var h = Math.floor(seconds / 3600);
|
|
164
|
+
var m = Math.floor((seconds % 3600) / 60);
|
|
165
|
+
return h + 'h ' + m + 'm';
|
|
166
|
+
}
|
|
167
|
+
var d = Math.floor(seconds / 86400);
|
|
168
|
+
var hrs = Math.floor((seconds % 86400) / 3600);
|
|
169
|
+
return d + 'd ' + hrs + 'h';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function pctClass(pct) {
|
|
173
|
+
if (pct === null) return 'pct-none';
|
|
174
|
+
if (pct >= 99.5) return 'pct-good';
|
|
175
|
+
if (pct >= 95) return 'pct-warn';
|
|
176
|
+
return 'pct-bad';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderStatusBar(checks) {
|
|
180
|
+
if (!checks.length) return '<div class="status-bar"></div>';
|
|
181
|
+
return '<div class="status-bar">' + checks.map(function(c) {
|
|
182
|
+
return '<div class="status-block ' + (c.ok ? 'up' : 'down') + '" title="' + c.latency_ms + 'ms"></div>';
|
|
183
|
+
}).join('') + '</div>';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderSparkline(checks) {
|
|
187
|
+
if (!checks.length) return '';
|
|
188
|
+
var maxMs = Math.max.apply(null, checks.map(function(c) { return c.latency_ms; }));
|
|
189
|
+
if (maxMs === 0) maxMs = 1;
|
|
190
|
+
return '<div class="sparkline">' + checks.map(function(c) {
|
|
191
|
+
var h = Math.max(3, Math.round(40 * c.latency_ms / maxMs));
|
|
192
|
+
var cls = 'spark-bar';
|
|
193
|
+
if (c.latency_ms > 5000) cls += ' timeout';
|
|
194
|
+
else if (c.latency_ms > 1000) cls += ' slow';
|
|
195
|
+
return '<div class="' + cls + '" style="height:' + h + 'px" title="' + c.latency_ms + 'ms"></div>';
|
|
196
|
+
}).join('') + '</div>';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderPeriod(label, p) {
|
|
200
|
+
var val = p.uptime_pct !== null ? p.uptime_pct + '%' : '--.--%';
|
|
201
|
+
var detail = p.total_checks > 0
|
|
202
|
+
? p.failed_checks + ' failed / ' + p.total_checks + ' checks'
|
|
203
|
+
: 'No data';
|
|
204
|
+
return '<div class="period">'
|
|
205
|
+
+ '<div class="period-label">' + label + '</div>'
|
|
206
|
+
+ '<div class="period-value ' + pctClass(p.uptime_pct) + '">' + val + '</div>'
|
|
207
|
+
+ '<div class="period-detail">' + detail + '</div>'
|
|
208
|
+
+ '</div>';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function refresh() {
|
|
212
|
+
try {
|
|
213
|
+
var responses = await Promise.all([
|
|
214
|
+
fetch('/api/status'), fetch('/api/checks')
|
|
215
|
+
]);
|
|
216
|
+
var statusData = await responses[0].json();
|
|
217
|
+
var checksData = await responses[1].json();
|
|
218
|
+
|
|
219
|
+
var monitors = statusData.monitors || [];
|
|
220
|
+
var checks = checksData.checks || [];
|
|
221
|
+
|
|
222
|
+
// Banner
|
|
223
|
+
var dot = document.getElementById('status-dot');
|
|
224
|
+
var statusText = document.getElementById('status-text');
|
|
225
|
+
if (monitors.length === 0) {
|
|
226
|
+
dot.className = 'dot unknown';
|
|
227
|
+
statusText.textContent = 'No data yet';
|
|
228
|
+
} else {
|
|
229
|
+
var allUp = monitors.every(function(m) { return m.all_up; });
|
|
230
|
+
dot.className = 'dot ' + (allUp ? 'up' : 'down');
|
|
231
|
+
statusText.textContent = allUp ? 'All systems operational' : 'Issues detected';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Group cards
|
|
235
|
+
var monitorsEl = document.getElementById('monitors');
|
|
236
|
+
if (monitors.length === 0) {
|
|
237
|
+
monitorsEl.innerHTML = '';
|
|
238
|
+
} else {
|
|
239
|
+
monitorsEl.innerHTML = monitors.map(function(m) {
|
|
240
|
+
var endpoints = m.endpoint_count || 1;
|
|
241
|
+
var streakText = m.all_up
|
|
242
|
+
? 'Currently up for ' + duration(m.streak_seconds)
|
|
243
|
+
: 'Currently experiencing issues';
|
|
244
|
+
var pct24 = m.periods['24h'].uptime_pct;
|
|
245
|
+
var barLabel = pct24 !== null ? pct24 + '%' : 'No data';
|
|
246
|
+
|
|
247
|
+
return '<div class="group-card">'
|
|
248
|
+
+ '<div class="group-header">'
|
|
249
|
+
+ '<span class="group-badge ' + (m.all_up ? 'up' : 'down') + '">' + (m.all_up ? 'Up' : 'Down') + '</span>'
|
|
250
|
+
+ '<span class="group-name">' + m.group_name + '</span>'
|
|
251
|
+
+ '<span class="group-endpoints">' + endpoints + ' endpoint' + (endpoints === 1 ? '' : 's') + '</span>'
|
|
252
|
+
+ '</div>'
|
|
253
|
+
+ '<div class="group-streak">' + streakText + '</div>'
|
|
254
|
+
|
|
255
|
+
+ '<div class="status-bar-section">'
|
|
256
|
+
+ '<div class="status-bar-label"><span>Last 24 hours</span><span>' + barLabel + '</span></div>'
|
|
257
|
+
+ renderStatusBar(m.recent_checks)
|
|
258
|
+
+ '</div>'
|
|
259
|
+
|
|
260
|
+
+ '<div class="periods">'
|
|
261
|
+
+ renderPeriod('24 hours', m.periods['24h'])
|
|
262
|
+
+ renderPeriod('7 days', m.periods['7d'])
|
|
263
|
+
+ renderPeriod('30 days', m.periods['30d'])
|
|
264
|
+
+ '</div>'
|
|
265
|
+
|
|
266
|
+
+ '<div class="response-section">'
|
|
267
|
+
+ '<div class="response-header">'
|
|
268
|
+
+ '<span class="response-title">Response time</span>'
|
|
269
|
+
+ '<div class="response-stats">'
|
|
270
|
+
+ '<span>avg <span class="val">' + m.latency.avg + 'ms</span></span>'
|
|
271
|
+
+ '<span>min <span class="val">' + m.latency.min + 'ms</span></span>'
|
|
272
|
+
+ '<span>max <span class="val">' + m.latency.max + 'ms</span></span>'
|
|
273
|
+
+ '</div></div>'
|
|
274
|
+
+ renderSparkline(m.recent_checks)
|
|
275
|
+
+ '</div>'
|
|
276
|
+
|
|
277
|
+
+ '</div>';
|
|
278
|
+
}).join('');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Checks table
|
|
282
|
+
var checksBody = document.getElementById('checks');
|
|
283
|
+
var checksEmpty = document.getElementById('checks-empty');
|
|
284
|
+
var checksCount = document.getElementById('checks-count');
|
|
285
|
+
if (checks.length) {
|
|
286
|
+
checksEmpty.style.display = 'none';
|
|
287
|
+
checksCount.textContent = checks.length + ' recent';
|
|
288
|
+
checksBody.innerHTML = checks.map(function(c) { return '<tr>'
|
|
289
|
+
+ '<td>' + timeAgo(c.created_at) + '</td>'
|
|
290
|
+
+ '<td>' + c.group_name + '</td>'
|
|
291
|
+
+ '<td>' + (c.ok ? '<span class="ok">' + (c.status_code || 'OK') + '</span>' : '<span class="fail">' + (c.error || c.status_code || 'FAIL') + '</span>') + '</td>'
|
|
292
|
+
+ '<td class="mono">' + c.latency_ms + 'ms</td>'
|
|
293
|
+
+ '<td>' + (c.source === 'manual' ? '<span style="color:#d29922">manual</span>' : '<span style="color:#3fb950">cron</span>') + '</td>'
|
|
294
|
+
+ '</tr>'; }).join('');
|
|
295
|
+
} else {
|
|
296
|
+
checksEmpty.style.display = '';
|
|
297
|
+
checksCount.textContent = '';
|
|
298
|
+
checksBody.innerHTML = '';
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.error('Refresh error:', e);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function trigger() {
|
|
306
|
+
try {
|
|
307
|
+
var res = await fetch('/api/trigger', { method: 'POST' });
|
|
308
|
+
var data = await res.json();
|
|
309
|
+
if (res.ok) {
|
|
310
|
+
toast(data.all_ok ? 'All ' + data.checked + ' checks passed' : 'Issues detected');
|
|
311
|
+
refresh();
|
|
312
|
+
} else toast('Check failed', true);
|
|
313
|
+
} catch (e) { toast('Network error', true); }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
refresh();
|
|
317
|
+
setInterval(refresh, 10000);
|
|
318
|
+
</script>
|
|
319
|
+
</body>
|
|
320
|
+
</html>`;
|
|
321
|
+
}
|