@stacksjs/ts-cloud 0.5.26 → 0.5.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/bin/cli.js +1216 -7359
  2. package/dist/deploy/dashboard-data-server.d.ts +27 -5
  3. package/dist/deploy/dashboard-database.d.ts +38 -0
  4. package/dist/deploy/dashboard-database.test.d.ts +1 -0
  5. package/dist/deploy/dashboard-operations.d.ts +48 -0
  6. package/dist/deploy/dashboard-operations.test.d.ts +1 -0
  7. package/dist/deploy/index.d.ts +1 -0
  8. package/dist/deploy/local-dashboard-server.d.ts +31 -0
  9. package/dist/deploy/management-dashboard.d.ts +21 -1
  10. package/dist/deploy/site-config-editor.d.ts +37 -0
  11. package/dist/deploy/site-target.d.ts +3 -1
  12. package/dist/deploy/ssh-config-editor.d.ts +22 -0
  13. package/dist/deploy/ssh-config-editor.test.d.ts +1 -0
  14. package/dist/drivers/factory.d.ts +4 -0
  15. package/dist/drivers/index.d.ts +1 -0
  16. package/dist/drivers/local-box/driver.d.ts +20 -0
  17. package/dist/drivers/shared/compute-deploy.d.ts +2 -0
  18. package/dist/drivers/shared/deploy-script.d.ts +28 -12
  19. package/dist/drivers/shared/rpx-gateway.d.ts +99 -2
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +3373 -202
  22. package/dist/ui/index.html +83 -1
  23. package/dist/ui/server/actions.html +1193 -0
  24. package/dist/ui/server/activity.html +1132 -0
  25. package/dist/ui/server/backups.html +57 -1
  26. package/dist/ui/server/database.html +1208 -0
  27. package/dist/ui/server/deployments.html +79 -1
  28. package/dist/ui/server/diagnostics.html +1132 -0
  29. package/dist/ui/server/logs.html +1165 -0
  30. package/dist/ui/server/metrics.html +1 -1
  31. package/dist/ui/server/security.html +1132 -0
  32. package/dist/ui/server/services.html +57 -1
  33. package/dist/ui/server/sites.html +141 -1
  34. package/dist/ui/server/ssh-keys.html +58 -1
  35. package/dist/ui/server/workers.html +57 -1
  36. package/dist/ui/serverless/assets.html +1 -1
  37. package/dist/ui/serverless/cost.html +1 -1
  38. package/dist/ui/serverless/data.html +1 -1
  39. package/dist/ui/serverless/deployments.html +1 -1
  40. package/dist/ui/serverless/firewall.html +1 -1
  41. package/dist/ui/serverless/functions.html +1 -1
  42. package/dist/ui/serverless/logs.html +1165 -0
  43. package/dist/ui/serverless/metrics.html +1 -1
  44. package/dist/ui/serverless/queues.html +1 -1
  45. package/dist/ui/serverless/scheduler.html +1 -1
  46. package/dist/ui/serverless/secrets.html +1 -1
  47. package/dist/ui/serverless.html +1 -1
  48. package/dist/ui-src/package.json +5 -0
  49. package/dist/ui-src/pages/index.stx +308 -0
  50. package/dist/ui-src/pages/partials/head.stx +294 -0
  51. package/dist/ui-src/pages/partials/nav.stx +54 -0
  52. package/dist/ui-src/pages/partials/op-confirm.stx +7 -0
  53. package/dist/ui-src/pages/server/actions.stx +60 -0
  54. package/dist/ui-src/pages/server/activity.stx +55 -0
  55. package/dist/ui-src/pages/server/backups.stx +91 -0
  56. package/dist/ui-src/pages/server/database.stx +119 -0
  57. package/dist/ui-src/pages/server/deployments.stx +106 -0
  58. package/dist/ui-src/pages/server/diagnostics.stx +88 -0
  59. package/dist/ui-src/pages/server/logs.stx +84 -0
  60. package/dist/ui-src/pages/server/metrics.stx +183 -0
  61. package/dist/ui-src/pages/server/security.stx +127 -0
  62. package/dist/ui-src/pages/server/services.stx +82 -0
  63. package/dist/ui-src/pages/server/sites.stx +143 -0
  64. package/dist/ui-src/pages/server/ssh-keys.stx +93 -0
  65. package/dist/ui-src/pages/server/workers.stx +99 -0
  66. package/dist/ui-src/pages/serverless/assets.stx +66 -0
  67. package/dist/ui-src/pages/serverless/cost.stx +149 -0
  68. package/dist/ui-src/pages/serverless/data.stx +76 -0
  69. package/dist/ui-src/pages/serverless/deployments.stx +56 -0
  70. package/dist/ui-src/pages/serverless/firewall.stx +58 -0
  71. package/dist/ui-src/pages/serverless/functions.stx +118 -0
  72. package/dist/ui-src/pages/serverless/logs.stx +84 -0
  73. package/dist/ui-src/pages/serverless/metrics.stx +76 -0
  74. package/dist/ui-src/pages/serverless/queues.stx +81 -0
  75. package/dist/ui-src/pages/serverless/scheduler.stx +60 -0
  76. package/dist/ui-src/pages/serverless/secrets.stx +55 -0
  77. package/dist/ui-src/pages/serverless.stx +222 -0
  78. package/dist/ui-src/src/charts.ts +17333 -0
  79. package/package.json +3 -3
