@stacksjs/ts-cloud 0.5.35 → 0.6.1
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/aws/cloudwatch.d.ts +2 -0
- package/dist/aws/xray.d.ts +45 -0
- package/dist/bin/cli.js +718 -701
- package/dist/deploy/dashboard-database.d.ts +25 -0
- package/dist/deploy/dashboard-operations.d.ts +7 -0
- package/dist/deploy/firewall-config-editor.d.ts +15 -0
- package/dist/deploy/firewall-config-editor.test.d.ts +1 -0
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/management-dashboard.d.ts +26 -3
- package/dist/deploy/serverless-operations.d.ts +150 -0
- package/dist/deploy/serverless-operations.test.d.ts +1 -0
- package/dist/deploy/site-config-editor.d.ts +6 -0
- package/dist/deploy/ssh-config-editor.d.ts +1 -0
- package/dist/deploy/terminal-session.d.ts +23 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1003 -33
- package/dist/ui/index.html +4 -4
- package/dist/ui/server/actions.html +34 -5
- package/dist/ui/server/activity.html +1 -1
- package/dist/ui/server/backups.html +4 -4
- package/dist/ui/server/database.html +27 -5
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +1 -1
- package/dist/ui/server/firewall.html +1233 -0
- package/dist/ui/server/logs.html +4 -4
- package/dist/ui/server/metrics.html +1 -1
- package/dist/ui/server/security.html +1 -1
- package/dist/ui/server/services.html +4 -4
- package/dist/ui/server/sites.html +24 -5
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/terminal.html +1191 -0
- package/dist/ui/server/workers.html +4 -4
- package/dist/ui/serverless/alarms.html +1236 -0
- package/dist/ui/serverless/assets.html +53 -1
- package/dist/ui/serverless/cost.html +1 -1
- package/dist/ui/serverless/data.html +58 -1
- package/dist/ui/serverless/deployments.html +53 -1
- package/dist/ui/serverless/firewall.html +1 -1
- package/dist/ui/serverless/functions.html +63 -1
- package/dist/ui/serverless/logs.html +4 -4
- package/dist/ui/serverless/metrics.html +1 -1
- package/dist/ui/serverless/queues.html +94 -1
- package/dist/ui/serverless/scheduler.html +55 -1
- package/dist/ui/serverless/secrets.html +99 -1
- package/dist/ui/serverless/traces.html +1177 -0
- package/dist/ui/serverless.html +87 -1
- package/dist/ui-src/pages/index.stx +1 -1
- package/dist/ui-src/pages/partials/head.stx +47 -35
- package/dist/ui-src/pages/partials/nav.stx +4 -3
- package/dist/ui-src/pages/server/actions.stx +34 -1
- package/dist/ui-src/pages/server/backups.stx +1 -1
- package/dist/ui-src/pages/server/database.stx +32 -9
- package/dist/ui-src/pages/server/deployments.stx +2 -2
- package/dist/ui-src/pages/server/firewall.stx +134 -0
- package/dist/ui-src/pages/server/logs.stx +2 -2
- package/dist/ui-src/pages/server/metrics.stx +5 -5
- package/dist/ui-src/pages/server/security.stx +1 -1
- package/dist/ui-src/pages/server/services.stx +1 -1
- package/dist/ui-src/pages/server/sites.stx +24 -3
- package/dist/ui-src/pages/server/ssh-keys.stx +2 -2
- package/dist/ui-src/pages/server/terminal.stx +65 -0
- package/dist/ui-src/pages/server/workers.stx +1 -1
- package/dist/ui-src/pages/serverless/alarms.stx +153 -0
- package/dist/ui-src/pages/serverless/assets.stx +29 -2
- package/dist/ui-src/pages/serverless/cost.stx +4 -4
- package/dist/ui-src/pages/serverless/data.stx +56 -3
- package/dist/ui-src/pages/serverless/deployments.stx +30 -6
- package/dist/ui-src/pages/serverless/firewall.stx +1 -1
- package/dist/ui-src/pages/serverless/functions.stx +62 -4
- package/dist/ui-src/pages/serverless/logs.stx +2 -2
- package/dist/ui-src/pages/serverless/metrics.stx +5 -1
- package/dist/ui-src/pages/serverless/queues.stx +90 -26
- package/dist/ui-src/pages/serverless/scheduler.stx +31 -2
- package/dist/ui-src/pages/serverless/secrets.stx +94 -14
- package/dist/ui-src/pages/serverless/traces.stx +88 -0
- package/dist/ui-src/pages/serverless.stx +87 -24
- package/package.json +3 -3
|
@@ -102,7 +102,7 @@ const initialData = __d ?? null
|
|
|
102
102
|
<script client>
|
|
103
103
|
// Live at-a-glance metrics: seeded from build-time data (stx server→client
|
|
104
104
|
// bridge) so the cards render real numbers on first paint, then polled from
|
|
105
|
-
// /api/dashboard-data
|
|
105
|
+
// /api/dashboard-data - bind cards, health pill, indicator, and service
|
|
106
106
|
// statuses reactively (no manual DOM).
|
|
107
107
|
const data = state(initialData)
|
|
108
108
|
const liveData = derived(() => { const d = data(); return !!d && d._serverReachable && !d.metricsUnavailable })
|
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
<meta charset="UTF-8">
|
|
2
2
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3
3
|
<title>{{ title ?? 'ts-cloud' }}</title>
|
|
4
|
-
<!-- Inline SVG favicon (
|
|
5
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%
|
|
4
|
+
<!-- Inline SVG favicon (single-accent tile) - declaring it stops the browser requesting /favicon.ico. -->
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='3' y='3' width='26' height='26' rx='8' fill='%235a8be0'/%3E%3C/svg%3E">
|
|
6
6
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
7
7
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
8
8
|
<style>
|
|
9
9
|
:root {
|
|
10
|
-
--bg: #
|
|
11
|
-
--txt: #
|
|
12
|
-
--accent: #
|
|
13
|
-
--
|
|
10
|
+
--bg: #0a0e15; --bg2: #0b0f18; --panel: rgba(255,255,255,0.03); --panel-br: rgba(255,255,255,0.072);
|
|
11
|
+
--txt: #e9edf5; --txt2: #94a1b8; --txt3: #5a6577;
|
|
12
|
+
--accent: #5a8be0; --accent-dim: rgba(90,139,224,0.14); --accent-ink: #061019;
|
|
13
|
+
--ok: #35d19b; --okbg: rgba(53,209,155,0.11);
|
|
14
|
+
--bad: #f0736e; --badbg: rgba(240,115,110,0.11); --warn: #edb949; --warnbg: rgba(237,185,73,0.11);
|
|
15
|
+
--mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
|
|
14
16
|
}
|
|
15
17
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
16
18
|
html { background: var(--bg); min-height: 100%; }
|
|
17
19
|
body {
|
|
18
20
|
font-family: 'Inter', system-ui, sans-serif; color: var(--txt); min-height: 100vh;
|
|
19
|
-
|
|
21
|
+
/* One calm accent wash, no AI blue-purple mesh. */
|
|
22
|
+
background:
|
|
23
|
+
radial-gradient(900px 460px at 88% -8%, rgba(90,139,224,0.06), transparent 62%),
|
|
24
|
+
linear-gradient(175deg, var(--bg), var(--bg2));
|
|
20
25
|
-webkit-font-smoothing: antialiased;
|
|
26
|
+
font-feature-settings: 'tnum' 1, 'ss01' 1;
|
|
21
27
|
}
|
|
28
|
+
/* Tabular monospaced numerals wherever a value is read at a glance (density 7). */
|
|
29
|
+
.stat, .gauge .pct, .vital .vval, .health-hero .hmeta .item b, .kv > b, td.mono { font-variant-numeric: tabular-nums; }
|
|
30
|
+
.stat, .gauge .pct { font-family: var(--mono); letter-spacing: -0.03em; }
|
|
22
31
|
a { color: inherit; text-decoration: none; }
|
|
23
32
|
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 24px 64px; }
|
|
24
33
|
|
|
25
34
|
nav {
|
|
26
|
-
display: flex; align-items: center; gap: 16px; padding:
|
|
27
|
-
background: rgba(
|
|
35
|
+
display: flex; align-items: center; gap: 16px; padding: 16px 24px; position: sticky; top: 0; z-index: 10;
|
|
36
|
+
background: rgba(10,14,21,0.72); backdrop-filter: blur(14px); border-bottom: 1px solid var(--panel-br);
|
|
28
37
|
}
|
|
29
|
-
.brand { display: flex; align-items: center; gap: 10px; font-weight:
|
|
30
|
-
.brand .dot { width:
|
|
38
|
+
.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 17px; letter-spacing: -0.02em; }
|
|
39
|
+
.brand .dot { width: 20px; height: 20px; border-radius: 6px; background: var(--accent); box-shadow: inset 0 1px 0 rgba(255,255,255,0.35); }
|
|
31
40
|
.tabs { display: flex; gap: 4px; margin-left: 18px; }
|
|
32
41
|
.tabs a { padding: 6px 12px; border-radius: 8px; font-size: 13px; font-weight: 600; color: var(--txt2); }
|
|
33
|
-
.tabs a.active { background: rgba(
|
|
42
|
+
.tabs a.active { background: rgba(90,139,224,0.14); color: var(--accent); }
|
|
34
43
|
.nav-meta { margin-left: auto; display: flex; align-items: center; gap: 14px; color: var(--txt2); font-size: 13px; }
|
|
35
44
|
.env { padding: 4px 10px; border-radius: 999px; background: var(--okbg); color: var(--ok); font-weight: 600; font-size: 12px; }
|
|
36
45
|
.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; }
|
|
@@ -58,12 +67,12 @@
|
|
|
58
67
|
@media (max-width: 880px) { .cards, .cards-3, .cards-2 { grid-template-columns: repeat(2, 1fr); } }
|
|
59
68
|
.panel { background: var(--panel); border: 1px solid var(--panel-br); border-radius: 16px; padding: 18px; backdrop-filter: blur(6px); }
|
|
60
69
|
a.panel { display: block; transition: border-color .15s, transform .15s; }
|
|
61
|
-
a.panel:hover { border-color: rgba(
|
|
70
|
+
a.panel:hover { border-color: rgba(90,139,224,0.5); transform: translateY(-1px); }
|
|
62
71
|
.panel h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--txt3); font-weight: 700; }
|
|
63
72
|
.stat { font-size: 28px; font-weight: 800; margin-top: 8px; letter-spacing: -0.02em; }
|
|
64
73
|
.stat small { font-size: 14px; color: var(--txt2); font-weight: 500; }
|
|
65
74
|
.bar { height: 7px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 12px; overflow: hidden; }
|
|
66
|
-
.bar > i { display: block; height: 100%; border-radius: 999px; background:
|
|
75
|
+
.bar > i { display: block; height: 100%; border-radius: 999px; background: var(--accent); }
|
|
67
76
|
|
|
68
77
|
.section { margin-top: 30px; }
|
|
69
78
|
.section > h2 { font-size: 15px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
|
@@ -83,7 +92,7 @@
|
|
|
83
92
|
.pill.ok { background: var(--okbg); color: var(--ok); }
|
|
84
93
|
.pill.bad { background: var(--badbg); color: var(--bad); }
|
|
85
94
|
.pill.warn { background: var(--warnbg); color: var(--warn); }
|
|
86
|
-
.tag { padding: 2px 8px; border-radius: 6px; background: rgba(
|
|
95
|
+
.tag { padding: 2px 8px; border-radius: 6px; background: rgba(90,139,224,0.12); color: var(--accent); font-size: 12px; font-weight: 600; }
|
|
87
96
|
|
|
88
97
|
.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; }
|
|
89
98
|
.kv:first-of-type { border-top: 0; }
|
|
@@ -93,10 +102,13 @@
|
|
|
93
102
|
.deploy:first-of-type { border-top: 0; }
|
|
94
103
|
.deploy .when { margin-left: auto; color: var(--txt3); font-size: 12px; }
|
|
95
104
|
|
|
96
|
-
.btn { background:
|
|
97
|
-
.btn:hover { filter: brightness(1.
|
|
98
|
-
.btn
|
|
99
|
-
.btn.
|
|
105
|
+
.btn { background: var(--accent); color: var(--accent-ink); border: 0; border-radius: 10px; padding: 9px 16px; font-weight: 700; font-size: 13.5px; cursor: pointer; font-family: inherit; white-space: nowrap; transition: filter .12s, transform .12s; }
|
|
106
|
+
.btn:hover { filter: brightness(1.06); }
|
|
107
|
+
.btn:active { transform: translateY(1px); }
|
|
108
|
+
.btn.ghost { background: rgba(255,255,255,0.045); border: 1px solid var(--panel-br); color: var(--txt); }
|
|
109
|
+
.btn.ghost:hover { background: rgba(255,255,255,0.075); filter: none; }
|
|
110
|
+
.btn.danger { background: transparent; border: 1px solid var(--badbg); color: var(--bad); }
|
|
111
|
+
.btn.danger:hover { background: var(--badbg); filter: none; }
|
|
100
112
|
.btn.sm { padding: 6px 12px; font-size: 12px; }
|
|
101
113
|
|
|
102
114
|
.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; }
|
|
@@ -109,7 +121,7 @@
|
|
|
109
121
|
|
|
110
122
|
/* Drill-down extras */
|
|
111
123
|
.spark { display: flex; align-items: flex-end; gap: 3px; height: 48px; margin-top: 12px; }
|
|
112
|
-
.spark > i { flex: 1; background: linear-gradient(180deg, var(--accent), rgba(
|
|
124
|
+
.spark > i { flex: 1; background: linear-gradient(180deg, var(--accent), rgba(90,139,224,0.25)); border-radius: 3px 3px 0 0; min-height: 2px; }
|
|
113
125
|
.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; }
|
|
114
126
|
.logs .ts { color: var(--txt3); }
|
|
115
127
|
.logs .err { color: var(--bad); }
|
|
@@ -166,7 +178,7 @@
|
|
|
166
178
|
padding: 10px 12px; color: var(--txt); font-family: inherit; font-size: 14px;
|
|
167
179
|
}
|
|
168
180
|
.field-block textarea { resize: vertical; min-height: 92px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; }
|
|
169
|
-
.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(
|
|
181
|
+
.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(90,139,224,0.12); }
|
|
170
182
|
.form-actions { grid-column: 1 / -1; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
171
183
|
.msg { color: var(--txt3); font-size: 13px; }
|
|
172
184
|
.msg.ok { color: var(--ok); }
|
|
@@ -175,7 +187,7 @@
|
|
|
175
187
|
.filters { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin: 16px 0; }
|
|
176
188
|
.segmented { display: inline-flex; padding: 4px; border: 1px solid var(--panel-br); background: rgba(255,255,255,0.035); border-radius: 12px; gap: 2px; }
|
|
177
189
|
.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; }
|
|
178
|
-
.segmented button.active { background: rgba(
|
|
190
|
+
.segmented button.active { background: rgba(90,139,224,0.18); color: var(--accent); box-shadow: inset 0 0 0 1px rgba(90,139,224,0.18); }
|
|
179
191
|
.select-filter { display: inline-flex; align-items: center; gap: 8px; }
|
|
180
192
|
.select-filter select { min-width: 150px; padding: 8px 10px; }
|
|
181
193
|
@media (max-width: 760px) {
|
|
@@ -186,10 +198,10 @@
|
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
/* Honest action affordance: a copyable CLI command (static dashboard has no
|
|
189
|
-
backend
|
|
201
|
+
backend - the buttons would POST nowhere, so we show the exact command). */
|
|
190
202
|
.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; }
|
|
191
203
|
.cmd::before { content: '$'; color: var(--txt3); }
|
|
192
|
-
.cmd.accent { border-color: rgba(
|
|
204
|
+
.cmd.accent { border-color: rgba(90,139,224,0.4); color: var(--accent); }
|
|
193
205
|
.cmds { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
194
206
|
|
|
195
207
|
/* Server dashboard components */
|
|
@@ -210,13 +222,13 @@
|
|
|
210
222
|
svg.chart .bar-mark.ok rect { fill: var(--ok); opacity: 0.85; }
|
|
211
223
|
svg.chart .bar-mark.warn rect { fill: var(--warn); opacity: 0.88; }
|
|
212
224
|
svg.chart .bar-mark.bad rect { fill: var(--bad); opacity: 0.9; }
|
|
213
|
-
svg.chart .spark-path { fill: none; stroke: var(--accent); stroke-width:
|
|
225
|
+
svg.chart .spark-path { fill: none; stroke: var(--accent); stroke-width: 2.25; stroke-linecap: round; stroke-linejoin: round; }
|
|
214
226
|
svg.chart circle.ok { fill: var(--ok); }
|
|
215
227
|
svg.chart circle.bad { fill: var(--bad); }
|
|
216
228
|
svg.chart.disabled { opacity: 0.86; }
|
|
217
229
|
|
|
218
230
|
/* ───────────── Health / vital signs (server metrics) ───────────── */
|
|
219
|
-
/* Overall status banner
|
|
231
|
+
/* Overall status banner - at-a-glance "is the box OK?" verdict. */
|
|
220
232
|
.health-hero {
|
|
221
233
|
--hue: var(--ok); --huebg: var(--okbg);
|
|
222
234
|
display: flex; align-items: center; gap: 18px 28px; flex-wrap: wrap; margin-top: 8px;
|
|
@@ -230,7 +242,7 @@
|
|
|
230
242
|
.health-hero .status { display: inline-flex; align-items: center; gap: 12px; font-weight: 800; font-size: 17px; letter-spacing: -0.01em; color: var(--hue); }
|
|
231
243
|
.health-hero .status .led {
|
|
232
244
|
width: 13px; height: 13px; border-radius: 50%; background: var(--hue);
|
|
233
|
-
box-shadow: 0 0 0 4px var(--huebg)
|
|
245
|
+
box-shadow: 0 0 0 4px var(--huebg); animation: hb-pulse 2.4s ease-in-out infinite;
|
|
234
246
|
}
|
|
235
247
|
.health-hero .status small { display: block; color: var(--txt3); font-size: 12px; font-weight: 500; margin-top: 2px; letter-spacing: 0; }
|
|
236
248
|
@keyframes hb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.5 } }
|
|
@@ -239,7 +251,7 @@
|
|
|
239
251
|
.health-hero .hmeta .item span { color: var(--txt3); font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; font-weight: 700; }
|
|
240
252
|
.health-hero .hmeta .item b { color: var(--txt); font-weight: 600; font-size: 14px; }
|
|
241
253
|
|
|
242
|
-
/* Radial gauges
|
|
254
|
+
/* Radial gauges - the centerpiece read for CPU / memory / disk. */
|
|
243
255
|
.gauges { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 16px; }
|
|
244
256
|
@media (max-width: 760px) { .gauges { grid-template-columns: 1fr; } }
|
|
245
257
|
.gauge {
|
|
@@ -260,7 +272,7 @@
|
|
|
260
272
|
}
|
|
261
273
|
.gauge .ring::after {
|
|
262
274
|
content: ''; position: absolute; inset: 15px; border-radius: 50%;
|
|
263
|
-
background: linear-gradient(160deg,
|
|
275
|
+
background: linear-gradient(160deg, #10151f, #0c1017);
|
|
264
276
|
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.035);
|
|
265
277
|
}
|
|
266
278
|
.gauge .center { position: absolute; inset: 0; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
|
@@ -271,7 +283,7 @@
|
|
|
271
283
|
.gauge .gsub b { color: var(--txt); font-weight: 700; }
|
|
272
284
|
.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; }
|
|
273
285
|
|
|
274
|
-
/* Vital-sign tiles
|
|
286
|
+
/* Vital-sign tiles - the secondary numbers (free RAM, uptime, etc.). */
|
|
275
287
|
.vitals { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
|
276
288
|
@media (max-width: 880px) { .vitals { grid-template-columns: repeat(2, 1fr); } }
|
|
277
289
|
.vital { background: var(--panel); border: 1px solid var(--panel-br); border-radius: 14px; padding: 14px 16px; }
|
|
@@ -282,14 +294,14 @@
|
|
|
282
294
|
.vital .vnote { font-size: 12px; color: var(--txt3); margin-top: 3px; }
|
|
283
295
|
|
|
284
296
|
/* Tone-colored progress bars (shared with index health cards). */
|
|
285
|
-
.bar.ok > i { background:
|
|
286
|
-
.bar.warn > i { background:
|
|
287
|
-
.bar.bad > i { background:
|
|
297
|
+
.bar.ok > i { background: var(--ok); }
|
|
298
|
+
.bar.warn > i { background: var(--warn); }
|
|
299
|
+
.bar.bad > i { background: var(--bad); }
|
|
288
300
|
|
|
289
|
-
/* Load trend chart
|
|
301
|
+
/* Load trend chart - sparkline with capacity guide + axis labels. */
|
|
290
302
|
.chart { position: relative; }
|
|
291
303
|
.chart .spark { height: 124px; align-items: flex-end; }
|
|
292
|
-
.chart .spark > i { background: linear-gradient(180deg, var(--accent), rgba(
|
|
304
|
+
.chart .spark > i { background: linear-gradient(180deg, var(--accent), rgba(90,139,224,0.22)); transition: height .6s ease; }
|
|
293
305
|
.chart .spark > i.hot { background: linear-gradient(180deg, var(--warn), rgba(251,191,36,0.22)); }
|
|
294
306
|
.chart .spark > i.peak { background: linear-gradient(180deg, var(--bad), rgba(248,113,113,0.25)); }
|
|
295
307
|
.chart .xaxis { display: flex; justify-content: space-between; margin-top: 9px; color: var(--txt3); font-size: 11px; }
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
<script server>
|
|
2
2
|
// Mode-aware, fully server-rendered nav. The deployment mode + environment list
|
|
3
3
|
// are injected with the dashboard data; the environment switcher is plain links
|
|
4
|
-
// (`?env=<name>`) the server handles
|
|
4
|
+
// (`?env=<name>`) the server handles - no client JavaScript needed.
|
|
5
5
|
let __nav = null
|
|
6
6
|
try { __nav = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __nav = null }
|
|
7
|
+
// Server and serverless are mutually exclusive; the dashboard shows one or the other.
|
|
7
8
|
const navMode = __nav?.mode ?? 'server'
|
|
8
|
-
const showServer = navMode === 'server'
|
|
9
|
-
const showServerless = navMode === 'serverless'
|
|
9
|
+
const showServer = navMode === 'server'
|
|
10
|
+
const showServerless = navMode === 'serverless'
|
|
10
11
|
const navEnvs = __nav?.environments ?? []
|
|
11
12
|
const navEnv = __nav?.environment ?? ''
|
|
12
13
|
</script>
|
|
@@ -32,6 +32,24 @@ async function runOp() {
|
|
|
32
32
|
pending.set(null); typed.set('')
|
|
33
33
|
await execAction(p.operation, p.confirm)
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
// Free-form server command runner (Forge-style). Runs on the app box over the
|
|
37
|
+
// active driver; confirmation is required server-side (confirm: 'run').
|
|
38
|
+
const serverCmd = state('')
|
|
39
|
+
const cmdOut = state('')
|
|
40
|
+
const cmdShown = state(false)
|
|
41
|
+
const cmdBusy = state(false)
|
|
42
|
+
async function runServerCmd() {
|
|
43
|
+
const c = serverCmd().trim()
|
|
44
|
+
if (!c) return
|
|
45
|
+
cmdBusy.set(true); cmdShown.set(true); cmdOut.set('Running ' + c + '...')
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch('/api/server/command', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ command: c, confirm: 'run' }) })
|
|
48
|
+
const b = await res.json()
|
|
49
|
+
cmdOut.set((b.ok ? 'OK ' : 'FAILED ') + (b.command || c) + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
|
|
50
|
+
} catch (e) { cmdOut.set('FAILED ' + c + '\n\n' + ((e && e.message) || e)) }
|
|
51
|
+
finally { cmdBusy.set(false) }
|
|
52
|
+
}
|
|
35
53
|
</script>
|
|
36
54
|
<!DOCTYPE html>
|
|
37
55
|
<html lang="en" data-theme="dark">
|
|
@@ -40,7 +58,10 @@ async function runOp() {
|
|
|
40
58
|
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
41
59
|
<div class="wrap">
|
|
42
60
|
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Operations</div>
|
|
43
|
-
<header class="page"
|
|
61
|
+
<header class="page">
|
|
62
|
+
<div><h1>Operations</h1><p>Run allowlisted local cloud commands and inspect their output.</p></div>
|
|
63
|
+
<div class="header-actions"><a class="btn ghost sm" href="/server/terminal">Open terminal →</a></div>
|
|
64
|
+
</header>
|
|
44
65
|
|
|
45
66
|
<div class="section">
|
|
46
67
|
<h2>Run command</h2>
|
|
@@ -55,6 +76,18 @@ async function runOp() {
|
|
|
55
76
|
<p class="note">Mutating commands require typed confirmation. Output is clamped so the browser stays responsive.</p>
|
|
56
77
|
</div>
|
|
57
78
|
</div>
|
|
79
|
+
|
|
80
|
+
<div class="section">
|
|
81
|
+
<h2>Run a command on the server</h2>
|
|
82
|
+
<div class="panel">
|
|
83
|
+
<div class="field">
|
|
84
|
+
<input :value="serverCmd()" @input="serverCmd.set($event.target.value)" @keydown.enter="runServerCmd()" placeholder="e.g. systemctl status rpx-gateway, or df -h" autocomplete="off">
|
|
85
|
+
<button class="btn" type="button" :disabled="cmdBusy()" @click="runServerCmd()">Run</button>
|
|
86
|
+
</div>
|
|
87
|
+
<pre class="action-output" @show="cmdShown()">{{ cmdOut() }}</pre>
|
|
88
|
+
<p class="note">Runs on the app box over the active driver (SSH/SSM). Use it for one-off recipes, service inspection, or installing software. Prefer declarative config for anything you want reproduced on the next deploy.</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
58
91
|
</div>
|
|
59
92
|
</body>
|
|
60
93
|
</html>
|
|
@@ -35,7 +35,7 @@ async function runOp() {
|
|
|
35
35
|
</script>
|
|
36
36
|
<!DOCTYPE html>
|
|
37
37
|
<html lang="en" data-theme="dark">
|
|
38
|
-
<head>@include('../partials/head', { title: 'ts-cloud
|
|
38
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Backups' })</head>
|
|
39
39
|
<body>
|
|
40
40
|
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
41
41
|
<div class="wrap">
|
|
@@ -4,7 +4,7 @@ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d =
|
|
|
4
4
|
const server = __d?.server ?? { name: 'acme-production-app' }
|
|
5
5
|
</script>
|
|
6
6
|
<script client>
|
|
7
|
-
// Reactive database management
|
|
7
|
+
// Reactive database management - create databases/users, live lists.
|
|
8
8
|
const engine = state('mysql')
|
|
9
9
|
const databases = state([])
|
|
10
10
|
const users = state([])
|
|
@@ -42,11 +42,24 @@ async function createUser() {
|
|
|
42
42
|
show((b.ok ? 'OK created user ' + (b.username || uName()) : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
|
|
43
43
|
if (b.ok) { uName.set(''); uPass.set(''); load() }
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
// Per-database backups: a gzipped dump written on the box, plus a live list.
|
|
47
|
+
const backups = state([])
|
|
48
|
+
async function loadBackups() {
|
|
49
|
+
try { const res = await fetch('/api/databases/backups'); const d = await res.json(); if (d.ok) backups.set(d.backups || []) } catch {}
|
|
50
|
+
}
|
|
51
|
+
async function backupDb(name) {
|
|
52
|
+
show('Backing up ' + name + '...')
|
|
53
|
+
const res = await fetch('/api/databases/backup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ database: name, confirm: name }) })
|
|
54
|
+
const b = await res.json()
|
|
55
|
+
show((b.ok ? 'OK backed up ' + (b.database || name) : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
|
|
56
|
+
if (b.ok) loadBackups()
|
|
57
|
+
}
|
|
58
|
+
onMount(() => { load(); loadBackups() })
|
|
46
59
|
</script>
|
|
47
60
|
<!DOCTYPE html>
|
|
48
61
|
<html lang="en" data-theme="dark">
|
|
49
|
-
<head>@include('../partials/head', { title: 'ts-cloud
|
|
62
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Databases' })</head>
|
|
50
63
|
<body>
|
|
51
64
|
@include('../partials/nav', { active: 'database', name: server.name, env: 'production' })
|
|
52
65
|
<div class="wrap">
|
|
@@ -95,11 +108,11 @@ onMount(() => { load() })
|
|
|
95
108
|
<div>
|
|
96
109
|
<h2>Databases</h2>
|
|
97
110
|
<div class="panel">
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
</
|
|
111
|
+
<template :for="n in databases()">
|
|
112
|
+
<div class="kv"><span class="mono">{{ n }}</span> <button class="btn ghost sm" type="button" @click="backupDb(n)">Back up</button></div>
|
|
113
|
+
</template>
|
|
114
|
+
<span class="item" @show="databases().length === 0 && dbError() !== ''">{{ dbError() }}</span>
|
|
115
|
+
<span class="item" @show="databases().length === 0 && dbError() === ''">no databases</span>
|
|
103
116
|
</div>
|
|
104
117
|
</div>
|
|
105
118
|
<div>
|
|
@@ -113,7 +126,17 @@ onMount(() => { load() })
|
|
|
113
126
|
</div>
|
|
114
127
|
</div>
|
|
115
128
|
</div>
|
|
116
|
-
<
|
|
129
|
+
<div class="section">
|
|
130
|
+
<h2>Backups</h2>
|
|
131
|
+
<div class="panel">
|
|
132
|
+
<template :for="b in backups()">
|
|
133
|
+
<div class="kv"><span class="mono">{{ b.database }}</span> <b class="mono" style="font-size:11px;color:var(--txt3)">{{ b.file }}</b></div>
|
|
134
|
+
</template>
|
|
135
|
+
<div class="compact empty" @show="backups().length === 0"><strong>No database dumps yet</strong><span>Use Back up on a database above to write a gzipped dump to the box.</span></div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<p class="note">Lists are read live from the engine via the app server. Create operations run as root over the pantry socket; apps connect with the credentials you set here. Per-database dumps are written to <span class="mono">/var/backups/ts-cloud/databases</span>.</p>
|
|
117
140
|
</div>
|
|
118
141
|
</body>
|
|
119
142
|
</html>
|
|
@@ -22,7 +22,7 @@ async function refresh() {
|
|
|
22
22
|
}
|
|
23
23
|
onMount(() => { refresh() })
|
|
24
24
|
|
|
25
|
-
// Reactive operation runner (rollback)
|
|
25
|
+
// Reactive operation runner (rollback) - typed-confirm bar + output.
|
|
26
26
|
const opOutput = state('')
|
|
27
27
|
const opShown = state(false)
|
|
28
28
|
const pending = state(null)
|
|
@@ -47,7 +47,7 @@ async function runOp() {
|
|
|
47
47
|
</script>
|
|
48
48
|
<!DOCTYPE html>
|
|
49
49
|
<html lang="en" data-theme="dark">
|
|
50
|
-
<head>@include('../partials/head', { title: 'ts-cloud
|
|
50
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Deployments' })</head>
|
|
51
51
|
<body>
|
|
52
52
|
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
53
53
|
<div class="wrap">
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
let __d = null
|
|
3
|
+
try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
|
|
4
|
+
const server = __d?.server ?? { name: 'stacks-production-app' }
|
|
5
|
+
const initialAlwaysOpen = __d?.firewall?.alwaysOpen ?? [22, 80, 443]
|
|
6
|
+
const initialPorts = __d?.firewall?.ports ?? [8080, 6379]
|
|
7
|
+
</script>
|
|
8
|
+
<script client>
|
|
9
|
+
// Reactive host firewall (UFW) management - list + allow + remove, via signals +
|
|
10
|
+
// directives. Seeded from build-time data (stx server→client bridge); load()
|
|
11
|
+
// reconciles against the live cloud config, and each mutation applies UFW live.
|
|
12
|
+
const alwaysOpen = state(initialAlwaysOpen)
|
|
13
|
+
const ports = state(initialPorts)
|
|
14
|
+
const newPort = state('')
|
|
15
|
+
const message = state('')
|
|
16
|
+
const messageKind = state('')
|
|
17
|
+
const reserved = [22, 80, 443]
|
|
18
|
+
function setMessage(text, kind) { message.set(text); messageKind.set(kind || '') }
|
|
19
|
+
function applyLine(apply) {
|
|
20
|
+
if (!apply) return ''
|
|
21
|
+
if (apply.ok === false) return ' Live UFW apply failed: ' + (apply.error || 'unknown error') + '.'
|
|
22
|
+
return ' Applied to UFW live on the box.'
|
|
23
|
+
}
|
|
24
|
+
async function load() {
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch('/api/firewall')
|
|
27
|
+
if (!res.ok) throw new Error('Could not read firewall config.')
|
|
28
|
+
const data = await res.json()
|
|
29
|
+
ports.set(data.ports || [])
|
|
30
|
+
alwaysOpen.set(data.alwaysOpen || reserved)
|
|
31
|
+
} catch (e) { setMessage((e && e.message) || 'Could not read firewall config.', 'bad') }
|
|
32
|
+
}
|
|
33
|
+
async function allowPort() {
|
|
34
|
+
const port = Number(newPort())
|
|
35
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) { setMessage('Port must be a whole number between 1 and 65535.', 'bad'); return }
|
|
36
|
+
if (reserved.includes(port)) { setMessage('Ports 22, 80, and 443 are always open, so there is nothing to add.', 'bad'); return }
|
|
37
|
+
setMessage('Allowing port ' + port + '...', '')
|
|
38
|
+
const res = await fetch('/api/firewall', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ port }) })
|
|
39
|
+
const data = await res.json().catch(() => ({}))
|
|
40
|
+
if (!res.ok || data.ok === false) { setMessage(data.error || 'Could not allow port.', 'bad'); return }
|
|
41
|
+
ports.set(data.ports || []); newPort.set('')
|
|
42
|
+
setMessage('Allowed port ' + port + ' in cloud config.' + applyLine(data.apply), 'ok')
|
|
43
|
+
}
|
|
44
|
+
async function removePort(port) {
|
|
45
|
+
setMessage('Removing port ' + port + '...', '')
|
|
46
|
+
const res = await fetch('/api/firewall', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ port }) })
|
|
47
|
+
const data = await res.json().catch(() => ({}))
|
|
48
|
+
if (!res.ok || data.ok === false) { setMessage(data.error || 'Could not remove port.', 'bad'); return }
|
|
49
|
+
ports.set(data.ports || [])
|
|
50
|
+
setMessage('Removed port ' + port + ' from cloud config.' + applyLine(data.apply), 'ok')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Remove runs through a typed-confirm bar: type the port number to confirm.
|
|
54
|
+
const pending = state(null); const typed = state('')
|
|
55
|
+
const confirmTok = derived(() => { const p = pending(); return p ? String(p) : '' })
|
|
56
|
+
const canRun = derived(() => { const p = pending(); return p !== null && typed() === String(p) })
|
|
57
|
+
function askRemove(port) { pending.set(port); typed.set('') }
|
|
58
|
+
function cancelRemove() { pending.set(null); typed.set('') }
|
|
59
|
+
async function runRemove() {
|
|
60
|
+
const p = pending(); if (p === null || typed() !== String(p)) return
|
|
61
|
+
pending.set(null); typed.set('')
|
|
62
|
+
await removePort(p)
|
|
63
|
+
}
|
|
64
|
+
onMount(() => { load() })
|
|
65
|
+
</script>
|
|
66
|
+
<!DOCTYPE html>
|
|
67
|
+
<html lang="en" data-theme="dark">
|
|
68
|
+
<head>@include('../partials/head', { title: 'ts-cloud · Firewall' })</head>
|
|
69
|
+
<body>
|
|
70
|
+
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
71
|
+
<div class="wrap">
|
|
72
|
+
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Firewall</div>
|
|
73
|
+
<header class="page">
|
|
74
|
+
<div><h1>Firewall</h1><p>ts-cloud reconciles the host firewall (UFW) from cloud config on every deploy. SSH (22), HTTP (80), and HTTPS (443) are always open and cannot be changed here.</p></div>
|
|
75
|
+
<div class="cmds header-actions"><span class="accent cmd">cloud server:secure {{ server.name }}</span></div>
|
|
76
|
+
</header>
|
|
77
|
+
|
|
78
|
+
<div class="section">
|
|
79
|
+
<h2>Always open</h2>
|
|
80
|
+
<div class="panel">
|
|
81
|
+
<div class="port-grid">
|
|
82
|
+
<template :for="p in alwaysOpen()">
|
|
83
|
+
<div class="port-card">
|
|
84
|
+
<div class="top"><span class="tag">tcp</span><span class="pill ok">reserved</span></div>
|
|
85
|
+
<div class="listen mono">{{ p }}</div>
|
|
86
|
+
<div class="proc">Always allowed by ts-cloud</div>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
</div>
|
|
90
|
+
<p class="note">These ports keep SSH, HTTP, and HTTPS reachable. They are read-only and managed by the framework.</p>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="section">
|
|
95
|
+
<h2>Allow a port</h2>
|
|
96
|
+
<div class="panel">
|
|
97
|
+
<form class="form-grid" @submit.prevent="allowPort()">
|
|
98
|
+
<div class="field-block">
|
|
99
|
+
<label for="fw-port">Port</label>
|
|
100
|
+
<input id="fw-port" type="number" min="1" max="65535" autocomplete="off" placeholder="8080" :value="newPort()" @input="newPort.set($event.target.value)" required>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="form-actions">
|
|
103
|
+
<button class="btn" type="submit">Allow</button>
|
|
104
|
+
<span class="msg" :class="messageKind()" role="status">{{ message() }}</span>
|
|
105
|
+
</div>
|
|
106
|
+
</form>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="section">
|
|
111
|
+
<h2>Managed ports</h2>
|
|
112
|
+
<div class="panel">
|
|
113
|
+
<div class="port-grid" @show="ports().length > 0">
|
|
114
|
+
<template :for="p in ports()">
|
|
115
|
+
<div class="port-card">
|
|
116
|
+
<div class="top"><span class="tag">tcp</span><button type="button" class="btn danger sm" @click="askRemove(p)">Remove</button></div>
|
|
117
|
+
<div class="listen mono">{{ p }}</div>
|
|
118
|
+
<div class="proc">Allowed via cloud config</div>
|
|
119
|
+
</div>
|
|
120
|
+
</template>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="compact empty" @show="ports().length === 0"><strong>No extra ports configured</strong><span>Only 22, 80, and 443 are open. Allow a port above to update <span class="mono">infrastructure.compute.firewall</span>.</span></div>
|
|
123
|
+
<div class="op-confirm panel" @show="pending() !== null">
|
|
124
|
+
<span>Type <b class="mono">{{ confirmTok() }}</b> to remove this port:</span>
|
|
125
|
+
<input class="op-confirm-input" :value="typed()" @input="typed.set($event.target.value)" @keydown.enter="runRemove()" placeholder="confirm" autocomplete="off">
|
|
126
|
+
<button class="btn sm" :disabled="!canRun()" @click="runRemove()">Run</button>
|
|
127
|
+
<button class="btn ghost sm" @click="cancelRemove()">Cancel</button>
|
|
128
|
+
</div>
|
|
129
|
+
<p class="note">Ports are declarative: this page edits <span class="mono">infrastructure.compute.firewall</span> in cloud config and applies <span class="mono">ufw allow</span> / <span class="mono">ufw delete allow</span> live on the box. The full config is reconciled again on the next <span class="mono">cloud deploy</span>.</p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</body>
|
|
134
|
+
</html>
|
|
@@ -7,7 +7,7 @@ const emptyReason = __d?.serverLogsEmptyReason ?? 'No server logs are available
|
|
|
7
7
|
</script>
|
|
8
8
|
<script client>
|
|
9
9
|
// Reactive log viewer: seeded from build-time data (stx server→client bridge),
|
|
10
|
-
// then filters by level + source and auto-refreshes
|
|
10
|
+
// then filters by level + source and auto-refreshes - all via signals +
|
|
11
11
|
// directives (no manual DOM).
|
|
12
12
|
const logs = state(initialLogs)
|
|
13
13
|
const level = state('all')
|
|
@@ -76,7 +76,7 @@ onMount(() => { refresh(); const id = setInterval(refresh, 8000); return () => c
|
|
|
76
76
|
<div class="compact empty" @show="logs().length > 0 && filtered().length === 0"><strong>No matching logs</strong><span>Try a different level or source filter.</span></div>
|
|
77
77
|
<div class="empty" @show="logs().length === 0"><strong>No logs available</strong><span>{{ emptyReason }}</span></div>
|
|
78
78
|
|
|
79
|
-
<p class="note">Live <span class="mono">journalctl</span> for the web server, every managed service, and each app's service, <b>queue workers</b>, and daemons
|
|
79
|
+
<p class="note">Live <span class="mono">journalctl</span> for the web server, every managed service, and each app's service, <b>queue workers</b>, and daemons - plus the backup runner. Auto-refreshes every 8s.</p>
|
|
80
80
|
</div>
|
|
81
81
|
</div>
|
|
82
82
|
</div>
|
|
@@ -35,7 +35,7 @@ const loadPerCpuNote = `${loadPerCpu} per core`
|
|
|
35
35
|
const memTotalNote = `of ${memTotalGb} GB total`
|
|
36
36
|
const diskTotalNote = `of ${m.diskTotalGb} GB total`
|
|
37
37
|
|
|
38
|
-
// ── Threshold tone: green < 70%, amber 70
|
|
38
|
+
// ── Threshold tone: green < 70%, amber 70-90%, red ≥ 90% ───────────────
|
|
39
39
|
function toneOf(p) { return p >= 90 ? 'bad' : p >= 70 ? 'warn' : 'ok' }
|
|
40
40
|
function word(p) { return p >= 90 ? 'Critical' : p >= 70 ? 'Elevated' : 'Healthy' }
|
|
41
41
|
const cpuTone = toneOf(loadPct), memTone = toneOf(memPct), diskTone = toneOf(diskPct)
|
|
@@ -45,12 +45,12 @@ const ranks = { ok: 0, warn: 1, bad: 2 }
|
|
|
45
45
|
const worst = [cpuTone, memTone, diskTone].sort((a, b) => ranks[b] - ranks[a])[0]
|
|
46
46
|
const measuredOverall = {
|
|
47
47
|
bad: { tone: 'bad', title: 'Action needed', note: 'One or more vitals are in the critical range.' },
|
|
48
|
-
warn: { tone: 'warn', title: 'Running warm', note: 'Resources are elevated
|
|
48
|
+
warn: { tone: 'warn', title: 'Running warm', note: 'Resources are elevated - keep an eye on it.' },
|
|
49
49
|
ok: { tone: 'ok', title: 'All systems healthy', note: 'CPU, memory, and disk are well within limits.' },
|
|
50
50
|
}[worst]
|
|
51
51
|
const overall = live ? measuredOverall : { tone: 'warn', title: 'Probe unavailable', note: unavailableNote }
|
|
52
52
|
|
|
53
|
-
// One descriptor per gauge
|
|
53
|
+
// One descriptor per gauge - rendered with @foreach below.
|
|
54
54
|
const gaugeCards = [
|
|
55
55
|
{ label: 'CPU Load', icon: 'cpu', tone: live ? cpuTone : 'warn', tag: live ? word(loadPct) : 'Unavailable', big: live ? loadPct : '-', pct: loadPct, unit: live ? 'of capacity' : 'probe offline', sub: live ? `${loadPerCpu} load / core · ${m.load} avg` : unavailableNote },
|
|
56
56
|
{ label: 'Memory', icon: 'mem', tone: live ? memTone : 'warn', tag: live ? word(memPct) : 'Unavailable', big: live ? memPct : '-', pct: memPct, unit: live ? 'memory used' : 'probe offline', sub: live ? `${memUsedGb} / ${memTotalGb} GB · ${memFreeGb} free` : unavailableNote },
|
|
@@ -63,7 +63,7 @@ const resourceChart = renderResourceBars([
|
|
|
63
63
|
{ label: 'Disk', value: diskPct, detail: live ? `${m.diskUsedGb} / ${m.diskTotalGb} GB` : unavailableNote },
|
|
64
64
|
], { disabled: !live })
|
|
65
65
|
|
|
66
|
-
// ── Load trend (sample only
|
|
66
|
+
// ── Load trend (sample only - not part of the live metrics payload) ────
|
|
67
67
|
const loadTrend = __d?.loadTrend ?? [22, 28, 31, 26, 40, 38, 33, 45, 41, 30, 27, 35, 48, 52, 39, 33, 29, 44, 50, 47, 36, 31, 28, 34]
|
|
68
68
|
const maxLoad = Math.max(...loadTrend, 1)
|
|
69
69
|
const trendPeak = Math.max(...loadTrend)
|
|
@@ -72,7 +72,7 @@ function barTone(v) { const r = v / maxLoad; return r >= 0.9 ? 'peak' : r >= 0.7
|
|
|
72
72
|
</script>
|
|
73
73
|
<!DOCTYPE html>
|
|
74
74
|
<html lang="en" data-theme="dark">
|
|
75
|
-
<head>@include('../partials/head', { title: 'ts-cloud
|
|
75
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Server health' })</head>
|
|
76
76
|
<body>
|
|
77
77
|
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
78
78
|
<div class="wrap">
|
|
@@ -31,7 +31,7 @@ function certLabel(cert) {
|
|
|
31
31
|
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Security</div>
|
|
32
32
|
<header class="page">
|
|
33
33
|
<div><h1>Security</h1><p>Firewall state, listening ports, TLS expiry, and recent SSH auth events.</p></div>
|
|
34
|
-
<div class="cmds header-actions"><span class="accent cmd">cloud server:secure {{ server.name }}</span></div>
|
|
34
|
+
<div class="cmds header-actions"><a class="btn ghost sm" href="/server/firewall">Manage firewall →</a><span class="accent cmd">cloud server:secure {{ server.name }}</span></div>
|
|
35
35
|
</header>
|
|
36
36
|
|
|
37
37
|
<div class="grid cards-3">
|
|
@@ -38,7 +38,7 @@ async function runOp() {
|
|
|
38
38
|
</script>
|
|
39
39
|
<!DOCTYPE html>
|
|
40
40
|
<html lang="en" data-theme="dark">
|
|
41
|
-
<head>@include('../partials/head', { title: 'ts-cloud
|
|
41
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Services' })</head>
|
|
42
42
|
<body>
|
|
43
43
|
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
44
44
|
<div class="wrap">
|