@stacksjs/ts-cloud 0.5.26 → 0.5.28
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 +1223 -7366
- 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 +3381 -210
- 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,54 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// Mode-aware, fully server-rendered nav. The deployment mode + environment list
|
|
3
|
+
// are injected with the dashboard data; the environment switcher is plain links
|
|
4
|
+
// (`?env=<name>`) the server handles — no client JavaScript needed.
|
|
5
|
+
let __nav = null
|
|
6
|
+
try { __nav = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __nav = null }
|
|
7
|
+
const navMode = __nav?.mode ?? 'server'
|
|
8
|
+
const showServer = navMode === 'server' || navMode === 'hybrid'
|
|
9
|
+
const showServerless = navMode === 'serverless' || navMode === 'hybrid'
|
|
10
|
+
const navEnvs = __nav?.environments ?? []
|
|
11
|
+
const navEnv = __nav?.environment ?? ''
|
|
12
|
+
</script>
|
|
13
|
+
<nav>
|
|
14
|
+
<div class="brand"><span class="dot"></span> ts-cloud</div>
|
|
15
|
+
<div class="tabs">
|
|
16
|
+
@if (showServer)
|
|
17
|
+
@if ((active ?? '') === 'server')
|
|
18
|
+
<a href="/" class="active">Server</a>
|
|
19
|
+
@else
|
|
20
|
+
<a href="/">Server</a>
|
|
21
|
+
@endif
|
|
22
|
+
@endif
|
|
23
|
+
@if (showServerless)
|
|
24
|
+
@if ((active ?? '') === 'serverless')
|
|
25
|
+
<a href="/serverless" class="active">Serverless</a>
|
|
26
|
+
@else
|
|
27
|
+
<a href="/serverless">Serverless</a>
|
|
28
|
+
@endif
|
|
29
|
+
@endif
|
|
30
|
+
@if (showServer)
|
|
31
|
+
@if ((active ?? '') === 'database')
|
|
32
|
+
<a href="/server/database" class="active">Databases</a>
|
|
33
|
+
@else
|
|
34
|
+
<a href="/server/database">Databases</a>
|
|
35
|
+
@endif
|
|
36
|
+
@endif
|
|
37
|
+
</div>
|
|
38
|
+
<div class="nav-meta">
|
|
39
|
+
<span>{{ name ?? '' }}</span>
|
|
40
|
+
@if (navEnvs.length > 1)
|
|
41
|
+
<span class="env-switch" title="Switch environment">
|
|
42
|
+
@foreach (navEnvs as e)
|
|
43
|
+
@if (e === navEnv)
|
|
44
|
+
<a class="active env-opt" href="?env={{ e }}">{{ e }}</a>
|
|
45
|
+
@else
|
|
46
|
+
<a class="env-opt" href="?env={{ e }}">{{ e }}</a>
|
|
47
|
+
@endif
|
|
48
|
+
@endforeach
|
|
49
|
+
</span>
|
|
50
|
+
@else
|
|
51
|
+
<span class="env">{{ navEnv || env || '' }}</span>
|
|
52
|
+
@endif
|
|
53
|
+
</div>
|
|
54
|
+
</nav>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<div class="op-confirm panel" @show="pending() !== null">
|
|
2
|
+
<span>Type <b class="mono">{{ confirmTok() }}</b> to {{ confirmVerb() }}:</span>
|
|
3
|
+
<input class="op-confirm-input" :value="typed()" @input="typed.set($event.target.value)" @keydown.enter="runOp()" placeholder="confirm" autocomplete="off">
|
|
4
|
+
<button class="btn sm" :disabled="!canRun()" @click="runOp()">Run</button>
|
|
5
|
+
<button class="btn ghost sm" @click="cancelOp()">Cancel</button>
|
|
6
|
+
</div>
|
|
7
|
+
<pre class="action-output op-output" @show="opShown()">{{ opOutput() }}</pre>
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
// Reactive action runner: non-mutating actions run immediately; mutating ones
|
|
8
|
+
// (deploy) stage a typed-confirmation via the shared op-confirm bar.
|
|
9
|
+
const opOutput = state('')
|
|
10
|
+
const opShown = state(false)
|
|
11
|
+
const pending = state(null)
|
|
12
|
+
const typed = state('')
|
|
13
|
+
const confirmTok = derived(() => { const p = pending(); return p ? p.confirm : '' })
|
|
14
|
+
const confirmVerb = derived(() => { const p = pending(); return p ? p.verb : '' })
|
|
15
|
+
const canRun = derived(() => { const p = pending(); return !!p && typed() === p.confirm })
|
|
16
|
+
async function execAction(action, confirm) {
|
|
17
|
+
opShown.set(true); opOutput.set('Running ' + action + '...')
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch('/api/actions/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action, confirm }) })
|
|
20
|
+
const body = await res.json()
|
|
21
|
+
opOutput.set((body.ok ? 'OK ' : 'FAILED ') + (body.command || action) + '\nexit ' + (body.exitCode == null ? '-' : body.exitCode) + '\n\n' + (body.stdout || '') + (body.stderr ? '\n' + body.stderr : '') + (body.error ? '\n' + body.error : ''))
|
|
22
|
+
} catch (e) { opOutput.set('FAILED ' + action + '\n\n' + ((e && e.message) || e)) }
|
|
23
|
+
}
|
|
24
|
+
function askAction(action, confirm, verb) {
|
|
25
|
+
if (!confirm) { execAction(action, ''); return }
|
|
26
|
+
pending.set({ operation: action, confirm, verb: verb || ('run ' + action) }); typed.set('')
|
|
27
|
+
}
|
|
28
|
+
function cancelOp() { pending.set(null) }
|
|
29
|
+
async function runOp() {
|
|
30
|
+
const p = pending()
|
|
31
|
+
if (!p || typed() !== p.confirm) return
|
|
32
|
+
pending.set(null); typed.set('')
|
|
33
|
+
await execAction(p.operation, p.confirm)
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
<!DOCTYPE html>
|
|
37
|
+
<html lang="en" data-theme="dark">
|
|
38
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Operations' })</head>
|
|
39
|
+
<body>
|
|
40
|
+
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
41
|
+
<div class="wrap">
|
|
42
|
+
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Operations</div>
|
|
43
|
+
<header class="page"><div><h1>Operations</h1><p>Run allowlisted local cloud commands and inspect their output.</p></div></header>
|
|
44
|
+
|
|
45
|
+
<div class="section">
|
|
46
|
+
<h2>Run command</h2>
|
|
47
|
+
<div class="panel">
|
|
48
|
+
<div class="grid cards-2">
|
|
49
|
+
<button class="btn ghost" type="button" @click="askAction('status', '', 'refresh status')">Refresh Cloud Status</button>
|
|
50
|
+
<button class="btn ghost" type="button" @click="askAction('doctor', '')">Run Doctor</button>
|
|
51
|
+
<button class="btn ghost" type="button" @click="askAction('security-scan', '')">Security Scan</button>
|
|
52
|
+
<button class="btn danger" type="button" @click="askAction('deploy', 'deploy', 'deploy the environment')">Deploy Environment</button>
|
|
53
|
+
</div>
|
|
54
|
+
@include('../partials/op-confirm')
|
|
55
|
+
<p class="note">Mutating commands require typed confirmation. Output is clamped so the browser stays responsive.</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
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 activity = __d?.activity ?? []
|
|
6
|
+
const counts = {
|
|
7
|
+
deploy: activity.filter(item => item.type === 'deploy').length,
|
|
8
|
+
log: activity.filter(item => item.type === 'log').length,
|
|
9
|
+
ssh: activity.filter(item => item.type === 'ssh').length,
|
|
10
|
+
}
|
|
11
|
+
function tone(status) {
|
|
12
|
+
if (status === 'ok') return 'ok'
|
|
13
|
+
if (status === 'bad') return 'bad'
|
|
14
|
+
return 'warn'
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
<!DOCTYPE html>
|
|
18
|
+
<html lang="en" data-theme="dark">
|
|
19
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Activity' })</head>
|
|
20
|
+
<body>
|
|
21
|
+
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
22
|
+
<div class="wrap">
|
|
23
|
+
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Activity</div>
|
|
24
|
+
<header class="page">
|
|
25
|
+
<div><h1>Activity</h1><p>Deploys, warnings, errors, and access changes collected from local config and live logs.</p></div>
|
|
26
|
+
<div class="cmds header-actions"><span class="cmd">cloud deploy:history</span><span class="cmd">cloud server:logs {{ server.name }}</span></div>
|
|
27
|
+
</header>
|
|
28
|
+
|
|
29
|
+
<div class="grid cards-3">
|
|
30
|
+
<div class="panel"><h3>Deploy activity</h3><div class="stat">{{ counts.deploy }}</div></div>
|
|
31
|
+
<div class="panel"><h3>Warnings & errors</h3><div class="stat">{{ counts.log }}</div></div>
|
|
32
|
+
<div class="panel"><h3>SSH key events</h3><div class="stat">{{ counts.ssh }}</div></div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="section">
|
|
36
|
+
<h2>Timeline</h2>
|
|
37
|
+
<div class="panel">
|
|
38
|
+
@if (activity.length)
|
|
39
|
+
<div class="timeline">
|
|
40
|
+
@foreach (activity as item)
|
|
41
|
+
<div class="timeline-row">
|
|
42
|
+
<div class="type"><span class="pill {{ tone(item.tone) }}">{{ item.type }}</span></div>
|
|
43
|
+
<div><div class="title">{{ item.title }}</div><div class="detail">{{ item.detail }}</div></div>
|
|
44
|
+
<div class="when">{{ item.when }}</div>
|
|
45
|
+
</div>
|
|
46
|
+
@endforeach
|
|
47
|
+
</div>
|
|
48
|
+
@else
|
|
49
|
+
<div class="empty"><strong>No activity yet</strong><span>Deploy history, warnings, and SSH key changes will appear here as the dashboard collects them.</span></div>
|
|
50
|
+
@endif
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
@@ -0,0 +1,91 @@
|
|
|
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: 'acme-production-app' }
|
|
5
|
+
const backup = __d?.backup ?? { enabled: true, destination: 'acme-backups', retention: 7, schedule: '0 2 * * *' }
|
|
6
|
+
const history = __d?.backupHistory ?? [
|
|
7
|
+
{ id: 'snap-0a1b', when: 'today 02:00', size: '142 MB', status: 'success', kind: 'snapshot' },
|
|
8
|
+
{ id: 'snap-9f2c', when: 'yesterday 02:00', size: '139 MB', status: 'success', kind: 'snapshot' },
|
|
9
|
+
{ id: 'snap-7d3e', when: '2d ago 02:00', size: '137 MB', status: 'success', kind: 'snapshot' },
|
|
10
|
+
]
|
|
11
|
+
function tone(s) { return s === 'success' ? 'ok' : (s === 'failed' ? 'bad' : 'warn') }
|
|
12
|
+
</script>
|
|
13
|
+
<script client>
|
|
14
|
+
const opOutput = state('')
|
|
15
|
+
const opShown = state(false)
|
|
16
|
+
const pending = state(null)
|
|
17
|
+
const typed = state('')
|
|
18
|
+
const confirmTok = derived(() => { const p = pending(); return p ? p.confirm : '' })
|
|
19
|
+
const confirmVerb = derived(() => { const p = pending(); return p ? p.verb : '' })
|
|
20
|
+
const canRun = derived(() => { const p = pending(); return !!p && typed() === p.confirm })
|
|
21
|
+
function askOp(operation, confirm, verb, to) { pending.set({ operation, confirm, verb, to: to || null }); typed.set('') }
|
|
22
|
+
function cancelOp() { pending.set(null) }
|
|
23
|
+
async function runOp() {
|
|
24
|
+
const p = pending()
|
|
25
|
+
if (!p || typed() !== p.confirm) return
|
|
26
|
+
pending.set(null); typed.set('')
|
|
27
|
+
opShown.set(true); opOutput.set('Running ' + p.operation + '...')
|
|
28
|
+
try {
|
|
29
|
+
const payload = p.to ? { operation: p.operation, confirm: p.confirm, to: p.to } : { operation: p.operation, confirm: p.confirm }
|
|
30
|
+
const res = await fetch('/api/server/operations/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
|
|
31
|
+
const body = await res.json()
|
|
32
|
+
opOutput.set((body.ok ? 'OK ' : 'FAILED ') + (body.command || p.operation) + '\n\n' + (body.stdout || '') + (body.stderr ? '\n' + body.stderr : '') + (body.error ? '\n' + body.error : ''))
|
|
33
|
+
} catch (e) { opOutput.set('FAILED ' + p.operation + '\n\n' + ((e && e.message) || e)) }
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
<!DOCTYPE html>
|
|
37
|
+
<html lang="en" data-theme="dark">
|
|
38
|
+
<head>@include('../partials/head', { title: 'ts-cloud — Backups' })</head>
|
|
39
|
+
<body>
|
|
40
|
+
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
41
|
+
<div class="wrap">
|
|
42
|
+
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Backups</div>
|
|
43
|
+
<header class="page">
|
|
44
|
+
<div><h1>Backups & snapshots</h1><p>Scheduled snapshots of the box and app data.</p></div>
|
|
45
|
+
<div class="cmds header-actions"><span class="accent cmd">cloud server:snapshot {{ server.name }}</span></div>
|
|
46
|
+
</header>
|
|
47
|
+
<div class="section">
|
|
48
|
+
<div class="panel">
|
|
49
|
+
<div class="kv"><span>state</span> <b>@if (backup.enabled)<span class="ok pill">on</span>@else<span class="pill warn">off</span>@endif</b></div>
|
|
50
|
+
<div class="kv"><span>destination</span> <b class="mono">{{ backup.destination }}</b></div>
|
|
51
|
+
<div class="kv"><span>schedule</span> <b class="mono">{{ backup.schedule }}</b></div>
|
|
52
|
+
<div class="kv"><span>retention</span> <b>{{ backup.retention }} copies</b></div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="section">
|
|
56
|
+
<h2>Run a backup</h2>
|
|
57
|
+
<div class="panel">
|
|
58
|
+
<div class="row-actions">
|
|
59
|
+
<button class="btn" type="button" @click="askOp('backup:run', 'backup', 'run a backup now')">Run backup now</button>
|
|
60
|
+
<button class="btn danger" type="button" @click="askOp('backup:restore', 'restore', 'restore the latest backup')">Restore latest backup</button>
|
|
61
|
+
</div>
|
|
62
|
+
@include('../partials/op-confirm')
|
|
63
|
+
<p class="note">Backups dump the app database via ts-backups and upload to the configured destination. Restore replaces the live database from the most recent dump.</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="section">
|
|
67
|
+
<h2>Recent backups</h2>
|
|
68
|
+
<div class="panel">
|
|
69
|
+
@if (history.length)
|
|
70
|
+
<table>
|
|
71
|
+
<thead><tr><th>Snapshot</th><th>Kind</th><th>Size</th><th>Status</th><th>When</th></tr></thead>
|
|
72
|
+
<tbody>
|
|
73
|
+
@foreach (history as b)
|
|
74
|
+
<tr>
|
|
75
|
+
<td class="mono">{{ b.id }}</td>
|
|
76
|
+
<td>{{ b.kind }}</td>
|
|
77
|
+
<td>{{ b.size }}</td>
|
|
78
|
+
<td><span class="pill {{ tone(b.status) }}">{{ b.status }}</span></td>
|
|
79
|
+
<td style="color:var(--txt3)">{{ b.when }}</td>
|
|
80
|
+
</tr>
|
|
81
|
+
@endforeach
|
|
82
|
+
</tbody>
|
|
83
|
+
</table>
|
|
84
|
+
@else
|
|
85
|
+
<div class="empty"><strong>No backup history</strong><span>Snapshot history has not been collected yet. Run a backup above, then entries will show here.</span></div>
|
|
86
|
+
@endif
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
|
@@ -0,0 +1,119 @@
|
|
|
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: 'acme-production-app' }
|
|
5
|
+
</script>
|
|
6
|
+
<script client>
|
|
7
|
+
// Reactive database management — create databases/users, live lists.
|
|
8
|
+
const engine = state('mysql')
|
|
9
|
+
const databases = state([])
|
|
10
|
+
const users = state([])
|
|
11
|
+
const dbError = state('')
|
|
12
|
+
const output = state('')
|
|
13
|
+
const outputShown = state(false)
|
|
14
|
+
const dbName = state('')
|
|
15
|
+
const uName = state('')
|
|
16
|
+
const uPass = state('')
|
|
17
|
+
const uDatabase = state('')
|
|
18
|
+
const uAccess = state('all')
|
|
19
|
+
function show(text) { output.set(text); outputShown.set(true) }
|
|
20
|
+
async function load() {
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch('/api/databases')
|
|
23
|
+
const data = await res.json()
|
|
24
|
+
engine.set(data.engine || 'mysql')
|
|
25
|
+
if (data.ok) { databases.set(data.databases || []); users.set(data.users || []); dbError.set('') }
|
|
26
|
+
else { databases.set([]); users.set([]); dbError.set(data.error || 'unavailable') }
|
|
27
|
+
} catch (e) { show('Could not read databases: ' + ((e && e.message) || e)) }
|
|
28
|
+
}
|
|
29
|
+
async function createDb() {
|
|
30
|
+
show('Creating database ' + dbName() + '...')
|
|
31
|
+
const res = await fetch('/api/databases', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: dbName() }) })
|
|
32
|
+
const b = await res.json()
|
|
33
|
+
show((b.ok ? 'OK created ' + (b.name || dbName()) : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
|
|
34
|
+
if (b.ok) { dbName.set(''); load() }
|
|
35
|
+
}
|
|
36
|
+
async function createUser() {
|
|
37
|
+
show('Creating user ' + uName() + '...')
|
|
38
|
+
const payload = { username: uName(), password: uPass(), access: uAccess() }
|
|
39
|
+
if (uDatabase()) payload.database = uDatabase()
|
|
40
|
+
const res = await fetch('/api/databases/users', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
|
|
41
|
+
const b = await res.json()
|
|
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
|
+
if (b.ok) { uName.set(''); uPass.set(''); load() }
|
|
44
|
+
}
|
|
45
|
+
onMount(() => { load() })
|
|
46
|
+
</script>
|
|
47
|
+
<!DOCTYPE html>
|
|
48
|
+
<html lang="en" data-theme="dark">
|
|
49
|
+
<head>@include('../partials/head', { title: 'ts-cloud — Databases' })</head>
|
|
50
|
+
<body>
|
|
51
|
+
@include('../partials/nav', { active: 'database', name: server.name, env: 'production' })
|
|
52
|
+
<div class="wrap">
|
|
53
|
+
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Databases</div>
|
|
54
|
+
<header class="page">
|
|
55
|
+
<div><h1>Databases & users</h1><p>Create databases and app users on the managed engine (<span class="mono">{{ engine() }}</span>).</p></div>
|
|
56
|
+
</header>
|
|
57
|
+
|
|
58
|
+
<div class="cols section">
|
|
59
|
+
<div>
|
|
60
|
+
<h2>Create database</h2>
|
|
61
|
+
<div class="panel">
|
|
62
|
+
<form class="form-grid" @submit.prevent="createDb()">
|
|
63
|
+
<div class="field-block wide"><label for="db-name">Name</label><input id="db-name" placeholder="acme_production" autocomplete="off" :value="dbName()" @input="dbName.set($event.target.value)"></div>
|
|
64
|
+
<div class="form-actions"><button class="btn" type="submit">Create database</button></div>
|
|
65
|
+
</form>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<h2>Create user</h2>
|
|
70
|
+
<div class="panel">
|
|
71
|
+
<form class="form-grid" @submit.prevent="createUser()">
|
|
72
|
+
<div class="field-block"><label for="db-user">Username</label><input id="db-user" placeholder="acme_app" autocomplete="off" :value="uName()" @input="uName.set($event.target.value)"></div>
|
|
73
|
+
<div class="field-block"><label for="db-pass">Password</label><input id="db-pass" type="password" autocomplete="new-password" :value="uPass()" @input="uPass.set($event.target.value)"></div>
|
|
74
|
+
<div class="field-block"><label for="db-user-db">Grant on database</label>
|
|
75
|
+
<select id="db-user-db" :value="uDatabase()" @change="uDatabase.set($event.target.value)">
|
|
76
|
+
<option value="">(none)</option>
|
|
77
|
+
<template :for="n in databases()"><option :value="n">{{ n }}</option></template>
|
|
78
|
+
</select>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="field-block"><label for="db-access">Access</label>
|
|
81
|
+
<select id="db-access" :value="uAccess()" @change="uAccess.set($event.target.value)">
|
|
82
|
+
<option value="all">all privileges</option>
|
|
83
|
+
<option value="readonly">read only</option>
|
|
84
|
+
</select>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="form-actions"><button class="btn" type="submit">Create user</button></div>
|
|
87
|
+
</form>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<pre class="action-output" @show="outputShown()">{{ output() }}</pre>
|
|
93
|
+
|
|
94
|
+
<div class="cols section">
|
|
95
|
+
<div>
|
|
96
|
+
<h2>Databases</h2>
|
|
97
|
+
<div class="panel">
|
|
98
|
+
<div class="svc">
|
|
99
|
+
<template :for="n in databases()"><span class="item">{{ n }}</span></template>
|
|
100
|
+
<span class="item" @show="databases().length === 0 && dbError() !== ''">{{ dbError() }}</span>
|
|
101
|
+
<span class="item" @show="databases().length === 0 && dbError() === ''">no databases</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div>
|
|
106
|
+
<h2>Users</h2>
|
|
107
|
+
<div class="panel">
|
|
108
|
+
<div class="svc">
|
|
109
|
+
<template :for="n in users()"><span class="item">{{ n }}</span></template>
|
|
110
|
+
<span class="item" @show="users().length === 0 && dbError() !== ''">{{ dbError() }}</span>
|
|
111
|
+
<span class="item" @show="users().length === 0 && dbError() === ''">no users</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<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.</p>
|
|
117
|
+
</div>
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
import { renderDeploymentSparkline } from '../../src/charts.ts'
|
|
3
|
+
|
|
4
|
+
let __d = null
|
|
5
|
+
try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
|
|
6
|
+
const server = __d?.server ?? { name: 'acme-production-app' }
|
|
7
|
+
const initialDeps = __d?.serverDeploymentsDetail ?? []
|
|
8
|
+
const deploymentChart = renderDeploymentSparkline(initialDeps)
|
|
9
|
+
const deploymentsEmptyReason = __d?.deploymentsEmptyReason ?? 'No deployment history has been recorded yet.'
|
|
10
|
+
</script>
|
|
11
|
+
<script client>
|
|
12
|
+
// Seeded from build-time data (stx server→client bridge), then live-refreshed.
|
|
13
|
+
const deps = state(initialDeps)
|
|
14
|
+
const status = state('all')
|
|
15
|
+
const site = state('all')
|
|
16
|
+
const sites = derived(() => [...new Set(deps().map(d => d.site).filter(Boolean))])
|
|
17
|
+
const filtered = derived(() => deps().filter(d =>
|
|
18
|
+
(status() === 'all' || d.status === status()) && (site() === 'all' || d.site === site())))
|
|
19
|
+
function tone(s) { return s === 'success' ? 'ok' : (s === 'failed' ? 'bad' : 'warn') }
|
|
20
|
+
async function refresh() {
|
|
21
|
+
try { const r = await fetch('/api/dashboard-data'); if (!r.ok) return; const x = await r.json(); if (Array.isArray(x.serverDeploymentsDetail)) deps.set(x.serverDeploymentsDetail) } catch {}
|
|
22
|
+
}
|
|
23
|
+
onMount(() => { refresh() })
|
|
24
|
+
|
|
25
|
+
// Reactive operation runner (rollback) — typed-confirm bar + output.
|
|
26
|
+
const opOutput = state('')
|
|
27
|
+
const opShown = state(false)
|
|
28
|
+
const pending = state(null)
|
|
29
|
+
const typed = state('')
|
|
30
|
+
const confirmTok = derived(() => { const p = pending(); return p ? p.confirm : '' })
|
|
31
|
+
const confirmVerb = derived(() => { const p = pending(); return p ? p.verb : '' })
|
|
32
|
+
const canRun = derived(() => { const p = pending(); return !!p && typed() === p.confirm })
|
|
33
|
+
function askOp(operation, confirm, verb, to) { pending.set({ operation, confirm, verb, to: to || null }); typed.set('') }
|
|
34
|
+
function cancelOp() { pending.set(null) }
|
|
35
|
+
async function runOp() {
|
|
36
|
+
const p = pending()
|
|
37
|
+
if (!p || typed() !== p.confirm) return
|
|
38
|
+
pending.set(null); typed.set('')
|
|
39
|
+
opShown.set(true); opOutput.set('Running ' + p.operation + '...')
|
|
40
|
+
try {
|
|
41
|
+
const payload = p.to ? { operation: p.operation, confirm: p.confirm, to: p.to } : { operation: p.operation, confirm: p.confirm }
|
|
42
|
+
const res = await fetch('/api/server/operations/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
|
|
43
|
+
const body = await res.json()
|
|
44
|
+
opOutput.set((body.ok ? 'OK ' : 'FAILED ') + (body.command || p.operation) + '\n\n' + (body.stdout || '') + (body.stderr ? '\n' + body.stderr : '') + (body.error ? '\n' + body.error : ''))
|
|
45
|
+
} catch (e) { opOutput.set('FAILED ' + p.operation + '\n\n' + ((e && e.message) || e)) }
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
<!DOCTYPE html>
|
|
49
|
+
<html lang="en" data-theme="dark">
|
|
50
|
+
<head>@include('../partials/head', { title: 'ts-cloud — Deployments' })</head>
|
|
51
|
+
<body>
|
|
52
|
+
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
53
|
+
<div class="wrap">
|
|
54
|
+
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Deployments</div>
|
|
55
|
+
<header class="page">
|
|
56
|
+
<div><h1>Deployments</h1><p>Server release history, newest first.</p></div>
|
|
57
|
+
<div class="cmds header-actions"><span class="accent cmd">cloud server:deploy {{ server.name }}</span></div>
|
|
58
|
+
</header>
|
|
59
|
+
<div class="section">
|
|
60
|
+
<div class="panel">
|
|
61
|
+
<div class="chart-panel subtle">
|
|
62
|
+
{{{ deploymentChart }}}
|
|
63
|
+
</div>
|
|
64
|
+
<div class="filters">
|
|
65
|
+
<div class="segmented" role="group" aria-label="Filter deployment status">
|
|
66
|
+
<button type="button" :class="{ active: status() === 'all' }" @click="status.set('all')">All</button>
|
|
67
|
+
<button type="button" :class="{ active: status() === 'success' }" @click="status.set('success')">Success</button>
|
|
68
|
+
<button type="button" :class="{ active: status() === 'failed' }" @click="status.set('failed')">Failed</button>
|
|
69
|
+
</div>
|
|
70
|
+
<label class="select-filter">
|
|
71
|
+
<span>Site</span>
|
|
72
|
+
<select :value="site()" @change="site.set($event.target.value)">
|
|
73
|
+
<option value="all">All sites</option>
|
|
74
|
+
<template :for="s in sites()">
|
|
75
|
+
<option :value="s">{{ s }}</option>
|
|
76
|
+
</template>
|
|
77
|
+
</select>
|
|
78
|
+
</label>
|
|
79
|
+
</div>
|
|
80
|
+
<table>
|
|
81
|
+
<thead><tr><th>Build</th><th>Site</th><th>Branch</th><th>Status</th><th>Steps</th><th>By</th><th>Took</th><th>When</th><th>Action</th></tr></thead>
|
|
82
|
+
<tbody>
|
|
83
|
+
<template :for="d in filtered()">
|
|
84
|
+
<tr>
|
|
85
|
+
<td class="mono">{{ d.sha }}</td>
|
|
86
|
+
<td><b>{{ d.site }}</b></td>
|
|
87
|
+
<td class="mono" style="color:var(--txt3)">{{ d.branch }}</td>
|
|
88
|
+
<td><span class="pill" :class="tone(d.status)">{{ d.status }}</span></td>
|
|
89
|
+
<td class="mono" style="font-size:11px;color:var(--txt3)">{{ d.steps.join(' · ') }}</td>
|
|
90
|
+
<td style="color:var(--txt2)">{{ d.by }}</td>
|
|
91
|
+
<td class="mono">{{ d.took }}</td>
|
|
92
|
+
<td style="color:var(--txt3)">{{ d.when }}</td>
|
|
93
|
+
<td class="table-actions"><button class="btn danger sm" type="button" @click="askOp('rollback:' + d.site, d.site, 'roll back', d.release)">Roll back</button></td>
|
|
94
|
+
</tr>
|
|
95
|
+
</template>
|
|
96
|
+
</tbody>
|
|
97
|
+
</table>
|
|
98
|
+
<div class="empty" @show="deps().length === 0"><strong>No deployments yet</strong><span>{{ deploymentsEmptyReason }}</span></div>
|
|
99
|
+
<div class="compact empty" @show="deps().length > 0 && filtered().length === 0"><strong>No matches</strong><span>Try a different status or site filter.</span></div>
|
|
100
|
+
@include('../partials/op-confirm')
|
|
101
|
+
<p class="note">Roll back a site to any prior release, or inspect a box with <span class="mono">cloud server:logs {{ server.name }}</span>.</p>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</body>
|
|
106
|
+
</html>
|
|
@@ -0,0 +1,88 @@
|
|
|
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 diagnostics = __d?.diagnostics ?? [
|
|
6
|
+
{ name: 'Live server probe', status: 'warn', detail: 'Run the local dashboard against a configured server to fetch live diagnostics.' },
|
|
7
|
+
]
|
|
8
|
+
const services = __d?.servicesDetail ?? []
|
|
9
|
+
const sites = __d?.sitesDetail ?? []
|
|
10
|
+
const shadowedSites = sites.filter(site => site.status === 'shadowed')
|
|
11
|
+
const logs = (__d?.serverLogs ?? []).filter(log => log.level !== 'info').slice(0, 8)
|
|
12
|
+
function tone(status) {
|
|
13
|
+
if (status === 'pass' || status === 'running' || status === 'live') return 'ok'
|
|
14
|
+
if (status === 'fail' || status === 'failed' || status === 'stopped') return 'bad'
|
|
15
|
+
return 'warn'
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
<!DOCTYPE html>
|
|
19
|
+
<html lang="en" data-theme="dark">
|
|
20
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Diagnostics' })</head>
|
|
21
|
+
<body>
|
|
22
|
+
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
23
|
+
<div class="wrap">
|
|
24
|
+
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Diagnostics</div>
|
|
25
|
+
<header class="page">
|
|
26
|
+
<div><h1>Diagnostics</h1><p>Config, route, service, firewall, and probe checks in one place.</p></div>
|
|
27
|
+
<div class="cmds header-actions"><span class="accent cmd">cloud doctor</span><span class="cmd">cloud status</span></div>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
<div class="section">
|
|
31
|
+
<h2>Doctor checks</h2>
|
|
32
|
+
<div class="check-list panel">
|
|
33
|
+
@foreach (diagnostics as check)
|
|
34
|
+
<div class="check {{ check.status === 'pass' ? '' : check.status }}">
|
|
35
|
+
<span class="mark"></span>
|
|
36
|
+
<div><div class="name">{{ check.name }}</div><div class="detail">{{ check.detail }}</div></div>
|
|
37
|
+
</div>
|
|
38
|
+
@endforeach
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="cols section">
|
|
43
|
+
<div>
|
|
44
|
+
<h2>Service status</h2>
|
|
45
|
+
<div class="panel">
|
|
46
|
+
@if (services.length)
|
|
47
|
+
@foreach (services as service)
|
|
48
|
+
<div class="kv"><span class="mono">{{ service.name }}</span><b><span class="pill {{ tone(service.status) }}">{{ service.status }}</span></b></div>
|
|
49
|
+
@endforeach
|
|
50
|
+
@else
|
|
51
|
+
<div class="compact empty"><strong>No service probe</strong><span>Service diagnostics appear after the dashboard can reach the server.</span></div>
|
|
52
|
+
@endif
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<h2>Route warnings</h2>
|
|
57
|
+
<div class="panel">
|
|
58
|
+
@if (shadowedSites.length)
|
|
59
|
+
@foreach (shadowedSites as site)
|
|
60
|
+
<div class="kv"><span>{{ site.name }}</span><b class="mono">shadowed by {{ site.shadowedBy }}</b></div>
|
|
61
|
+
@endforeach
|
|
62
|
+
@else
|
|
63
|
+
<div class="compact empty"><strong>No shadowed routes</strong><span>Site routes are not masking each other.</span></div>
|
|
64
|
+
@endif
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="section">
|
|
70
|
+
<h2>Recent warnings</h2>
|
|
71
|
+
<div class="panel">
|
|
72
|
+
@if (logs.length)
|
|
73
|
+
<div class="log-list">
|
|
74
|
+
@foreach (logs as log)
|
|
75
|
+
<div class="log-row {{ log.level }}">
|
|
76
|
+
<div class="log-meta"><span>{{ log.when }}</span><span>{{ log.source }}</span><span class="pill {{ tone(log.level) }}">{{ log.level }}</span></div>
|
|
77
|
+
<pre>{{ log.message }}</pre>
|
|
78
|
+
</div>
|
|
79
|
+
@endforeach
|
|
80
|
+
</div>
|
|
81
|
+
@else
|
|
82
|
+
<div class="compact empty"><strong>No warnings found</strong><span>Warning and error logs will show here as the server emits them.</span></div>
|
|
83
|
+
@endif
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|