@@ -0,0 +1,93 @@
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 initialKeys = __d?.sshKeys ?? []
6
+ </script>
7
+ <script client>
8
+ // Reactive SSH key management — list + add + remove, via signals + directives.
9
+ // Seeded from build-time data (stx server→client bridge); load() reconciles
10
+ // against the live cloud config (same describeSshKeys shape).
11
+ const keys = state(initialKeys)
12
+ const name = state('')
13
+ const pub = state('')
14
+ const message = state('')
15
+ const messageKind = state('')
16
+ function setMessage(text, kind) { message.set(text); messageKind.set(kind || '') }
17
+ async function load() {
18
+ try {
19
+ const res = await fetch('/api/ssh-keys')
20
+ if (!res.ok) throw new Error('Could not read SSH keys.')
21
+ const data = await res.json()
22
+ keys.set(data.keys || [])
23
+ } catch (e) { setMessage((e && e.message) || 'Could not read SSH keys.', 'bad') }
24
+ }
25
+ async function addKey() {
26
+ setMessage('Saving...', '')
27
+ const res = await fetch('/api/ssh-keys', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: name(), publicKey: pub() }) })
28
+ const data = await res.json().catch(() => ({}))
29
+ if (!res.ok || data.ok === false) { setMessage(data.error || 'Could not add key.', 'bad'); return }
30
+ keys.set(data.keys || []); name.set(''); pub.set('')
31
+ setMessage('Saved to cloud config. Run cloud deploy to apply.', 'ok')
32
+ }
33
+ async function removeKey(n) {
34
+ setMessage('Removing...', '')
35
+ const res = await fetch('/api/ssh-keys', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: n }) })
36
+ const data = await res.json().catch(() => ({}))
37
+ if (!res.ok || data.ok === false) { setMessage(data.error || 'Could not remove key.', 'bad'); return }
38
+ keys.set(data.keys || [])
39
+ setMessage('Removed from cloud config. Run cloud deploy to apply.', 'ok')
40
+ }
41
+ onMount(() => { load() })
42
+ </script>
43
+ <!DOCTYPE html>
44
+ <html lang="en" data-theme="dark">
45
+ <head>@include('../partials/head', { title: 'ts-cloud — SSH Keys' })</head>
46
+ <body>
47
+ @include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
48
+ <div class="wrap">
49
+ <div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> SSH Keys</div>
50
+ <header class="page"><div><h1>SSH Keys</h1><p>Authorized keys for the box, stored declaratively and applied on the next deploy.</p></div></header>
51
+ <div class="section">
52
+ <h2>Add key</h2>
53
+ <div class="panel">
54
+ <form class="form-grid" @submit.prevent="addKey()">
55
+ <div class="field-block">
56
+ <label for="ssh-key-name">Name</label>
57
+ <input id="ssh-key-name" autocomplete="off" placeholder="chris@macbook" :value="name()" @input="name.set($event.target.value)" required>
58
+ </div>
59
+ <div class="field-block wide">
60
+ <label for="ssh-public-key">Public key</label>
61
+ <textarea id="ssh-public-key" rows="4" placeholder="ssh-ed25519 AAAA... chris@macbook" :value="pub()" @input="pub.set($event.target.value)" required></textarea>
62
+ </div>
63
+ <div class="form-actions">
64
+ <button class="btn" type="submit">Add key</button>
65
+ <span class="msg" :class="messageKind()" role="status">{{ message() }}</span>
66
+ </div>
67
+ </form>
68
+ </div>
69
+ </div>
70
+ <div class="section">
71
+ <h2>Configured keys</h2>
72
+ <div class="panel">
73
+ <table>
74
+ <thead><tr><th>Name</th><th>Type</th><th>Fingerprint</th><th>Added</th><th></th></tr></thead>
75
+ <tbody>
76
+ <template :for="k in keys()">
77
+ <tr>
78
+ <td><b>{{ k.name }}</b></td>
79
+ <td class="mono" style="color:var(--txt3)">{{ k.type }}</td>
80
+ <td class="mono">{{ k.fingerprint }}</td>
81
+ <td style="color:var(--txt3)">{{ k.added }}</td>
82
+ <td class="table-actions"><button type="button" class="btn danger sm" @click="removeKey(k.name)">Remove</button></td>
83
+ </tr>
84
+ </template>
85
+ </tbody>
86
+ </table>
87
+ <div class="compact empty" @show="keys().length === 0"><strong>No SSH keys configured</strong><span>Add a public key above to update <span class="mono">infrastructure.compute.sshKeys</span>.</span></div>
88
+ <p class="note">Keys are declarative: this page edits <span class="mono">infrastructure.compute.sshKeys</span> in cloud config. Run <span class="mono">cloud deploy</span> to apply changes to the box. Connect with <span class="mono">cloud server:ssh {{ server.name }}</span>.</p>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </body>
93
+ </html>
@@ -0,0 +1,99 @@
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 workers = __d?.workersDetail ?? __d?.workers ?? [
6
+ { name: 'default', processes: 2, connection: 'redis', status: 'running', processed: 128400, failed: 3 },
7
+ { name: 'emails', processes: 1, connection: 'redis', status: 'running', processed: 9210, failed: 1 },
8
+ ]
9
+ const scheduler = __d?.serverScheduler ?? __d?.scheduler ?? { enabled: true, lastRun: '36s ago' }
10
+ const cron = __d?.cron ?? (__d ? [] : [
11
+ { schedule: '* * * * *', command: 'php artisan schedule:run', desc: 'Laravel scheduler' },
12
+ { schedule: '0 3 * * *', command: 'backup:run', desc: 'Nightly backup' },
13
+ ])
14
+ function tone(s) { return s === 'running' ? 'ok' : (s === 'stopped' ? 'bad' : 'warn') }
15
+ function fmt(n) { n = Number(n ?? 0); return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k` : String(n) }
16
+ </script>
17
+ <script client>
18
+ const opOutput = state('')
19
+ const opShown = state(false)
20
+ const pending = state(null)
21
+ const typed = state('')
22
+ const confirmTok = derived(() => { const p = pending(); return p ? p.confirm : '' })
23
+ const confirmVerb = derived(() => { const p = pending(); return p ? p.verb : '' })
24
+ const canRun = derived(() => { const p = pending(); return !!p && typed() === p.confirm })
25
+ function askOp(operation, confirm, verb, to) { pending.set({ operation, confirm, verb, to: to || null }); typed.set('') }
26
+ function cancelOp() { pending.set(null) }
27
+ async function runOp() {
28
+ const p = pending()
29
+ if (!p || typed() !== p.confirm) return
30
+ pending.set(null); typed.set('')
31
+ opShown.set(true); opOutput.set('Running ' + p.operation + '...')
32
+ try {
33
+ const payload = p.to ? { operation: p.operation, confirm: p.confirm, to: p.to } : { operation: p.operation, confirm: p.confirm }
34
+ const res = await fetch('/api/server/operations/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
35
+ const body = await res.json()
36
+ opOutput.set((body.ok ? 'OK ' : 'FAILED ') + (body.command || p.operation) + '\n\n' + (body.stdout || '') + (body.stderr ? '\n' + body.stderr : '') + (body.error ? '\n' + body.error : ''))
37
+ } catch (e) { opOutput.set('FAILED ' + p.operation + '\n\n' + ((e && e.message) || e)) }
38
+ }
39
+ </script>
40
+ <!DOCTYPE html>
41
+ <html lang="en" data-theme="dark">
42
+ <head>@include('../partials/head', { title: 'ts-cloud — Workers' })</head>
43
+ <body>
44
+ @include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
45
+ <div class="wrap">
46
+ <div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Workers &amp; Scheduler</div>
47
+ <header class="page"><div><h1>Workers &amp; Scheduler</h1><p>Queue workers (supervisor/systemd) and the cron-driven scheduler.</p></div></header>
48
+ <div class="section">
49
+ <h2>Queue workers</h2>
50
+ <div class="panel">
51
+ @if (workers.length)
52
+ <table>
53
+ <thead><tr><th>Queue</th><th>Processes</th><th>Connection</th><th>Processed</th><th>Failed</th><th>Status</th><th>Restart</th></tr></thead>
54
+ <tbody>
55
+ @foreach (workers as w)
56
+ <tr>
57
+ <td><b>{{ w.name }}</b></td>
58
+ <td>×{{ w.processes }}</td>
59
+ <td class="mono">{{ w.connection ?? 'configured' }}</td>
60
+ <td style="color:var(--txt2)">{{ fmt(w.processed) }}</td>
61
+ <td>@if ((w.failed ?? 0) > 0)<span class="bad pill">{{ w.failed }}</span>@else<span style="color:var(--txt3)">0</span>@endif</td>
62
+ <td><span class="pill {{ tone(w.status) }}">{{ w.status }}</span></td>
63
+ <td class="table-actions"><button class="btn ghost sm" type="button" @click="askOp('worker:restart:{{ w.site ?? w.name }}', '{{ w.site ?? w.name }}', 'restart workers for')">Restart</button></td>
64
+ </tr>
65
+ @endforeach
66
+ </tbody>
67
+ </table>
68
+ @else
69
+ <div class="empty"><strong>No queue workers</strong><span>Add worker definitions in your site config or with the worker command.</span></div>
70
+ @endif
71
+ @include('../partials/op-confirm')
72
+ <p class="note">Restart gracefully signals <span class="mono">queue:restart</span> so workers finish the current job, then pick up the latest code.</p>
73
+ </div>
74
+ </div>
75
+ <div class="cols section">
76
+ <div>
77
+ <h2>Scheduler</h2>
78
+ <div class="panel">
79
+ <div class="kv"><span>state</span> @if (scheduler.enabled)<span class="ok pill">on</span>@else<span class="pill warn">off</span>@endif</div>
80
+ <div class="kv"><span>last run</span> <span style="color:var(--txt2)">{{ scheduler.lastRun }}</span></div>
81
+ </div>
82
+ </div>
83
+ <div>
84
+ <h2>Cron jobs</h2>
85
+ <div class="panel">
86
+ @if (cron.length)
87
+ @foreach (cron as c)
88
+ <div class="kv"><span class="mono">{{ c.schedule }}</span> <b class="mono" style="font-size:12px">{{ c.command }}</b></div>
89
+ @endforeach
90
+ @else
91
+ <div class="compact empty"><strong>No cron jobs</strong><span>Cron definitions have not been discovered for this server.</span></div>
92
+ @endif
93
+ <p class="note">Manage with <span class="mono">cloud server:cron:add {{ server.name }} &lt;schedule&gt; &lt;command&gt;</span>.</p>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </body>
99
+ </html>
@@ -0,0 +1,66 @@
1
+ <script server>
2
+ let __d = null
3
+ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
4
+ const app = __d?.app ?? { name: 'acme', env: 'production' }
5
+ const assets = __d?.assetsInfo ?? {
6
+ bucket: 'acme-production-assets', cdn: 'd2x1.cloudfront.net', customDomain: 'cdn.acme.com',
7
+ assetUrl: 'https://cdn.acme.com/a4d63d2', files: 412, sizeMb: 18.4, cacheHitPct: 98.7, build: 'a4d63d2',
8
+ }
9
+ const recent = __d?.assetsRecent ?? [
10
+ { path: '/a4d63d2/app.css', size: '123 KB', type: 'text/css' },
11
+ { path: '/a4d63d2/app.js', size: '456 KB', type: 'application/javascript' },
12
+ { path: '/a4d63d2/logo.svg', size: '4 KB', type: 'image/svg+xml' },
13
+ ]
14
+ </script>
15
+ <!DOCTYPE html>
16
+ <html lang="en" data-theme="dark">
17
+ <head>
18
+ @include('../partials/head', { title: 'ts-cloud — Assets & CDN' })
19
+ </head>
20
+ <body>
21
+ @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
22
+ <div class="wrap">
23
+ <div class="crumbs"><a href="/serverless">Serverless</a> <span class="sep">/</span> Assets &amp; CDN</div>
24
+ <header class="page">
25
+ <div>
26
+ <h1>Assets &amp; CDN</h1>
27
+ <p>Versioned static assets uploaded to S3 and served via CloudFront; <span class="mono">ASSET_URL</span> is injected into the app.</p>
28
+ </div>
29
+ <div class="header-actions">
30
+ <span class="cmd">cloud assets:invalidate</span>
31
+ </div>
32
+ </header>
33
+
34
+ <div class="grid cards">
35
+ <div class="panel"><h3>Files</h3><div class="stat">{{ assets.files }}</div></div>
36
+ <div class="panel"><h3>Size</h3><div class="stat">{{ assets.sizeMb }} <small>MB</small></div></div>
37
+ <div class="panel"><h3>Cache hit</h3><div class="stat">{{ assets.cacheHitPct }}%</div></div>
38
+ <div class="panel"><h3>Version</h3><div class="mono" style="margin-top:12px">{{ assets.build }}</div></div>
39
+ </div>
40
+
41
+ <div class="section">
42
+ <div class="panel">
43
+ <div class="kv"><span>bucket</span> <b class="mono">{{ assets.bucket }}</b></div>
44
+ <div class="kv"><span>CloudFront</span> <b class="mono">{{ assets.cdn }}</b></div>
45
+ <div class="kv"><span>custom domain</span> <b class="mono">{{ assets.customDomain }}</b></div>
46
+ <div class="kv"><span>ASSET_URL</span> <b class="mono" style="font-size:12px">{{ assets.assetUrl }}</b></div>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="section">
51
+ <h2>Recent assets</h2>
52
+ <div class="panel">
53
+ <table>
54
+ <thead><tr><th>Path</th><th>Type</th><th>Size</th></tr></thead>
55
+ <tbody>
56
+ @foreach (recent as a)
57
+ <tr><td class="mono">{{ a.path }}</td><td style="color:var(--txt2)">{{ a.type }}</td><td>{{ a.size }}</td></tr>
58
+ @endforeach
59
+ </tbody>
60
+ </table>
61
+ <p class="note">Sync with <span class="mono">cloud assets:deploy</span> · invalidate with <span class="mono">cloud assets:invalidate</span></p>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </body>
66
+ </html>
@@ -0,0 +1,149 @@
1
+ <script server>
2
+ // Cost analysis drill-down. Sample data renders the design; the dashboard build injects
3
+ // real figures from Cost Explorer + per-resource estimates when present.
4
+ let __d = null
5
+ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
6
+ const app = __d?.app ?? { name: 'acme', env: 'production' }
7
+ const period = { label: 'Jun 2026 (month-to-date)', days: 17, ofDays: 30 }
8
+
9
+ const summary = __d?.costSummary ?? {
10
+ monthToDateUsd: 126.40,
11
+ projectedUsd: 223.10,
12
+ lastMonthUsd: 198.75,
13
+ dailyAvgUsd: 7.44,
14
+ }
15
+
16
+ // Cost by AWS service (month-to-date), largest first.
17
+ const services = __d?.costServices ?? [
18
+ { name: 'Lambda (compute)', usd: 41.20, note: 'GB-seconds across http/queue/cli' },
19
+ { name: 'Provisioned concurrency', usd: 28.60, note: '2 envs always-warm' },
20
+ { name: 'Aurora Serverless v2', usd: 19.80, note: '0.5–4 ACU' },
21
+ { name: 'API Gateway (HTTP)', usd: 11.30, note: '4.7M requests' },
22
+ { name: 'CloudFront + S3 assets', usd: 8.10, note: 'egress + storage' },
23
+ { name: 'ElastiCache (redis)', usd: 6.40, note: 'cache.t4g.micro' },
24
+ { name: 'DynamoDB (cache)', usd: 4.20, note: 'on-demand' },
25
+ { name: 'SQS', usd: 2.90, note: '12.8M messages' },
26
+ { name: 'RDS Proxy', usd: 2.10, note: 'proxy hours' },
27
+ { name: 'Secrets Manager', usd: 1.20, note: '4 secrets' },
28
+ { name: 'WAF', usd: 0.60, note: 'web ACL + rules' },
29
+ ]
30
+
31
+ // Per-function Lambda compute split.
32
+ const perFn = __d?.costPerFn ?? [
33
+ { key: 'http', requests: 171200, gbSec: 168200, usd: 31.40 },
34
+ { key: 'queue', requests: 12840, gbSec: 41200, usd: 7.80 },
35
+ { key: 'cli', requests: 1440, gbSec: 11600, usd: 2.00 },
36
+ ]
37
+
38
+ // Daily spend trend (most recent last).
39
+ const trend = __d?.costTrend ?? [5.9, 6.2, 7.1, 6.8, 7.4, 8.0, 7.6, 9.1, 8.4, 7.0, 6.4, 7.8, 8.9, 9.3, 8.1, 7.4, 8.2]
40
+
41
+ const totalServices = services.reduce((s, x) => s + x.usd, 0)
42
+ function pct(v, max) { return Math.round((v / max) * 100) }
43
+ const maxService = Math.max(...services.map(s => s.usd))
44
+ const maxTrend = Math.max(...trend)
45
+ </script>
46
+ <!DOCTYPE html>
47
+ <html lang="en" data-theme="dark">
48
+ <head>
49
+ @include('../partials/head', { title: 'ts-cloud — Cost' })
50
+ </head>
51
+ <body>
52
+ @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
53
+ <div class="wrap">
54
+ <div class="crumbs"><a href="/serverless">Serverless</a> <span class="sep">/</span> Cost</div>
55
+ <header class="page">
56
+ <div>
57
+ <h1>Cost analysis</h1>
58
+ <p>Spend by service and function — {{ period.label }}.</p>
59
+ </div>
60
+ </header>
61
+
62
+ <div class="grid cards">
63
+ <div class="panel">
64
+ <h3>Month to date</h3>
65
+ <div class="stat">${{ summary.monthToDateUsd.toFixed(2) }}</div>
66
+ <div class="bar"><i style="width: {{ pct(period.days, period.ofDays) }}%"></i></div>
67
+ <p class="note">day {{ period.days }} of {{ period.ofDays }}</p>
68
+ </div>
69
+ <div class="panel">
70
+ <h3>Projected (month)</h3>
71
+ <div class="stat">${{ summary.projectedUsd.toFixed(2) }}</div>
72
+ @if (summary.projectedUsd > summary.lastMonthUsd)
73
+ <p class="note" style="color:var(--warn)">▲ ${{ (summary.projectedUsd - summary.lastMonthUsd).toFixed(2) }} vs last month</p>
74
+ @else
75
+ <p class="note" style="color:var(--ok)">▼ ${{ (summary.lastMonthUsd - summary.projectedUsd).toFixed(2) }} vs last month</p>
76
+ @endif
77
+ </div>
78
+ <div class="panel">
79
+ <h3>Daily average</h3>
80
+ <div class="stat">${{ summary.dailyAvgUsd.toFixed(2) }}</div>
81
+ <p class="note">per day this month</p>
82
+ </div>
83
+ <div class="panel">
84
+ <h3>Last month</h3>
85
+ <div class="stat">${{ summary.lastMonthUsd.toFixed(2) }}</div>
86
+ <p class="note">final total</p>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="section">
91
+ <h2>Daily spend</h2>
92
+ <div class="panel">
93
+ <div class="spark" style="height:96px">
94
+ @foreach (trend as d)
95
+ <i style="height: {{ pct(d, maxTrend) }}%"></i>
96
+ @endforeach
97
+ </div>
98
+ <p class="note">USD/day · {{ period.label }}</p>
99
+ </div>
100
+ </div>
101
+
102
+ <div class="cols section">
103
+ <div>
104
+ <h2>By service</h2>
105
+ <div class="panel">
106
+ @foreach (services as s)
107
+ <div style="padding:11px 0; border-top:1px solid rgba(255,255,255,0.05);">
108
+ <div style="display:flex; align-items:center; gap:12px;">
109
+ <b style="min-width:200px">{{ s.name }}</b>
110
+ <span class="detail" style="color:var(--txt3); font-size:12px;">{{ s.note }}</span>
111
+ <span style="margin-left:auto; font-weight:700;">${{ s.usd.toFixed(2) }}</span>
112
+ </div>
113
+ <div class="bar" style="margin-top:8px;"><i style="width: {{ pct(s.usd, maxService) }}%"></i></div>
114
+ </div>
115
+ @endforeach
116
+ <div class="kv" style="margin-top:6px; border-top:1px solid var(--panel-br); padding-top:12px;"><span>total</span> <b>${{ totalServices.toFixed(2) }}</b></div>
117
+ </div>
118
+ </div>
119
+ <div>
120
+ <h2>Lambda by function</h2>
121
+ <div class="panel">
122
+ <table>
123
+ <thead><tr><th>Fn</th><th>Requests</th><th>GB-sec</th><th>Cost</th></tr></thead>
124
+ <tbody>
125
+ @foreach (perFn as f)
126
+ <tr>
127
+ <td><span class="tag">{{ f.key }}</span></td>
128
+ <td>{{ (f.requests / 1000).toFixed(f.requests >= 10000 ? 0 : 1) }}k</td>
129
+ <td class="mono">{{ (f.gbSec / 1000).toFixed(0) }}k</td>
130
+ <td style="font-weight:700">${{ f.usd.toFixed(2) }}</td>
131
+ </tr>
132
+ @endforeach
133
+ </tbody>
134
+ </table>
135
+ </div>
136
+ <div style="height:16px"></div>
137
+ <h2>Optimize</h2>
138
+ <div class="panel">
139
+ <p class="note" style="margin-top:0">Biggest lever: <b style="color:var(--txt)">{{ services[0].name }}</b> at ${{ services[0].usd.toFixed(2) }}.</p>
140
+ <p class="note">• Right-size memory — over-provisioned MB inflates GB-seconds.</p>
141
+ <p class="note">• Drop unused <span class="mono">provisionedConcurrency</span> for ping-based <span class="mono">warm</span> if cold starts are tolerable.</p>
142
+ <p class="note">• Tighten Aurora <span class="mono">db-scale</span> min ACU off-peak.</p>
143
+ <p class="note">Full breakdown: <span class="mono">cloud cost --env {{ app.env }}</span></p>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </body>
149
+ </html>
@@ -0,0 +1,76 @@
1
+ <script server>
2
+ let __d = null
3
+ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
4
+ const app = __d?.app ?? { name: 'acme', env: 'production' }
5
+ const aurora = __d?.aurora ?? { id: 'acme-production-db', engine: 'aurora-mysql', minAcu: 0.5, maxAcu: 4, currentAcu: 1.5, database: 'app', connections: 12, status: 'available' }
6
+ const proxy = __d?.proxy ?? { name: 'acme-production-proxy', endpoint: 'acme-production-proxy.proxy-xxxx.us-east-1.rds.amazonaws.com', pooledConns: 8, status: 'available' }
7
+ const redis = __d?.redis ?? { id: 'acme-production-redis', node: 'cache.t4g.micro', engine: 'redis 7.1', hitRate: 96.4, status: 'available' }
8
+ const efs = __d?.efs ?? { id: 'fs-0abc', mount: '/mnt/local', sizeMb: 312, status: 'available' }
9
+ function tone(s) { return s === 'available' ? 'ok' : 'warn' }
10
+ </script>
11
+ <!DOCTYPE html>
12
+ <html lang="en" data-theme="dark">
13
+ <head>
14
+ @include('../partials/head', { title: 'ts-cloud — Data' })
15
+ </head>
16
+ <body>
17
+ @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
18
+ <div class="wrap">
19
+ <div class="crumbs"><a href="/serverless">Serverless</a> <span class="sep">/</span> Data</div>
20
+ <header class="page">
21
+ <div>
22
+ <h1>Data services</h1>
23
+ <p>Managed data attached to the functions over a private VPC — database, connection proxy, cache, and shared filesystem.</p>
24
+ </div>
25
+ </header>
26
+
27
+ <div class="section">
28
+ <h2>Aurora Serverless v2 <span class="more"><span class="pill {{ tone(aurora.status) }}">{{ aurora.status }}</span></span></h2>
29
+ <div class="cols">
30
+ <div class="panel">
31
+ <div class="kv"><span>cluster</span> <b class="mono">{{ aurora.id }}</b></div>
32
+ <div class="kv"><span>engine</span> <b class="mono">{{ aurora.engine }}</b></div>
33
+ <div class="kv"><span>database</span> <b class="mono">{{ aurora.database }}</b></div>
34
+ <div class="kv"><span>active connections</span> <b>{{ aurora.connections }}</b></div>
35
+ </div>
36
+ <div class="panel">
37
+ <h3>Capacity (ACU)</h3>
38
+ <div class="stat">{{ aurora.currentAcu }} <small>now</small></div>
39
+ <div class="bar"><i style="width: {{ Math.round((aurora.currentAcu / aurora.maxAcu) * 100) }}%"></i></div>
40
+ <p class="note">range {{ aurora.minAcu }}–{{ aurora.maxAcu }} ACU · rescale with <span class="mono">cloud serverless:db-scale {{ aurora.minAcu }} {{ aurora.maxAcu }}</span></p>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="cols section">
46
+ <div>
47
+ <h2>RDS Proxy</h2>
48
+ <div class="panel">
49
+ <div class="kv"><span>name</span> <b class="mono">{{ proxy.name }}</b></div>
50
+ <div class="kv"><span>endpoint</span> <b class="mono" style="font-size:11px">{{ proxy.endpoint }}</b></div>
51
+ <div class="kv"><span>pooled connections</span> <b>{{ proxy.pooledConns }}</b></div>
52
+ <div class="kv"><span>status</span> <b><span class="pill {{ tone(proxy.status) }}">{{ proxy.status }}</span></b></div>
53
+ </div>
54
+ <div style="height:16px"></div>
55
+ <h2>Shared filesystem (EFS)</h2>
56
+ <div class="panel">
57
+ <div class="kv"><span>file system</span> <b class="mono">{{ efs.id }}</b></div>
58
+ <div class="kv"><span>mount</span> <b class="mono">{{ efs.mount }}</b></div>
59
+ <div class="kv"><span>used</span> <b>{{ efs.sizeMb }} MB</b></div>
60
+ </div>
61
+ </div>
62
+ <div>
63
+ <h2>ElastiCache (Redis)</h2>
64
+ <div class="panel">
65
+ <div class="kv"><span>cluster</span> <b class="mono">{{ redis.id }}</b></div>
66
+ <div class="kv"><span>node</span> <b class="mono">{{ redis.node }}</b></div>
67
+ <div class="kv"><span>engine</span> <b class="mono">{{ redis.engine }}</b></div>
68
+ <div class="kv"><span>hit rate</span> <b>{{ redis.hitRate }}%</b></div>
69
+ <div class="kv"><span>status</span> <b><span class="pill {{ tone(redis.status) }}">{{ redis.status }}</span></b></div>
70
+ </div>
71
+ <p class="note">Query the private DB without a bastion: <span class="mono">cloud serverless:db-shell "select 1"</span></p>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </body>
76
+ </html>
@@ -0,0 +1,56 @@
1
+ <script server>
2
+ let __d = null
3
+ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
4
+ const app = __d?.app ?? { name: 'acme', env: 'production' }
5
+ const deployments = __d?.deploymentsDetail ?? [
6
+ { sha: 'a4d63d2', status: 'success', when: '12m ago', took: '41s', by: 'chris', version: 7, hooks: ['composer install --no-dev', 'config:cache', 'route:cache', 'migrate --force'] },
7
+ { sha: '9557430', status: 'success', when: '2h ago', took: '38s', by: 'ci', version: 6, hooks: ['config:cache', 'route:cache', 'migrate --force'] },
8
+ { sha: 'c38fc09', status: 'success', when: '1d ago', took: '44s', by: 'ci', version: 5, hooks: ['migrate --force'] },
9
+ { sha: '5a2b284', status: 'rolled-back', when: '1d ago', took: '12s', by: 'chris', version: 4, hooks: [] },
10
+ ]
11
+ function tone(s) { return s === 'success' ? 'ok' : (s === 'rolled-back' ? 'warn' : 'bad') }
12
+ </script>
13
+ <!DOCTYPE html>
14
+ <html lang="en" data-theme="dark">
15
+ <head>
16
+ @include('../partials/head', { title: 'ts-cloud — Deployments' })
17
+ </head>
18
+ <body>
19
+ @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
20
+ <div class="wrap">
21
+ <div class="crumbs"><a href="/serverless">Serverless</a> <span class="sep">/</span> Deployments</div>
22
+ <header class="page">
23
+ <div>
24
+ <h1>Deployments</h1>
25
+ <p>Every deploy publishes an immutable artifact; activation is atomic and rollback flips back instantly.</p>
26
+ </div>
27
+ <div class="cmds header-actions">
28
+ <span class="cmd">cloud serverless:rollback</span>
29
+ <span class="accent cmd">cloud deploy:serverless</span>
30
+ </div>
31
+ </header>
32
+
33
+ <div class="section">
34
+ <div class="panel">
35
+ <table>
36
+ <thead><tr><th>Build</th><th>Status</th><th>Version</th><th>Deploy hooks</th><th>By</th><th>Took</th><th>When</th></tr></thead>
37
+ <tbody>
38
+ @foreach (deployments as d)
39
+ <tr>
40
+ <td class="mono">{{ d.sha }}</td>
41
+ <td><span class="pill {{ tone(d.status) }}">{{ d.status }}</span></td>
42
+ <td class="mono">v{{ d.version }}</td>
43
+ <td class="mono" style="font-size:11px;color:var(--txt3)">@if (d.hooks.length === 0)—@else{{ d.hooks.join(' · ') }}@endif</td>
44
+ <td style="color:var(--txt2)">{{ d.by }}</td>
45
+ <td class="mono">{{ d.took }}</td>
46
+ <td style="color:var(--txt3)">{{ d.when }}</td>
47
+ </tr>
48
+ @endforeach
49
+ </tbody>
50
+ </table>
51
+ <p class="note">Re-activate the last build without rebuilding: <span class="mono">cloud deploy:serverless --redeploy</span></p>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </body>
56
+ </html>
@@ -0,0 +1,58 @@
1
+ <script server>
2
+ let __d = null
3
+ try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
4
+ const app = __d?.app ?? { name: 'acme', env: 'production' }
5
+ const waf = { ...{ enabled: true, scope: 'REGIONAL', acl: 'acme-production-waf', allowed24h: 183994, blocked24h: 318 }, ...(__d?.waf ?? {}) }
6
+ const rules = __d?.wafRules ?? [
7
+ { name: 'rate-limit', detail: '2000 req / 5 min / IP', action: 'block', blocked24h: 211 },
8
+ { name: 'common (AWSManagedRulesCommonRuleSet)', detail: 'OWASP common', action: 'block', blocked24h: 64 },
9
+ { name: 'sqlInjection (AWSManagedRulesSQLiRuleSet)', detail: 'SQLi signatures', action: 'block', blocked24h: 43 },
10
+ ]
11
+ function fmt(n) { return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k` : String(n) }
12
+ </script>
13
+ <!DOCTYPE html>
14
+ <html lang="en" data-theme="dark">
15
+ <head>
16
+ @include('../partials/head', { title: 'ts-cloud — Firewall' })
17
+ </head>
18
+ <body>
19
+ @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
20
+ <div class="wrap">
21
+ <div class="crumbs"><a href="/serverless">Serverless</a> <span class="sep">/</span> Firewall</div>
22
+ <header class="page">
23
+ <div>
24
+ <h1>Firewall (WAF)</h1>
25
+ <p>An AWS WAF web ACL fronting the HTTP API.</p>
26
+ </div>
27
+ <div class="header-actions">@if (waf.enabled)<span class="ok pill">on</span>@else<span class="pill warn">off</span>@endif</div>
28
+ </header>
29
+
30
+ <div class="grid cards">
31
+ <div class="panel"><h3>Allowed (24h)</h3><div class="stat">{{ fmt(waf.allowed24h) }}</div></div>
32
+ <div class="panel"><h3>Blocked (24h)</h3><div class="stat" style="color:var(--bad)">{{ fmt(waf.blocked24h) }}</div></div>
33
+ <div class="panel"><h3>Scope</h3><div class="stat" style="font-size:20px">{{ waf.scope }}</div></div>
34
+ <div class="panel"><h3>Web ACL</h3><div class="mono" style="margin-top:12px;font-size:12px">{{ waf.acl }}</div></div>
35
+ </div>
36
+
37
+ <div class="section">
38
+ <h2>Rules</h2>
39
+ <div class="panel">
40
+ <table>
41
+ <thead><tr><th>Rule</th><th>Detail</th><th>Action</th><th>Blocked (24h)</th></tr></thead>
42
+ <tbody>
43
+ @foreach (rules as r)
44
+ <tr>
45
+ <td class="mono">{{ r.name }}</td>
46
+ <td style="color:var(--txt2)">{{ r.detail }}</td>
47
+ <td><span class="bad pill">{{ r.action }}</span></td>
48
+ <td>{{ fmt(r.blocked24h) }}</td>
49
+ </tr>
50
+ @endforeach
51
+ </tbody>
52
+ </table>
53
+ <p class="note">Configured via <span class="mono">app.firewall</span> in cloud.config.</p>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </body>
58
+ </html>