@stacksjs/ts-cloud 0.5.26 → 0.5.27
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/dist/bin/cli.js +1216 -7359
- package/dist/deploy/dashboard-data-server.d.ts +27 -5
- package/dist/deploy/dashboard-database.d.ts +38 -0
- package/dist/deploy/dashboard-database.test.d.ts +1 -0
- package/dist/deploy/dashboard-operations.d.ts +48 -0
- package/dist/deploy/dashboard-operations.test.d.ts +1 -0
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/local-dashboard-server.d.ts +31 -0
- package/dist/deploy/management-dashboard.d.ts +21 -1
- package/dist/deploy/site-config-editor.d.ts +37 -0
- package/dist/deploy/site-target.d.ts +3 -1
- package/dist/deploy/ssh-config-editor.d.ts +22 -0
- package/dist/deploy/ssh-config-editor.test.d.ts +1 -0
- package/dist/drivers/factory.d.ts +4 -0
- package/dist/drivers/index.d.ts +1 -0
- package/dist/drivers/local-box/driver.d.ts +20 -0
- package/dist/drivers/shared/compute-deploy.d.ts +2 -0
- package/dist/drivers/shared/deploy-script.d.ts +28 -12
- package/dist/drivers/shared/rpx-gateway.d.ts +99 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3373 -202
- package/dist/ui/index.html +83 -1
- package/dist/ui/server/actions.html +1193 -0
- package/dist/ui/server/activity.html +1132 -0
- package/dist/ui/server/backups.html +57 -1
- package/dist/ui/server/database.html +1208 -0
- package/dist/ui/server/deployments.html +79 -1
- package/dist/ui/server/diagnostics.html +1132 -0
- package/dist/ui/server/logs.html +1165 -0
- package/dist/ui/server/metrics.html +1 -1
- package/dist/ui/server/security.html +1132 -0
- package/dist/ui/server/services.html +57 -1
- package/dist/ui/server/sites.html +141 -1
- package/dist/ui/server/ssh-keys.html +58 -1
- package/dist/ui/server/workers.html +57 -1
- package/dist/ui/serverless/assets.html +1 -1
- package/dist/ui/serverless/cost.html +1 -1
- package/dist/ui/serverless/data.html +1 -1
- package/dist/ui/serverless/deployments.html +1 -1
- package/dist/ui/serverless/firewall.html +1 -1
- package/dist/ui/serverless/functions.html +1 -1
- package/dist/ui/serverless/logs.html +1165 -0
- package/dist/ui/serverless/metrics.html +1 -1
- package/dist/ui/serverless/queues.html +1 -1
- package/dist/ui/serverless/scheduler.html +1 -1
- package/dist/ui/serverless/secrets.html +1 -1
- package/dist/ui/serverless.html +1 -1
- package/dist/ui-src/package.json +5 -0
- package/dist/ui-src/pages/index.stx +308 -0
- package/dist/ui-src/pages/partials/head.stx +294 -0
- package/dist/ui-src/pages/partials/nav.stx +54 -0
- package/dist/ui-src/pages/partials/op-confirm.stx +7 -0
- package/dist/ui-src/pages/server/actions.stx +60 -0
- package/dist/ui-src/pages/server/activity.stx +55 -0
- package/dist/ui-src/pages/server/backups.stx +91 -0
- package/dist/ui-src/pages/server/database.stx +119 -0
- package/dist/ui-src/pages/server/deployments.stx +106 -0
- package/dist/ui-src/pages/server/diagnostics.stx +88 -0
- package/dist/ui-src/pages/server/logs.stx +84 -0
- package/dist/ui-src/pages/server/metrics.stx +183 -0
- package/dist/ui-src/pages/server/security.stx +127 -0
- package/dist/ui-src/pages/server/services.stx +82 -0
- package/dist/ui-src/pages/server/sites.stx +143 -0
- package/dist/ui-src/pages/server/ssh-keys.stx +93 -0
- package/dist/ui-src/pages/server/workers.stx +99 -0
- package/dist/ui-src/pages/serverless/assets.stx +66 -0
- package/dist/ui-src/pages/serverless/cost.stx +149 -0
- package/dist/ui-src/pages/serverless/data.stx +76 -0
- package/dist/ui-src/pages/serverless/deployments.stx +56 -0
- package/dist/ui-src/pages/serverless/firewall.stx +58 -0
- package/dist/ui-src/pages/serverless/functions.stx +118 -0
- package/dist/ui-src/pages/serverless/logs.stx +84 -0
- package/dist/ui-src/pages/serverless/metrics.stx +76 -0
- package/dist/ui-src/pages/serverless/queues.stx +81 -0
- package/dist/ui-src/pages/serverless/scheduler.stx +60 -0
- package/dist/ui-src/pages/serverless/secrets.stx +55 -0
- package/dist/ui-src/pages/serverless.stx +222 -0
- package/dist/ui-src/src/charts.ts +17333 -0
- package/package.json +3 -3
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
import { renderDeploymentSparkline, renderResourceBars } from '../src/charts.ts'
|
|
3
|
+
|
|
4
|
+
// ts-cloud Server dashboard. Real data is injected at build time via the
|
|
5
|
+
// TSCLOUD_DASHBOARD_DATA env var (run `cloud dashboard:build`); the sample values
|
|
6
|
+
// below render the design when nothing is injected. Each section drills into a
|
|
7
|
+
// dedicated page under /server/*.
|
|
8
|
+
let __d = null
|
|
9
|
+
try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') }
|
|
10
|
+
catch { __d = null }
|
|
11
|
+
|
|
12
|
+
const meta = {
|
|
13
|
+
title: 'ts-cloud - Dashboard',
|
|
14
|
+
description: 'ts-cloud server & deployment dashboard',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const server = __d?.server ?? {
|
|
18
|
+
name: 'stacks-production-app',
|
|
19
|
+
provider: 'Hetzner',
|
|
20
|
+
region: 'fsn1',
|
|
21
|
+
ip: '-',
|
|
22
|
+
os: 'Linux',
|
|
23
|
+
uptime: '-',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const metrics = __d?.systemMetrics ?? {
|
|
27
|
+
load: 0.42,
|
|
28
|
+
cpus: 4,
|
|
29
|
+
memUsedMb: 2480,
|
|
30
|
+
memTotalMb: 7860,
|
|
31
|
+
diskUsedPct: 38,
|
|
32
|
+
}
|
|
33
|
+
const hasLivePayload = !!__d
|
|
34
|
+
const metricsLive = hasLivePayload ? !!__d?._serverReachable && !__d?.metricsUnavailable : true
|
|
35
|
+
const memPct = metricsLive ? Math.round((metrics.memUsedMb / Math.max(1, metrics.memTotalMb)) * 100) : null
|
|
36
|
+
const loadPct = metricsLive ? Math.min(100, Math.round((metrics.load / Math.max(1, metrics.cpus || 1)) * 100)) : null
|
|
37
|
+
const diskPct = metricsLive ? Math.min(100, Math.round(metrics.diskUsedPct)) : null
|
|
38
|
+
// Threshold tone: green < 70%, amber 70-90%, red >= 90% - shared with the
|
|
39
|
+
// server health view (/server/metrics).
|
|
40
|
+
function healthTone(p) { return p >= 90 ? 'bad' : p >= 70 ? 'warn' : 'ok' }
|
|
41
|
+
const ranks = { ok: 0, warn: 1, bad: 2 }
|
|
42
|
+
const overallTone = metricsLive ? [healthTone(loadPct), healthTone(memPct), healthTone(diskPct)].sort((a, b) => ranks[b] - ranks[a])[0] : 'warn'
|
|
43
|
+
const overallWord = metricsLive ? { ok: 'healthy', warn: 'elevated', bad: 'critical' }[overallTone] : 'probe unavailable'
|
|
44
|
+
const gb = mb => (mb / 1024).toFixed(1)
|
|
45
|
+
const metricStat = (pct, detail) => metricsLive ? `${pct}%` : 'Unavailable'
|
|
46
|
+
const metricSmall = detail => metricsLive ? detail : 'probe unavailable'
|
|
47
|
+
const metricWidth = pct => metricsLive ? pct : 0
|
|
48
|
+
const cpuSmall = ` · ${metrics.load}/${metrics.cpus}`
|
|
49
|
+
const memSmall = ` · ${gb(metrics.memUsedMb)}/${gb(metrics.memTotalMb)} GB`
|
|
50
|
+
|
|
51
|
+
const services = __d?.services ?? [
|
|
52
|
+
{ name: 'rpx-gateway', status: 'configured' },
|
|
53
|
+
{ name: 'redis', status: 'configured' },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const sites = __d?.sites ?? [
|
|
57
|
+
{ name: 'main', route: 'stacksjs.com', href: 'https://stacksjs.com', kind: 'stacks', runtime: 'bun', tls: 'https', deploy: 'service', status: 'live' },
|
|
58
|
+
{ name: 'api', route: 'internal', kind: 'api', runtime: 'bun', tls: 'loopback', deploy: 'service', status: 'live' },
|
|
59
|
+
{ name: 'docs', route: 'stacksjs.com/docs', href: 'https://stacksjs.com/docs', kind: 'bunpress', runtime: 'static/bun', tls: 'https', deploy: 'server static', status: 'live' },
|
|
60
|
+
{ name: 'blog', route: 'stacksjs.com/blog', href: 'https://stacksjs.com/blog', kind: 'bunpress blog', runtime: 'static/bun', tls: 'https', deploy: 'server static', status: 'live' },
|
|
61
|
+
{ name: 'public', route: 'stacksjs.com', href: 'https://stacksjs.com', kind: 'static', runtime: 'static/bun', tls: 'https', deploy: 'server static', status: 'shadowed' },
|
|
62
|
+
{ name: 'verygoodadblock', route: 'verygoodadblock.org', href: 'https://verygoodadblock.org', kind: 'stx static', runtime: 'static/bun', tls: 'https', deploy: 'server static', status: 'live' },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const workers = __d?.workers ?? [
|
|
66
|
+
{ name: 'default', processes: 2, status: 'running' },
|
|
67
|
+
{ name: 'emails', processes: 1, status: 'running' },
|
|
68
|
+
]
|
|
69
|
+
const scheduler = __d?.serverScheduler ?? { enabled: true, lastRun: '36s ago' }
|
|
70
|
+
|
|
71
|
+
const deployments = __d?.serverDeployments ?? [
|
|
72
|
+
]
|
|
73
|
+
const deploymentsEmptyReason = __d?.deploymentsEmptyReason ?? 'No deployment history has been recorded yet.'
|
|
74
|
+
const deploymentChart = renderDeploymentSparkline(deployments)
|
|
75
|
+
|
|
76
|
+
const backup = __d?.backup ?? { enabled: false, last: 'not configured', size: '0 MB', destination: 'off', retention: 0 }
|
|
77
|
+
|
|
78
|
+
const sshKeys = __d?.sshKeys ?? [
|
|
79
|
+
]
|
|
80
|
+
const diagnostics = __d?.diagnostics ?? []
|
|
81
|
+
const activity = __d?.activity ?? []
|
|
82
|
+
const security = __d?.security ?? { firewall: { status: 'configured' }, ports: [], tlsCertificates: [] }
|
|
83
|
+
const failingChecks = diagnostics.filter(check => check.status === 'fail').length
|
|
84
|
+
const warningChecks = diagnostics.filter(check => check.status === 'warn').length
|
|
85
|
+
|
|
86
|
+
function statusTone(status) {
|
|
87
|
+
if (status === 'running' || status === 'live' || status === 'success') return 'ok'
|
|
88
|
+
if (status === 'failed' || status === 'stopped') return 'bad'
|
|
89
|
+
return 'warn'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const resourceChart = renderResourceBars([
|
|
93
|
+
{ label: 'CPU', value: loadPct ?? 0, detail: metricsLive ? `${metrics.load} load / ${metrics.cpus} vCPU` : 'Server probe unavailable' },
|
|
94
|
+
{ label: 'Memory', value: memPct ?? 0, detail: metricsLive ? `${gb(metrics.memUsedMb)} / ${gb(metrics.memTotalMb)} GB` : 'Server probe unavailable' },
|
|
95
|
+
{ label: 'Disk', value: diskPct ?? 0, detail: metricsLive ? `${metrics.diskUsedGb ?? 0} / ${metrics.diskTotalGb ?? 0} GB` : 'Server probe unavailable' },
|
|
96
|
+
], { disabled: !metricsLive })
|
|
97
|
+
|
|
98
|
+
// Expose the full payload to <script client> via the stx server→client bridge
|
|
99
|
+
// (the `__d`/`$`-prefixed names are intentionally bridge-excluded).
|
|
100
|
+
const initialData = __d ?? null
|
|
101
|
+
</script>
|
|
102
|
+
<script client>
|
|
103
|
+
// Live at-a-glance metrics: seeded from build-time data (stx server→client
|
|
104
|
+
// bridge) so the cards render real numbers on first paint, then polled from
|
|
105
|
+
// /api/dashboard-data — bind cards, health pill, indicator, and service
|
|
106
|
+
// statuses reactively (no manual DOM).
|
|
107
|
+
const data = state(initialData)
|
|
108
|
+
const liveData = derived(() => { const d = data(); return !!d && d._serverReachable && !d.metricsUnavailable })
|
|
109
|
+
function n(v, d) { const x = Number(v); return Number.isFinite(x) ? x : (d || 0) }
|
|
110
|
+
function gbVal(mb) { return (n(mb) / 1024).toFixed(1) }
|
|
111
|
+
function pctOf(which) {
|
|
112
|
+
const d = data(); const m = (d && d.systemMetrics) || {}
|
|
113
|
+
if (!liveData()) return null
|
|
114
|
+
if (which === 'load') return Math.min(100, Math.round((n(m.load) / Math.max(1, n(m.cpus, 1))) * 100))
|
|
115
|
+
if (which === 'mem') return Math.round((n(m.memUsedMb) / Math.max(1, n(m.memTotalMb))) * 100)
|
|
116
|
+
return Math.min(100, Math.round(n(m.diskUsedPct)))
|
|
117
|
+
}
|
|
118
|
+
const loadP = derived(() => pctOf('load'))
|
|
119
|
+
const memP = derived(() => pctOf('mem'))
|
|
120
|
+
const diskP = derived(() => pctOf('disk'))
|
|
121
|
+
function pTone(p) { return p == null ? 'warn' : (p >= 90 ? 'bad' : (p >= 70 ? 'warn' : 'ok')) }
|
|
122
|
+
function pDisp(p) { return p == null ? 'Unavailable' : p + '%' }
|
|
123
|
+
function barCss(p) { return 'bar ' + pTone(p) }
|
|
124
|
+
function barStyle(p) { return 'width: ' + (p == null ? 0 : p) + '%' }
|
|
125
|
+
const cpuSub = derived(() => { const m = (data() && data().systemMetrics) || {}; return liveData() ? ' · ' + n(m.load) + '/' + n(m.cpus, 1) : 'probe unavailable' })
|
|
126
|
+
const memSub = derived(() => { const m = (data() && data().systemMetrics) || {}; return liveData() ? ' · ' + gbVal(m.memUsedMb) + '/' + gbVal(m.memTotalMb) + ' GB' : 'probe unavailable' })
|
|
127
|
+
const overallC = derived(() => { if (!liveData()) return 'warn'; const r = { ok: 0, warn: 1, bad: 2 }; return [pTone(loadP()), pTone(memP()), pTone(diskP())].sort((a, b) => r[b] - r[a])[0] })
|
|
128
|
+
const overallWordC = derived(() => liveData() ? ({ ok: 'healthy', warn: 'elevated', bad: 'critical' }[overallC()]) : 'probe unavailable')
|
|
129
|
+
const indicator = derived(() => liveData() ? 'live' : 'snapshot')
|
|
130
|
+
const servicesC = derived(() => (data() && data().services) || [])
|
|
131
|
+
function svcTone(s) { return (s === 'running' || s === 'active') ? 'ok' : ((s === 'failed' || s === 'stopped') ? 'bad' : 'warn') }
|
|
132
|
+
async function refresh() { try { const res = await fetch('/api/dashboard-data'); if (!res.ok) return; data.set(await res.json()) } catch {} }
|
|
133
|
+
onMount(() => { refresh(); const id = setInterval(refresh, 10000); return () => clearInterval(id) })
|
|
134
|
+
</script>
|
|
135
|
+
<!DOCTYPE html>
|
|
136
|
+
<html lang="en" data-theme="dark">
|
|
137
|
+
<head>
|
|
138
|
+
@include('./partials/head', { title: meta.title })
|
|
139
|
+
<meta name="description" content="{{ meta.description }}">
|
|
140
|
+
</head>
|
|
141
|
+
<body>
|
|
142
|
+
@include('./partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
143
|
+
|
|
144
|
+
<div class="wrap">
|
|
145
|
+
<header class="page">
|
|
146
|
+
<div>
|
|
147
|
+
<h1>Server Dashboard</h1>
|
|
148
|
+
<p>{{ server.provider }} · {{ server.region }} · {{ server.os }}</p>
|
|
149
|
+
<div class="server-line">
|
|
150
|
+
<span>IP <b>{{ server.ip }}</b></span>
|
|
151
|
+
<span>Uptime <b>{{ server.uptime }}</b></span>
|
|
152
|
+
<span>CPUs <b>{{ metrics.cpus }}</b></span>
|
|
153
|
+
<span class="pill" :class="overallC()">{{ overallWordC() }}</span>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="header-actions"><span class="env" title="Auto-refreshing every 10s">{{ indicator() }}</span><a class="btn ghost" href="/server/actions">Operations</a></div>
|
|
157
|
+
</header>
|
|
158
|
+
|
|
159
|
+
<div class="grid cards">
|
|
160
|
+
<a class="panel" href="/server/metrics">
|
|
161
|
+
<h3>CPU Load</h3>
|
|
162
|
+
<div class="stat">{{ pDisp(loadP()) }}<small>{{ cpuSub() }}</small></div>
|
|
163
|
+
<div :class="barCss(loadP())"><i :style="barStyle(loadP())"></i></div>
|
|
164
|
+
</a>
|
|
165
|
+
<a class="panel" href="/server/metrics">
|
|
166
|
+
<h3>Memory</h3>
|
|
167
|
+
<div class="stat">{{ pDisp(memP()) }}<small>{{ memSub() }}</small></div>
|
|
168
|
+
<div :class="barCss(memP())"><i :style="barStyle(memP())"></i></div>
|
|
169
|
+
</a>
|
|
170
|
+
<a class="panel" href="/server/metrics">
|
|
171
|
+
<h3>Disk</h3>
|
|
172
|
+
<div class="stat">{{ pDisp(diskP()) }}<small></small></div>
|
|
173
|
+
<div :class="barCss(diskP())"><i :style="barStyle(diskP())"></i></div>
|
|
174
|
+
</a>
|
|
175
|
+
<a class="panel" href="/server/backups">
|
|
176
|
+
<h3>Backups</h3>
|
|
177
|
+
<div class="stat">{{ backup.size }}</div>
|
|
178
|
+
<p style="color: var(--txt2); font-size: 13px; margin-top: 8px;">Last {{ backup.last }}</p>
|
|
179
|
+
</a>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div class="grid cards-3" style="margin-top:16px;">
|
|
183
|
+
<a class="panel" href="/server/security">
|
|
184
|
+
<h3>Security</h3>
|
|
185
|
+
<div class="stat"><span class="pill {{ statusTone(security.firewall.status === 'active' || security.firewall.status === 'configured' ? 'running' : 'configured') }}">{{ security.firewall.status }}</span></div>
|
|
186
|
+
<p style="color: var(--txt2); font-size: 13px; margin-top: 8px;">{{ security.ports.length }} listeners · {{ security.tlsCertificates.length }} certs</p>
|
|
187
|
+
</a>
|
|
188
|
+
<a class="panel" href="/server/diagnostics">
|
|
189
|
+
<h3>Diagnostics</h3>
|
|
190
|
+
<div class="stat">{{ failingChecks }}<small> fail</small></div>
|
|
191
|
+
<p style="color: var(--txt2); font-size: 13px; margin-top: 8px;">{{ warningChecks }} warnings</p>
|
|
192
|
+
</a>
|
|
193
|
+
<a class="panel" href="/server/activity">
|
|
194
|
+
<h3>Activity</h3>
|
|
195
|
+
<div class="stat">{{ activity.length }}</div>
|
|
196
|
+
<p style="color: var(--txt2); font-size: 13px; margin-top: 8px;">deploys, warnings, key changes</p>
|
|
197
|
+
</a>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div class="section">
|
|
201
|
+
<h2>Resource Shape <a class="more" href="/server/metrics">Open health →</a></h2>
|
|
202
|
+
<div class="chart-panel panel">
|
|
203
|
+
{{{ resourceChart }}}
|
|
204
|
+
@if (!metricsLive)
|
|
205
|
+
<p class="note">The dashboard has config/state data, but the live server probe did not return metrics.</p>
|
|
206
|
+
@endif
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div class="section">
|
|
211
|
+
<h2>Services <span class="more"><a href="/server/services">View all →</a> <a href="/server/logs">Logs →</a> <a href="/server/security">Security →</a></span></h2>
|
|
212
|
+
<div class="panel">
|
|
213
|
+
<div class="svc">
|
|
214
|
+
<template :for="svc in servicesC()">
|
|
215
|
+
<div class="item">{{ svc.name }} <span class="pill" :class="svcTone(svc.status)">{{ svc.status }}</span></div>
|
|
216
|
+
</template>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<div class="section">
|
|
222
|
+
<h2>SSH Keys <a class="more" href="/server/ssh-keys">Manage →</a></h2>
|
|
223
|
+
<div class="panel">
|
|
224
|
+
@if (sshKeys.length)
|
|
225
|
+
@foreach (sshKeys as key)
|
|
226
|
+
<div class="key">
|
|
227
|
+
<span class="ok pill"></span>
|
|
228
|
+
<b>{{ key.name }}</b>
|
|
229
|
+
<span style="color: var(--txt3); font-size: 12px">added {{ key.added }}</span>
|
|
230
|
+
<span class="fp">{{ key.fingerprint }}</span>
|
|
231
|
+
</div>
|
|
232
|
+
@endforeach
|
|
233
|
+
@else
|
|
234
|
+
<div class="compact empty"><strong>No SSH keys configured</strong><span>Add keys from the management page and apply them on the next deploy.</span></div>
|
|
235
|
+
@endif
|
|
236
|
+
<p class="note">SSH keys are managed in <span class="mono">infrastructure.compute.sshKeys</span> in your cloud config and applied on the next <span class="mono">cloud deploy</span>.</p>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div class="section">
|
|
241
|
+
<h2>Sites <a class="more" href="/server/sites">View all →</a></h2>
|
|
242
|
+
<div class="panel">
|
|
243
|
+
<table>
|
|
244
|
+
<thead>
|
|
245
|
+
<tr><th>Site</th><th>Route</th><th>Kind</th><th>Runtime</th><th>TLS</th><th>Deploy</th><th>Status</th></tr>
|
|
246
|
+
</thead>
|
|
247
|
+
<tbody>
|
|
248
|
+
@foreach (sites as site)
|
|
249
|
+
<tr>
|
|
250
|
+
<td><b>{{ site.name }}</b></td>
|
|
251
|
+
<td>
|
|
252
|
+
@if (site.href)
|
|
253
|
+
<a href="{{ site.href }}" class="mono">{{ site.route }}</a>
|
|
254
|
+
@else
|
|
255
|
+
<span class="mono">{{ site.route }}</span>
|
|
256
|
+
@endif
|
|
257
|
+
</td>
|
|
258
|
+
<td><span class="tag">{{ site.kind ?? site.type }}</span></td>
|
|
259
|
+
<td class="mono">{{ site.runtime }}</td>
|
|
260
|
+
<td><span class="{{ site.tls === 'https' ? 'ok' : 'warn' }} pill">{{ site.tls }}</span></td>
|
|
261
|
+
<td style="color: var(--txt2)">{{ site.deploy }}</td>
|
|
262
|
+
<td><span class="pill {{ statusTone(site.status) }}">{{ site.status }}</span></td>
|
|
263
|
+
</tr>
|
|
264
|
+
@endforeach
|
|
265
|
+
</tbody>
|
|
266
|
+
</table>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div class="cols section">
|
|
271
|
+
<div>
|
|
272
|
+
<h2>Recent Deployments <a class="more" href="/server/deployments">View all →</a></h2>
|
|
273
|
+
<div class="panel">
|
|
274
|
+
{{{ deploymentChart }}}
|
|
275
|
+
@if (deployments.length)
|
|
276
|
+
@foreach (deployments as d)
|
|
277
|
+
<div class="deploy">
|
|
278
|
+
<span class="pill {{ statusTone(d.status) }}">{{ d.status }}</span>
|
|
279
|
+
<span class="mono">{{ d.sha }}</span>
|
|
280
|
+
<span style="color: var(--txt2)">{{ d.site }} · {{ d.branch }}</span>
|
|
281
|
+
<span class="when">{{ d.when }} · {{ d.took }}</span>
|
|
282
|
+
</div>
|
|
283
|
+
@endforeach
|
|
284
|
+
@else
|
|
285
|
+
<div class="compact empty"><strong>No recent deployments</strong><span>{{ deploymentsEmptyReason }}</span></div>
|
|
286
|
+
@endif
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
<div>
|
|
290
|
+
<h2>Queues & Scheduler <a class="more" href="/server/workers">Details →</a></h2>
|
|
291
|
+
<div class="panel">
|
|
292
|
+
@foreach (workers as w)
|
|
293
|
+
<div class="kv"><span>queue:{{ w.name }} ×{{ w.processes }}</span> <span class="pill {{ statusTone(w.status) }}">{{ w.status }}</span></div>
|
|
294
|
+
@endforeach
|
|
295
|
+
<div class="kv"><span>scheduler</span> @if (scheduler.enabled)<span class="ok pill">on · {{ scheduler.lastRun }}</span>@else<span class="pill warn">off</span>@endif</div>
|
|
296
|
+
</div>
|
|
297
|
+
<div style="height: 16px"></div>
|
|
298
|
+
<h2>Backups <a class="more" href="/server/backups">Details →</a></h2>
|
|
299
|
+
<div class="panel">
|
|
300
|
+
<div class="kv"><span>destination</span> <span class="mono">{{ backup.destination }}</span></div>
|
|
301
|
+
<div class="kv"><span>retention</span> <span>{{ backup.retention }} copies</span></div>
|
|
302
|
+
<div class="kv"><span>last run</span> <span style="color: var(--txt2)">{{ backup.last }}</span></div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</body>
|
|
308
|
+
</html>
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<meta charset="UTF-8">
|
|
2
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3
|
+
<title>{{ title ?? 'ts-cloud' }}</title>
|
|
4
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
5
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
6
|
+
<style>
|
|
7
|
+
:root {
|
|
8
|
+
--bg: #0b1020; --bg2: #0f1730; --panel: rgba(255,255,255,0.04); --panel-br: rgba(255,255,255,0.08);
|
|
9
|
+
--txt: #e8edf7; --txt2: #9fb0cc; --txt3: #64748b;
|
|
10
|
+
--accent: #5b8cff; --ok: #34d399; --okbg: rgba(52,211,153,0.12);
|
|
11
|
+
--bad: #f87171; --badbg: rgba(248,113,113,0.12); --warn: #fbbf24; --warnbg: rgba(251,191,36,0.12);
|
|
12
|
+
}
|
|
13
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
14
|
+
html { background: var(--bg); min-height: 100%; }
|
|
15
|
+
body {
|
|
16
|
+
font-family: 'Inter', system-ui, sans-serif; color: var(--txt); min-height: 100vh;
|
|
17
|
+
background: radial-gradient(1200px 600px at 80% -10%, rgba(91,140,255,0.12), transparent 60%), linear-gradient(160deg, var(--bg), var(--bg2));
|
|
18
|
+
-webkit-font-smoothing: antialiased;
|
|
19
|
+
}
|
|
20
|
+
a { color: inherit; text-decoration: none; }
|
|
21
|
+
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 24px 64px; }
|
|
22
|
+
|
|
23
|
+
nav {
|
|
24
|
+
display: flex; align-items: center; gap: 16px; padding: 18px 24px; position: sticky; top: 0; z-index: 10;
|
|
25
|
+
background: rgba(11,16,32,0.7); backdrop-filter: blur(14px); border-bottom: 1px solid var(--panel-br);
|
|
26
|
+
}
|
|
27
|
+
.brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 18px; letter-spacing: -0.02em; }
|
|
28
|
+
.brand .dot { width: 22px; height: 22px; border-radius: 7px; background: linear-gradient(135deg, var(--accent), #8b5cf6); box-shadow: 0 0 18px rgba(91,140,255,0.6); }
|
|
29
|
+
.tabs { display: flex; gap: 4px; margin-left: 18px; }
|
|
30
|
+
.tabs a { padding: 6px 12px; border-radius: 8px; font-size: 13px; font-weight: 600; color: var(--txt2); }
|
|
31
|
+
.tabs a.active { background: rgba(91,140,255,0.14); color: var(--accent); }
|
|
32
|
+
.nav-meta { margin-left: auto; display: flex; align-items: center; gap: 14px; color: var(--txt2); font-size: 13px; }
|
|
33
|
+
.env { padding: 4px 10px; border-radius: 999px; background: var(--okbg); color: var(--ok); font-weight: 600; font-size: 12px; }
|
|
34
|
+
.env-switch { display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--panel-br); background: rgba(255,255,255,0.035); border-radius: 999px; }
|
|
35
|
+
.env-opt { padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; color: var(--txt2); }
|
|
36
|
+
.env-opt:hover { color: var(--txt); }
|
|
37
|
+
.env-opt.active { background: var(--okbg); color: var(--ok); }
|
|
38
|
+
|
|
39
|
+
header.page { padding: 30px 0 22px; display: flex; align-items: flex-start; gap: 20px; flex-wrap: wrap; }
|
|
40
|
+
header.page h1 { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; }
|
|
41
|
+
header.page p { color: var(--txt2); margin-top: 4px; font-size: 14px; }
|
|
42
|
+
.server-line { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 14px; color: var(--txt2); font-size: 13px; }
|
|
43
|
+
.server-line b { color: var(--txt); font-weight: 600; }
|
|
44
|
+
.header-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; }
|
|
45
|
+
|
|
46
|
+
/* Breadcrumb / back link for drill-down pages */
|
|
47
|
+
.crumbs { display: flex; align-items: center; gap: 8px; padding-top: 26px; font-size: 13px; color: var(--txt3); }
|
|
48
|
+
.crumbs a { color: var(--txt2); }
|
|
49
|
+
.crumbs a:hover { color: var(--accent); }
|
|
50
|
+
.crumbs .sep { color: var(--txt3); }
|
|
51
|
+
|
|
52
|
+
.grid { display: grid; gap: 16px; }
|
|
53
|
+
.cards { grid-template-columns: repeat(4, 1fr); }
|
|
54
|
+
.cards-3 { grid-template-columns: repeat(3, 1fr); }
|
|
55
|
+
.cards-2 { grid-template-columns: repeat(2, 1fr); }
|
|
56
|
+
@media (max-width: 880px) { .cards, .cards-3, .cards-2 { grid-template-columns: repeat(2, 1fr); } }
|
|
57
|
+
.panel { background: var(--panel); border: 1px solid var(--panel-br); border-radius: 16px; padding: 18px; backdrop-filter: blur(6px); }
|
|
58
|
+
a.panel { display: block; transition: border-color .15s, transform .15s; }
|
|
59
|
+
a.panel:hover { border-color: rgba(91,140,255,0.5); transform: translateY(-1px); }
|
|
60
|
+
.panel h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--txt3); font-weight: 700; }
|
|
61
|
+
.stat { font-size: 28px; font-weight: 800; margin-top: 8px; letter-spacing: -0.02em; }
|
|
62
|
+
.stat small { font-size: 14px; color: var(--txt2); font-weight: 500; }
|
|
63
|
+
.bar { height: 7px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 12px; overflow: hidden; }
|
|
64
|
+
.bar > i { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--accent), #8b5cf6); }
|
|
65
|
+
|
|
66
|
+
.section { margin-top: 30px; }
|
|
67
|
+
.section > h2 { font-size: 15px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
|
68
|
+
.section > h2 .more { margin-left: auto; display: inline-flex; align-items: center; gap: 12px; font-size: 12px; font-weight: 600; color: var(--accent); }
|
|
69
|
+
.section > h2 .more a { color: var(--accent); }
|
|
70
|
+
.cols { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; }
|
|
71
|
+
@media (max-width: 880px) { .cols { grid-template-columns: 1fr; } }
|
|
72
|
+
|
|
73
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
74
|
+
th { text-align: left; color: var(--txt3); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; padding: 0 12px 10px; }
|
|
75
|
+
td { padding: 12px; border-top: 1px solid rgba(255,255,255,0.05); }
|
|
76
|
+
tr:hover td { background: rgba(255,255,255,0.02); }
|
|
77
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; color: var(--txt2); }
|
|
78
|
+
|
|
79
|
+
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; }
|
|
80
|
+
.pill::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
|
|
81
|
+
.pill.ok { background: var(--okbg); color: var(--ok); }
|
|
82
|
+
.pill.bad { background: var(--badbg); color: var(--bad); }
|
|
83
|
+
.pill.warn { background: var(--warnbg); color: var(--warn); }
|
|
84
|
+
.tag { padding: 2px 8px; border-radius: 6px; background: rgba(91,140,255,0.12); color: var(--accent); font-size: 12px; font-weight: 600; }
|
|
85
|
+
|
|
86
|
+
.kv { display: flex; justify-content: space-between; padding: 9px 0; border-top: 1px solid rgba(255,255,255,0.05); font-size: 14px; gap: 16px; }
|
|
87
|
+
.kv:first-of-type { border-top: 0; }
|
|
88
|
+
.kv > span { color: var(--txt2); white-space: nowrap; }
|
|
89
|
+
.kv > b { text-align: right; word-break: break-all; }
|
|
90
|
+
.deploy { display: flex; align-items: center; gap: 12px; padding: 11px 0; border-top: 1px solid rgba(255,255,255,0.05); font-size: 14px; }
|
|
91
|
+
.deploy:first-of-type { border-top: 0; }
|
|
92
|
+
.deploy .when { margin-left: auto; color: var(--txt3); font-size: 12px; }
|
|
93
|
+
|
|
94
|
+
.btn { background: linear-gradient(135deg, var(--accent), #8b5cf6); color: #fff; border: 0; border-radius: 10px; padding: 10px 18px; font-weight: 700; font-size: 14px; cursor: pointer; font-family: inherit; white-space: nowrap; }
|
|
95
|
+
.btn:hover { filter: brightness(1.08); }
|
|
96
|
+
.btn.ghost { background: rgba(255,255,255,0.05); border: 1px solid var(--panel-br); color: var(--txt); }
|
|
97
|
+
.btn.danger { background: var(--badbg); color: var(--bad); }
|
|
98
|
+
.btn.sm { padding: 6px 12px; font-size: 12px; }
|
|
99
|
+
|
|
100
|
+
.data-item { display: flex; align-items: center; gap: 12px; padding: 11px 0; border-top: 1px solid rgba(255,255,255,0.05); font-size: 14px; }
|
|
101
|
+
.data-item:first-of-type { border-top: 0; }
|
|
102
|
+
.data-item .detail { color: var(--txt3); font-size: 12px; }
|
|
103
|
+
.data-item .st { margin-left: auto; }
|
|
104
|
+
.secret { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-top: 1px solid rgba(255,255,255,0.05); font-size: 14px; }
|
|
105
|
+
.secret:first-of-type { border-top: 0; }
|
|
106
|
+
.secret .val { margin-left: auto; color: var(--txt3); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; letter-spacing: 2px; }
|
|
107
|
+
|
|
108
|
+
/* Drill-down extras */
|
|
109
|
+
.spark { display: flex; align-items: flex-end; gap: 3px; height: 48px; margin-top: 12px; }
|
|
110
|
+
.spark > i { flex: 1; background: linear-gradient(180deg, var(--accent), rgba(91,140,255,0.25)); border-radius: 3px 3px 0 0; min-height: 2px; }
|
|
111
|
+
.logs { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.7; color: var(--txt2); max-height: 360px; overflow: auto; }
|
|
112
|
+
.logs .ts { color: var(--txt3); }
|
|
113
|
+
.logs .err { color: var(--bad); }
|
|
114
|
+
.log-list { display: grid; gap: 10px; max-height: 680px; overflow: auto; padding-right: 4px; }
|
|
115
|
+
.log-row { border: 1px solid rgba(255,255,255,0.07); border-radius: 12px; background: rgba(255,255,255,0.025); padding: 12px 14px; }
|
|
116
|
+
.log-row.warn { border-color: rgba(251,191,36,0.18); background: rgba(251,191,36,0.04); }
|
|
117
|
+
.log-row.error { border-color: rgba(248,113,113,0.2); background: rgba(248,113,113,0.045); }
|
|
118
|
+
.log-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 9px; color: var(--txt3); font-size: 12px; margin-bottom: 9px; }
|
|
119
|
+
.log-row pre { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--txt2); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.55; }
|
|
120
|
+
.timeline { display: grid; gap: 11px; }
|
|
121
|
+
.timeline-row { display: grid; grid-template-columns: 92px 1fr auto; gap: 13px; align-items: start; border: 1px solid rgba(255,255,255,0.07); border-radius: 12px; background: rgba(255,255,255,0.025); padding: 12px 14px; }
|
|
122
|
+
.timeline-row .type { color: var(--txt3); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
123
|
+
.timeline-row .title { font-weight: 800; }
|
|
124
|
+
.timeline-row .detail { color: var(--txt2); font-size: 13px; margin-top: 3px; overflow-wrap: anywhere; }
|
|
125
|
+
.timeline-row .when { color: var(--txt3); font-size: 12px; white-space: nowrap; }
|
|
126
|
+
.check-list { display: grid; gap: 10px; }
|
|
127
|
+
.check { display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: start; padding: 13px 0; border-top: 1px solid rgba(255,255,255,0.05); }
|
|
128
|
+
.check:first-child { border-top: 0; padding-top: 0; }
|
|
129
|
+
.check .mark { width: 12px; height: 12px; border-radius: 50%; margin-top: 4px; background: var(--ok); box-shadow: 0 0 0 4px var(--okbg); }
|
|
130
|
+
.check.warn .mark { background: var(--warn); box-shadow: 0 0 0 4px var(--warnbg); }
|
|
131
|
+
.check.fail .mark { background: var(--bad); box-shadow: 0 0 0 4px var(--badbg); }
|
|
132
|
+
.check .name { font-weight: 800; }
|
|
133
|
+
.check .detail { color: var(--txt2); font-size: 13px; margin-top: 3px; line-height: 1.45; }
|
|
134
|
+
.port-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
|
135
|
+
@media (max-width: 880px) { .port-grid { grid-template-columns: 1fr; } }
|
|
136
|
+
.port-card { border: 1px solid rgba(255,255,255,0.07); border-radius: 12px; background: rgba(255,255,255,0.025); padding: 13px 14px; min-width: 0; }
|
|
137
|
+
.port-card .top { display: flex; align-items: center; gap: 8px; justify-content: space-between; }
|
|
138
|
+
.port-card .listen { margin-top: 8px; font-weight: 800; overflow-wrap: anywhere; }
|
|
139
|
+
.port-card .proc { color: var(--txt3); font-size: 12px; margin-top: 5px; overflow-wrap: anywhere; }
|
|
140
|
+
.action-runner { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
141
|
+
.action-runner select {
|
|
142
|
+
background: rgba(255,255,255,0.05); border: 1px solid var(--panel-br); border-radius: 10px;
|
|
143
|
+
padding: 8px 10px; color: var(--txt); font-family: inherit; font-size: 13px; min-width: 150px;
|
|
144
|
+
}
|
|
145
|
+
.action-output { margin-top: 12px; white-space: pre-wrap; overflow: auto; max-height: 260px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.07); background: rgba(0,0,0,0.22); padding: 12px; color: var(--txt2); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; }
|
|
146
|
+
.op-confirm { margin-top: 14px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
147
|
+
.op-confirm-input { flex: 0 0 200px; background: rgba(255,255,255,0.05); border: 1px solid var(--panel-br); border-radius: 10px; padding: 8px 12px; color: var(--txt); font-family: inherit; font-size: 14px; }
|
|
148
|
+
.op-confirm-input:focus { outline: none; border-color: var(--accent); }
|
|
149
|
+
.note { color: var(--txt3); font-size: 12px; margin-top: 10px; }
|
|
150
|
+
.row-actions { display: flex; gap: 8px; }
|
|
151
|
+
.field { display: flex; gap: 8px; margin-top: 12px; }
|
|
152
|
+
.field input { flex: 1; background: rgba(255,255,255,0.05); border: 1px solid var(--panel-br); border-radius: 10px; padding: 9px 12px; color: var(--txt); font-family: inherit; font-size: 14px; }
|
|
153
|
+
.field input:focus { outline: none; border-color: var(--accent); }
|
|
154
|
+
.empty { display: grid; place-items: center; gap: 6px; min-height: 132px; text-align: center; color: var(--txt3); border: 1px dashed rgba(255,255,255,0.12); border-radius: 14px; background: rgba(255,255,255,0.025); padding: 22px; }
|
|
155
|
+
.empty strong { color: var(--txt); font-size: 14px; }
|
|
156
|
+
.empty span { max-width: 540px; font-size: 13px; line-height: 1.5; }
|
|
157
|
+
.empty.compact { min-height: 74px; margin-top: 10px; padding: 14px; }
|
|
158
|
+
.form-grid { display: grid; grid-template-columns: minmax(180px, 0.7fr) minmax(280px, 1.4fr); gap: 14px; align-items: end; }
|
|
159
|
+
.field-block { display: flex; flex-direction: column; gap: 7px; }
|
|
160
|
+
.field-block.wide { grid-column: 1 / -1; }
|
|
161
|
+
.field-block label, .select-filter span { color: var(--txt3); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
162
|
+
.field-block input, .field-block textarea, .select-filter select {
|
|
163
|
+
width: 100%; background: rgba(255,255,255,0.05); border: 1px solid var(--panel-br); border-radius: 10px;
|
|
164
|
+
padding: 10px 12px; color: var(--txt); font-family: inherit; font-size: 14px;
|
|
165
|
+
}
|
|
166
|
+
.field-block textarea { resize: vertical; min-height: 92px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; }
|
|
167
|
+
.field-block input:focus, .field-block textarea:focus, .select-filter select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(91,140,255,0.12); }
|
|
168
|
+
.form-actions { grid-column: 1 / -1; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
169
|
+
.msg { color: var(--txt3); font-size: 13px; }
|
|
170
|
+
.msg.ok { color: var(--ok); }
|
|
171
|
+
.msg.bad { color: var(--bad); }
|
|
172
|
+
.table-actions { text-align: right; white-space: nowrap; }
|
|
173
|
+
.filters { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin: 16px 0; }
|
|
174
|
+
.segmented { display: inline-flex; padding: 4px; border: 1px solid var(--panel-br); background: rgba(255,255,255,0.035); border-radius: 12px; gap: 2px; }
|
|
175
|
+
.segmented button { appearance: none; border: 0; background: transparent; color: var(--txt2); border-radius: 9px; padding: 7px 12px; font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; }
|
|
176
|
+
.segmented button.active { background: rgba(91,140,255,0.18); color: var(--accent); box-shadow: inset 0 0 0 1px rgba(91,140,255,0.18); }
|
|
177
|
+
.select-filter { display: inline-flex; align-items: center; gap: 8px; }
|
|
178
|
+
.select-filter select { min-width: 150px; padding: 8px 10px; }
|
|
179
|
+
@media (max-width: 760px) {
|
|
180
|
+
.form-grid { grid-template-columns: 1fr; }
|
|
181
|
+
.filters { align-items: stretch; }
|
|
182
|
+
.segmented, .select-filter, .select-filter select { width: 100%; }
|
|
183
|
+
.segmented button { flex: 1; }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Honest action affordance: a copyable CLI command (static dashboard has no
|
|
187
|
+
backend — the buttons would POST nowhere, so we show the exact command). */
|
|
188
|
+
.cmd { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 10px; background: rgba(255,255,255,0.05); border: 1px solid var(--panel-br); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; color: var(--txt); white-space: nowrap; }
|
|
189
|
+
.cmd::before { content: '$'; color: var(--txt3); }
|
|
190
|
+
.cmd.accent { border-color: rgba(91,140,255,0.4); color: var(--accent); }
|
|
191
|
+
.cmds { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
192
|
+
|
|
193
|
+
/* Server dashboard components */
|
|
194
|
+
.svc { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
195
|
+
.svc .item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 10px; background: rgba(255,255,255,0.03); border: 1px solid var(--panel-br); font-size: 13px; font-weight: 500; }
|
|
196
|
+
.key { display: flex; align-items: center; gap: 12px; padding: 11px 0; border-top: 1px solid rgba(255,255,255,0.05); font-size: 14px; }
|
|
197
|
+
.key:first-of-type { border-top: 0; }
|
|
198
|
+
.key .fp { margin-left: auto; color: var(--txt3); font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
199
|
+
.chart-panel { overflow: hidden; }
|
|
200
|
+
.chart-panel.subtle { padding: 0; margin-bottom: 14px; background: transparent; border: 0; }
|
|
201
|
+
svg.chart { display: block; width: 100%; height: auto; min-height: 150px; }
|
|
202
|
+
svg.chart .chart-shell { fill: rgba(255,255,255,0.025); stroke: rgba(255,255,255,0.07); }
|
|
203
|
+
svg.chart .chart-grid { stroke: rgba(255,255,255,0.08); stroke-width: 1; }
|
|
204
|
+
svg.chart .chart-axis, svg.chart .chart-label { fill: var(--txt3); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
205
|
+
svg.chart .chart-value { fill: var(--txt); font-size: 12px; font-weight: 800; }
|
|
206
|
+
svg.chart .chart-empty-title { fill: var(--txt); font-size: 15px; font-weight: 800; }
|
|
207
|
+
svg.chart .chart-empty-sub { fill: var(--txt3); font-size: 12px; }
|
|
208
|
+
svg.chart .bar-mark.ok rect { fill: var(--ok); opacity: 0.85; }
|
|
209
|
+
svg.chart .bar-mark.warn rect { fill: var(--warn); opacity: 0.88; }
|
|
210
|
+
svg.chart .bar-mark.bad rect { fill: var(--bad); opacity: 0.9; }
|
|
211
|
+
svg.chart .spark-path { fill: none; stroke: var(--accent); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 0 8px rgba(91,140,255,0.38)); }
|
|
212
|
+
svg.chart circle.ok { fill: var(--ok); }
|
|
213
|
+
svg.chart circle.bad { fill: var(--bad); }
|
|
214
|
+
svg.chart.disabled { opacity: 0.86; }
|
|
215
|
+
|
|
216
|
+
/* ───────────── Health / vital signs (server metrics) ───────────── */
|
|
217
|
+
/* Overall status banner — at-a-glance "is the box OK?" verdict. */
|
|
218
|
+
.health-hero {
|
|
219
|
+
--hue: var(--ok); --huebg: var(--okbg);
|
|
220
|
+
display: flex; align-items: center; gap: 18px 28px; flex-wrap: wrap; margin-top: 8px;
|
|
221
|
+
background:
|
|
222
|
+
radial-gradient(700px 200px at 0% 0%, var(--huebg), transparent 70%),
|
|
223
|
+
var(--panel);
|
|
224
|
+
border: 1px solid var(--panel-br); border-radius: 18px; padding: 18px 22px; backdrop-filter: blur(6px);
|
|
225
|
+
}
|
|
226
|
+
.health-hero.warn { --hue: var(--warn); --huebg: var(--warnbg); }
|
|
227
|
+
.health-hero.bad { --hue: var(--bad); --huebg: var(--badbg); }
|
|
228
|
+
.health-hero .status { display: inline-flex; align-items: center; gap: 12px; font-weight: 800; font-size: 17px; letter-spacing: -0.01em; color: var(--hue); }
|
|
229
|
+
.health-hero .status .led {
|
|
230
|
+
width: 13px; height: 13px; border-radius: 50%; background: var(--hue);
|
|
231
|
+
box-shadow: 0 0 0 4px var(--huebg), 0 0 16px var(--hue); animation: hb-pulse 2.4s ease-in-out infinite;
|
|
232
|
+
}
|
|
233
|
+
.health-hero .status small { display: block; color: var(--txt3); font-size: 12px; font-weight: 500; margin-top: 2px; letter-spacing: 0; }
|
|
234
|
+
@keyframes hb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.5 } }
|
|
235
|
+
.health-hero .hmeta { margin-left: auto; display: flex; flex-wrap: wrap; gap: 22px 26px; color: var(--txt2); font-size: 13px; }
|
|
236
|
+
.health-hero .hmeta .item { display: flex; flex-direction: column; gap: 2px; }
|
|
237
|
+
.health-hero .hmeta .item span { color: var(--txt3); font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; font-weight: 700; }
|
|
238
|
+
.health-hero .hmeta .item b { color: var(--txt); font-weight: 600; font-size: 14px; }
|
|
239
|
+
|
|
240
|
+
/* Radial gauges — the centerpiece read for CPU / memory / disk. */
|
|
241
|
+
.gauges { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 16px; }
|
|
242
|
+
@media (max-width: 760px) { .gauges { grid-template-columns: 1fr; } }
|
|
243
|
+
.gauge {
|
|
244
|
+
--g: var(--ok); --gbg: var(--okbg);
|
|
245
|
+
background: var(--panel); border: 1px solid var(--panel-br); border-radius: 18px;
|
|
246
|
+
padding: 20px 20px 18px; backdrop-filter: blur(6px);
|
|
247
|
+
display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;
|
|
248
|
+
transition: border-color .15s, transform .15s;
|
|
249
|
+
}
|
|
250
|
+
.gauge:hover { border-color: color-mix(in srgb, var(--g) 45%, transparent); transform: translateY(-1px); }
|
|
251
|
+
.gauge.warn { --g: var(--warn); --gbg: var(--warnbg); }
|
|
252
|
+
.gauge.bad { --g: var(--bad); --gbg: var(--badbg); }
|
|
253
|
+
.gauge .ghead { display: flex; align-items: center; gap: 8px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--txt3); font-weight: 700; }
|
|
254
|
+
.gauge .ghead svg { width: 15px; height: 15px; color: var(--g); }
|
|
255
|
+
.gauge .ring {
|
|
256
|
+
position: relative; width: 172px; height: 172px; margin: 6px 0 2px; border-radius: 50%;
|
|
257
|
+
background: conic-gradient(var(--g) calc(var(--pct) * 1%), rgba(255,255,255,0.07) 0);
|
|
258
|
+
}
|
|
259
|
+
.gauge .ring::after {
|
|
260
|
+
content: ''; position: absolute; inset: 15px; border-radius: 50%;
|
|
261
|
+
background: linear-gradient(160deg, rgba(20,28,52,0.98), rgba(14,20,40,0.98));
|
|
262
|
+
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.035);
|
|
263
|
+
}
|
|
264
|
+
.gauge .center { position: absolute; inset: 0; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
|
265
|
+
.gauge .pct { font-size: 40px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; }
|
|
266
|
+
.gauge .pct sup { font-size: 17px; font-weight: 700; color: var(--txt2); margin-left: 1px; top: -0.7em; }
|
|
267
|
+
.gauge .punit { font-size: 11px; color: var(--txt3); font-weight: 600; margin-top: 7px; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
268
|
+
.gauge .gsub { color: var(--txt2); font-size: 14px; font-weight: 500; }
|
|
269
|
+
.gauge .gsub b { color: var(--txt); font-weight: 700; }
|
|
270
|
+
.gauge .gtag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 11px; border-radius: 999px; background: var(--gbg); color: var(--g); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
271
|
+
|
|
272
|
+
/* Vital-sign tiles — the secondary numbers (free RAM, uptime, etc.). */
|
|
273
|
+
.vitals { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
|
274
|
+
@media (max-width: 880px) { .vitals { grid-template-columns: repeat(2, 1fr); } }
|
|
275
|
+
.vital { background: var(--panel); border: 1px solid var(--panel-br); border-radius: 14px; padding: 14px 16px; }
|
|
276
|
+
.vital .vlabel { display: flex; align-items: center; gap: 7px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--txt3); font-weight: 700; }
|
|
277
|
+
.vital .vlabel svg { width: 13px; height: 13px; }
|
|
278
|
+
.vital .vval { font-size: 21px; font-weight: 700; margin-top: 7px; letter-spacing: -0.01em; }
|
|
279
|
+
.vital .vval small { font-size: 12.5px; color: var(--txt2); font-weight: 500; }
|
|
280
|
+
.vital .vnote { font-size: 12px; color: var(--txt3); margin-top: 3px; }
|
|
281
|
+
|
|
282
|
+
/* Tone-colored progress bars (shared with index health cards). */
|
|
283
|
+
.bar.ok > i { background: linear-gradient(90deg, var(--ok), #22c55e); }
|
|
284
|
+
.bar.warn > i { background: linear-gradient(90deg, var(--warn), #f59e0b); }
|
|
285
|
+
.bar.bad > i { background: linear-gradient(90deg, var(--bad), #ef4444); }
|
|
286
|
+
|
|
287
|
+
/* Load trend chart — sparkline with capacity guide + axis labels. */
|
|
288
|
+
.chart { position: relative; }
|
|
289
|
+
.chart .spark { height: 124px; align-items: flex-end; }
|
|
290
|
+
.chart .spark > i { background: linear-gradient(180deg, var(--accent), rgba(91,140,255,0.22)); transition: height .6s ease; }
|
|
291
|
+
.chart .spark > i.hot { background: linear-gradient(180deg, var(--warn), rgba(251,191,36,0.22)); }
|
|
292
|
+
.chart .spark > i.peak { background: linear-gradient(180deg, var(--bad), rgba(248,113,113,0.25)); }
|
|
293
|
+
.chart .xaxis { display: flex; justify-content: space-between; margin-top: 9px; color: var(--txt3); font-size: 11px; }
|
|
294
|
+
</style>
|