@stacksjs/ts-cloud 0.5.35 → 0.6.0
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 +6 -6
- 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,7 +1,7 @@
|
|
|
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 (brand gradient tile)
|
|
4
|
+
<!-- Inline SVG favicon (brand gradient tile) - declaring it stops the browser requesting /favicon.ico. -->
|
|
5
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%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%235b8cff'/%3E%3Cstop offset='1' stop-color='%238b5cf6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='3' y='3' width='26' height='26' rx='8' fill='url(%23g)'/%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">
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
/* Honest action affordance: a copyable CLI command (static dashboard has no
|
|
189
|
-
backend
|
|
189
|
+
backend - the buttons would POST nowhere, so we show the exact command). */
|
|
190
190
|
.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
191
|
.cmd::before { content: '$'; color: var(--txt3); }
|
|
192
192
|
.cmd.accent { border-color: rgba(91,140,255,0.4); color: var(--accent); }
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
svg.chart.disabled { opacity: 0.86; }
|
|
217
217
|
|
|
218
218
|
/* ───────────── Health / vital signs (server metrics) ───────────── */
|
|
219
|
-
/* Overall status banner
|
|
219
|
+
/* Overall status banner - at-a-glance "is the box OK?" verdict. */
|
|
220
220
|
.health-hero {
|
|
221
221
|
--hue: var(--ok); --huebg: var(--okbg);
|
|
222
222
|
display: flex; align-items: center; gap: 18px 28px; flex-wrap: wrap; margin-top: 8px;
|
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
.health-hero .hmeta .item span { color: var(--txt3); font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; font-weight: 700; }
|
|
240
240
|
.health-hero .hmeta .item b { color: var(--txt); font-weight: 600; font-size: 14px; }
|
|
241
241
|
|
|
242
|
-
/* Radial gauges
|
|
242
|
+
/* Radial gauges - the centerpiece read for CPU / memory / disk. */
|
|
243
243
|
.gauges { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 16px; }
|
|
244
244
|
@media (max-width: 760px) { .gauges { grid-template-columns: 1fr; } }
|
|
245
245
|
.gauge {
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
.gauge .gsub b { color: var(--txt); font-weight: 700; }
|
|
272
272
|
.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
273
|
|
|
274
|
-
/* Vital-sign tiles
|
|
274
|
+
/* Vital-sign tiles - the secondary numbers (free RAM, uptime, etc.). */
|
|
275
275
|
.vitals { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
|
276
276
|
@media (max-width: 880px) { .vitals { grid-template-columns: repeat(2, 1fr); } }
|
|
277
277
|
.vital { background: var(--panel); border: 1px solid var(--panel-br); border-radius: 14px; padding: 14px 16px; }
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
.bar.warn > i { background: linear-gradient(90deg, var(--warn), #f59e0b); }
|
|
287
287
|
.bar.bad > i { background: linear-gradient(90deg, var(--bad), #ef4444); }
|
|
288
288
|
|
|
289
|
-
/* Load trend chart
|
|
289
|
+
/* Load trend chart - sparkline with capacity guide + axis labels. */
|
|
290
290
|
.chart { position: relative; }
|
|
291
291
|
.chart .spark { height: 124px; align-items: flex-end; }
|
|
292
292
|
.chart .spark > i { background: linear-gradient(180deg, var(--accent), rgba(91,140,255,0.22)); transition: height .6s ease; }
|
|
@@ -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">
|
|
@@ -10,7 +10,7 @@ const sites = __d?.sitesDetail ?? [
|
|
|
10
10
|
function tone(s) { return s === 'live' ? 'ok' : (s === 'failed' ? 'bad' : 'warn') }
|
|
11
11
|
</script>
|
|
12
12
|
<script client>
|
|
13
|
-
// Reactive site management
|
|
13
|
+
// Reactive site management - add, per-site deploy/edit/remove - via signals +
|
|
14
14
|
// directives. Mutations re-render the server-built page (location.reload) once
|
|
15
15
|
// the cloud config has changed.
|
|
16
16
|
const addName = state(''); const addRoot = state(''); const addDomain = state(''); const addPath = state('')
|
|
@@ -53,12 +53,15 @@ async function runOp() {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Inline per-site edit (TLS / domain / env).
|
|
56
|
-
const editing = state(''); const editSsl = state(''); const editDomain = state(''); const editEnv = state('')
|
|
57
|
-
function toggleEdit(name) { editing.set(editing() === name ? '' : name); editSsl.set(''); editDomain.set(''); editEnv.set('') }
|
|
56
|
+
const editing = state(''); const editSsl = state(''); const editDomain = state(''); const editEnv = state(''); const editPhp = state(''); const editAliases = state(''); const editRedirects = state('')
|
|
57
|
+
function toggleEdit(name) { editing.set(editing() === name ? '' : name); editSsl.set(''); editDomain.set(''); editEnv.set(''); editPhp.set(''); editAliases.set(''); editRedirects.set('') }
|
|
58
58
|
async function saveEdit(name) {
|
|
59
59
|
const payload = { name }
|
|
60
60
|
if (editSsl() === 'false') payload.ssl = false; else if (editSsl()) payload.ssl = { provider: editSsl() }
|
|
61
61
|
if (editDomain()) payload.domain = editDomain()
|
|
62
|
+
if (editPhp()) payload.php = editPhp()
|
|
63
|
+
if (editAliases().trim()) payload.aliases = editAliases().split(/[\s,]+/).filter(Boolean)
|
|
64
|
+
if (editRedirects().trim()) { payload.redirects = {}; for (const line of editRedirects().split('\n')) { const i = line.indexOf('='); if (i > 0) payload.redirects[line.slice(0, i).trim()] = line.slice(i + 1).trim() } }
|
|
62
65
|
if (editEnv()) { payload.env = {}; for (const line of editEnv().split('\n')) { const i = line.indexOf('='); if (i > 0) payload.env[line.slice(0, i).trim()] = line.slice(i + 1).trim() } }
|
|
63
66
|
opShown.set(true); opOutput.set('Updating ' + name + '...')
|
|
64
67
|
try {
|
|
@@ -112,6 +115,12 @@ async function saveEdit(name) {
|
|
|
112
115
|
@if (site.envKeys && site.envKeys.length)
|
|
113
116
|
<div class="kv"><span>env vars</span> <b class="mono">{{ site.envKeys.join(', ') }}</b></div>
|
|
114
117
|
@endif
|
|
118
|
+
@if (site.aliases && site.aliases.length)
|
|
119
|
+
<div class="kv"><span>aliases</span> <b class="mono">{{ site.aliases.join(', ') }}</b></div>
|
|
120
|
+
@endif
|
|
121
|
+
@if (site.redirects && Object.keys(site.redirects).length)
|
|
122
|
+
<div class="kv"><span>redirects</span> <b class="mono">{{ Object.keys(site.redirects).length }}</b></div>
|
|
123
|
+
@endif
|
|
115
124
|
@if (site.shadowedBy)
|
|
116
125
|
<div class="kv"><span>shadowed by</span> <b class="mono">{{ site.shadowedBy }}</b></div>
|
|
117
126
|
@endif
|
|
@@ -132,6 +141,18 @@ async function saveEdit(name) {
|
|
|
132
141
|
</select>
|
|
133
142
|
</div>
|
|
134
143
|
<div class="field-block"><label>Domain</label><input placeholder="{{ site.domain }}" :value="editDomain()" @input="editDomain.set($event.target.value)"></div>
|
|
144
|
+
<div class="field-block">
|
|
145
|
+
<label>PHP version</label>
|
|
146
|
+
<select :value="editPhp()" @change="editPhp.set($event.target.value)">
|
|
147
|
+
<option value="">(unchanged)</option>
|
|
148
|
+
<option value="8.1">8.1</option>
|
|
149
|
+
<option value="8.2">8.2</option>
|
|
150
|
+
<option value="8.3">8.3</option>
|
|
151
|
+
<option value="8.4">8.4</option>
|
|
152
|
+
</select>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="field-block wide"><label>Domain aliases (space or comma separated)</label><input placeholder="www.example.com, cdn.example.com" :value="editAliases()" @input="editAliases.set($event.target.value)"></div>
|
|
155
|
+
<div class="field-block wide"><label>Redirects (from = to per line)</label><textarea rows="2" placeholder="/old = /new" :value="editRedirects()" @input="editRedirects.set($event.target.value)"></textarea></div>
|
|
135
156
|
<div class="field-block wide"><label>Env vars (KEY=value per line)</label><textarea rows="3" placeholder="NODE_ENV=production" :value="editEnv()" @input="editEnv.set($event.target.value)"></textarea></div>
|
|
136
157
|
<div class="form-actions"><button class="btn" type="submit">Save changes</button></div>
|
|
137
158
|
</form>
|
|
@@ -5,7 +5,7 @@ const server = __d?.server ?? { name: 'acme-production-app' }
|
|
|
5
5
|
const initialKeys = __d?.sshKeys ?? []
|
|
6
6
|
</script>
|
|
7
7
|
<script client>
|
|
8
|
-
// Reactive SSH key management
|
|
8
|
+
// Reactive SSH key management - list + add + remove, via signals + directives.
|
|
9
9
|
// Seeded from build-time data (stx server→client bridge); load() reconciles
|
|
10
10
|
// against the live cloud config (same describeSshKeys shape).
|
|
11
11
|
const keys = state(initialKeys)
|
|
@@ -42,7 +42,7 @@ onMount(() => { load() })
|
|
|
42
42
|
</script>
|
|
43
43
|
<!DOCTYPE html>
|
|
44
44
|
<html lang="en" data-theme="dark">
|
|
45
|
-
<head>@include('../partials/head', { title: 'ts-cloud
|
|
45
|
+
<head>@include('../partials/head', { title: 'ts-cloud - SSH Keys' })</head>
|
|
46
46
|
<body>
|
|
47
47
|
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
48
48
|
<div class="wrap">
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
</script>
|
|
6
|
+
<script client>
|
|
7
|
+
// Web terminal: a WebSocket to /api/terminal streams a persistent shell running
|
|
8
|
+
// on the dashboard host (the box, in box mode). Line-oriented: type a command
|
|
9
|
+
// and press Enter; output streams back. Not a full PTY (no vim/htop).
|
|
10
|
+
const buf = state('')
|
|
11
|
+
const status = state('connecting')
|
|
12
|
+
const cmd = state('')
|
|
13
|
+
let ws = null
|
|
14
|
+
function scrollBottom() {
|
|
15
|
+
const el = document.getElementById('term-out')
|
|
16
|
+
if (el) el.scrollTop = el.scrollHeight
|
|
17
|
+
}
|
|
18
|
+
function connect() {
|
|
19
|
+
status.set('connecting')
|
|
20
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
21
|
+
ws = new WebSocket(proto + '//' + location.host + '/api/terminal')
|
|
22
|
+
ws.onopen = () => status.set('connected')
|
|
23
|
+
ws.onclose = () => status.set('disconnected')
|
|
24
|
+
ws.onerror = () => status.set('error')
|
|
25
|
+
ws.onmessage = (e) => { buf.set((buf() + e.data).slice(-40000)); scrollBottom() }
|
|
26
|
+
}
|
|
27
|
+
function send() {
|
|
28
|
+
if (!ws || ws.readyState !== 1) return
|
|
29
|
+
ws.send(cmd() + '\n')
|
|
30
|
+
cmd.set('')
|
|
31
|
+
}
|
|
32
|
+
function reconnect() { try { if (ws) ws.close() } catch {} buf.set(''); connect() }
|
|
33
|
+
function clearOut() { buf.set('') }
|
|
34
|
+
function statusTone(s) { return s === 'connected' ? 'ok' : (s === 'disconnected' || s === 'error' ? 'bad' : 'warn') }
|
|
35
|
+
onMount(() => { connect(); return () => { try { if (ws) ws.close() } catch {} } })
|
|
36
|
+
</script>
|
|
37
|
+
<!DOCTYPE html>
|
|
38
|
+
<html lang="en" data-theme="dark">
|
|
39
|
+
<head>@include('../partials/head', { title: 'ts-cloud · Terminal' })</head>
|
|
40
|
+
<body>
|
|
41
|
+
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
42
|
+
<div class="wrap">
|
|
43
|
+
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Terminal</div>
|
|
44
|
+
<header class="page">
|
|
45
|
+
<div><h1>Terminal</h1><p>A live shell on the dashboard host. Type a command and press Enter; output streams back.</p></div>
|
|
46
|
+
<div class="header-actions">
|
|
47
|
+
<span class="pill" :class="statusTone(status())">{{ status() }}</span>
|
|
48
|
+
<button class="btn ghost sm" type="button" @click="clearOut()">Clear</button>
|
|
49
|
+
<button class="btn ghost sm" type="button" @click="reconnect()">Reconnect</button>
|
|
50
|
+
</div>
|
|
51
|
+
</header>
|
|
52
|
+
|
|
53
|
+
<div class="section">
|
|
54
|
+
<div class="panel">
|
|
55
|
+
<pre id="term-out" class="logs" style="max-height:520px; white-space:pre-wrap; overflow-wrap:anywhere;">{{ buf() }}</pre>
|
|
56
|
+
<div class="field" style="margin-top:12px;">
|
|
57
|
+
<input :value="cmd()" @input="cmd.set($event.target.value)" @keydown.enter="send()" placeholder="e.g. systemctl status rpx-gateway, tail -n 50 /var/log/syslog" autocomplete="off" spellcheck="false" style="font-family:ui-monospace, SFMono-Regular, Menlo, monospace;">
|
|
58
|
+
<button class="btn" type="button" @click="send()">Run</button>
|
|
59
|
+
</div>
|
|
60
|
+
<p class="note">The shell runs on the machine serving this dashboard (in box mode, the provisioned server) and is protected by the same Basic auth. It is line-oriented, so full-screen programs like vim or htop are not supported. Disable it with <span class="mono">TS_CLOUD_DASHBOARD_TERMINAL=0</span>.</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
|
@@ -39,7 +39,7 @@ async function runOp() {
|
|
|
39
39
|
</script>
|
|
40
40
|
<!DOCTYPE html>
|
|
41
41
|
<html lang="en" data-theme="dark">
|
|
42
|
-
<head>@include('../partials/head', { title: 'ts-cloud
|
|
42
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Workers' })</head>
|
|
43
43
|
<body>
|
|
44
44
|
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
45
45
|
<div class="wrap">
|