@stacksjs/ts-cloud 0.5.34 → 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.
Files changed (77) hide show
  1. package/dist/aws/cloudwatch.d.ts +2 -0
  2. package/dist/aws/xray.d.ts +45 -0
  3. package/dist/bin/cli.js +723 -701
  4. package/dist/deploy/dashboard-database.d.ts +25 -0
  5. package/dist/deploy/dashboard-operations.d.ts +7 -0
  6. package/dist/deploy/firewall-config-editor.d.ts +15 -0
  7. package/dist/deploy/firewall-config-editor.test.d.ts +1 -0
  8. package/dist/deploy/index.d.ts +1 -0
  9. package/dist/deploy/management-dashboard.d.ts +26 -3
  10. package/dist/deploy/serverless-operations.d.ts +150 -0
  11. package/dist/deploy/serverless-operations.test.d.ts +1 -0
  12. package/dist/deploy/site-config-editor.d.ts +6 -0
  13. package/dist/deploy/ssh-config-editor.d.ts +1 -0
  14. package/dist/deploy/terminal-session.d.ts +23 -0
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +1008 -33
  17. package/dist/ui/index.html +4 -4
  18. package/dist/ui/server/actions.html +34 -5
  19. package/dist/ui/server/activity.html +1 -1
  20. package/dist/ui/server/backups.html +4 -4
  21. package/dist/ui/server/database.html +27 -5
  22. package/dist/ui/server/deployments.html +4 -4
  23. package/dist/ui/server/diagnostics.html +1 -1
  24. package/dist/ui/server/firewall.html +1233 -0
  25. package/dist/ui/server/logs.html +4 -4
  26. package/dist/ui/server/metrics.html +1 -1
  27. package/dist/ui/server/security.html +1 -1
  28. package/dist/ui/server/services.html +4 -4
  29. package/dist/ui/server/sites.html +24 -5
  30. package/dist/ui/server/ssh-keys.html +4 -4
  31. package/dist/ui/server/terminal.html +1191 -0
  32. package/dist/ui/server/workers.html +4 -4
  33. package/dist/ui/serverless/alarms.html +1236 -0
  34. package/dist/ui/serverless/assets.html +53 -1
  35. package/dist/ui/serverless/cost.html +1 -1
  36. package/dist/ui/serverless/data.html +58 -1
  37. package/dist/ui/serverless/deployments.html +53 -1
  38. package/dist/ui/serverless/firewall.html +1 -1
  39. package/dist/ui/serverless/functions.html +63 -1
  40. package/dist/ui/serverless/logs.html +4 -4
  41. package/dist/ui/serverless/metrics.html +1 -1
  42. package/dist/ui/serverless/queues.html +94 -1
  43. package/dist/ui/serverless/scheduler.html +55 -1
  44. package/dist/ui/serverless/secrets.html +99 -1
  45. package/dist/ui/serverless/traces.html +1177 -0
  46. package/dist/ui/serverless.html +87 -1
  47. package/dist/ui-src/pages/index.stx +1 -1
  48. package/dist/ui-src/pages/partials/head.stx +6 -6
  49. package/dist/ui-src/pages/partials/nav.stx +4 -3
  50. package/dist/ui-src/pages/server/actions.stx +34 -1
  51. package/dist/ui-src/pages/server/backups.stx +1 -1
  52. package/dist/ui-src/pages/server/database.stx +32 -9
  53. package/dist/ui-src/pages/server/deployments.stx +2 -2
  54. package/dist/ui-src/pages/server/firewall.stx +134 -0
  55. package/dist/ui-src/pages/server/logs.stx +2 -2
  56. package/dist/ui-src/pages/server/metrics.stx +5 -5
  57. package/dist/ui-src/pages/server/security.stx +1 -1
  58. package/dist/ui-src/pages/server/services.stx +1 -1
  59. package/dist/ui-src/pages/server/sites.stx +24 -3
  60. package/dist/ui-src/pages/server/ssh-keys.stx +2 -2
  61. package/dist/ui-src/pages/server/terminal.stx +65 -0
  62. package/dist/ui-src/pages/server/workers.stx +1 -1
  63. package/dist/ui-src/pages/serverless/alarms.stx +153 -0
  64. package/dist/ui-src/pages/serverless/assets.stx +29 -2
  65. package/dist/ui-src/pages/serverless/cost.stx +4 -4
  66. package/dist/ui-src/pages/serverless/data.stx +56 -3
  67. package/dist/ui-src/pages/serverless/deployments.stx +30 -6
  68. package/dist/ui-src/pages/serverless/firewall.stx +1 -1
  69. package/dist/ui-src/pages/serverless/functions.stx +62 -4
  70. package/dist/ui-src/pages/serverless/logs.stx +2 -2
  71. package/dist/ui-src/pages/serverless/metrics.stx +5 -1
  72. package/dist/ui-src/pages/serverless/queues.stx +90 -26
  73. package/dist/ui-src/pages/serverless/scheduler.stx +31 -2
  74. package/dist/ui-src/pages/serverless/secrets.stx +94 -14
  75. package/dist/ui-src/pages/serverless/traces.stx +88 -0
  76. package/dist/ui-src/pages/serverless.stx +87 -24
  77. package/package.json +3 -3
@@ -0,0 +1,153 @@
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
+ // Alarms + presets are fetched live on mount; these seed the first render.
6
+ const alarmsList = __d?.alarmsList ?? [
7
+ { AlarmName: 'acme-production-http-errors', StateValue: 'OK', MetricName: 'Errors', ComparisonOperator: 'GreaterThanThreshold', Threshold: 5, Namespace: 'AWS/Lambda' },
8
+ { AlarmName: 'acme-production-http-p95', StateValue: 'ALARM', MetricName: 'Duration', ComparisonOperator: 'GreaterThanThreshold', Threshold: 500, Namespace: 'AWS/Lambda' },
9
+ { AlarmName: 'acme-production-queue-throttles', StateValue: 'INSUFFICIENT_DATA', MetricName: 'Throttles', ComparisonOperator: 'GreaterThanThreshold', Threshold: 1, Namespace: 'AWS/Lambda' },
10
+ ]
11
+ const presetsList = __d?.alarmsPresets ?? [
12
+ { key: 'errors', label: 'Function errors', unit: 'count' },
13
+ { key: 'p95', label: 'p95 latency (ms)', unit: 'ms' },
14
+ { key: 'throttles', label: 'Throttles', unit: 'count' },
15
+ { key: 'concurrency', label: 'Concurrent executions', unit: 'count' },
16
+ ]
17
+ const initialAlarms = alarmsList
18
+ const initialPresets = presetsList
19
+ function tone(s) { return s === 'ALARM' ? 'bad' : (s === 'OK' ? 'ok' : 'warn') }
20
+ </script>
21
+ <script client>
22
+ // Alarms manager: list/create/delete CloudWatch alarms via /api/serverless/alarms.
23
+ const rows = state(initialAlarms)
24
+ const presets = state(initialPresets)
25
+ const preset = state(initialPresets[0] ? initialPresets[0].key : '')
26
+ const threshold = state('')
27
+ const msg = state('')
28
+ const msgOk = state(false)
29
+ const busy = state(false)
30
+
31
+ // Delete flow uses a typed-confirm (the alarm name) before firing.
32
+ const pendingDelete = state(null)
33
+ const typedDelete = state('')
34
+ const canDelete = derived(() => { const p = pendingDelete(); return !!p && typedDelete() === p })
35
+
36
+ function toneOf(s) { return s === 'ALARM' ? 'bad' : (s === 'OK' ? 'ok' : 'warn') }
37
+
38
+ async function refresh() {
39
+ try {
40
+ const r = await fetch('/api/serverless/alarms')
41
+ if (!r.ok) return
42
+ const x = await r.json()
43
+ if (Array.isArray(x.alarms)) rows.set(x.alarms)
44
+ if (Array.isArray(x.presets)) { presets.set(x.presets); if (!preset() && x.presets[0]) preset.set(x.presets[0].key) }
45
+ } catch {}
46
+ }
47
+ onMount(() => { refresh() })
48
+
49
+ async function createAlarm() {
50
+ const p = preset()
51
+ if (!p) { msg.set('Pick a preset.'); msgOk.set(false); return }
52
+ if (threshold() === '' || Number.isNaN(Number(threshold()))) { msg.set('Enter a threshold.'); msgOk.set(false); return }
53
+ busy.set(true); msg.set('Creating alarm...'); msgOk.set(false)
54
+ try {
55
+ const res = await fetch('/api/serverless/alarms', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ preset: p, threshold: Number(threshold()) }) })
56
+ const body = await res.json()
57
+ msg.set(body.ok ? (body.stdout || 'Alarm created.') : (body.error || 'Failed to create alarm.')); msgOk.set(!!body.ok)
58
+ if (body.ok) { threshold.set(''); refresh() }
59
+ } catch (e) { msg.set('Failed: ' + ((e && e.message) || e)); msgOk.set(false) }
60
+ finally { busy.set(false) }
61
+ }
62
+
63
+ function askDelete(name) { pendingDelete.set(name); typedDelete.set('') }
64
+ function cancelDelete() { pendingDelete.set(null); typedDelete.set('') }
65
+ async function confirmDelete() {
66
+ const name = pendingDelete()
67
+ if (!name || typedDelete() !== name) return
68
+ pendingDelete.set(null); typedDelete.set('')
69
+ msg.set('Deleting ' + name + '...'); msgOk.set(false)
70
+ try {
71
+ const res = await fetch('/api/serverless/alarms', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, confirm: name }) })
72
+ const body = await res.json()
73
+ msg.set(body.ok ? (body.stdout || 'Deleted.') : (body.error || 'Failed to delete.')); msgOk.set(!!body.ok)
74
+ if (body.ok) refresh()
75
+ } catch (e) { msg.set('Failed: ' + ((e && e.message) || e)); msgOk.set(false) }
76
+ }
77
+ </script>
78
+ <!DOCTYPE html>
79
+ <html lang="en" data-theme="dark">
80
+ <head>
81
+ @include('../partials/head', { title: 'ts-cloud · Alarms' })
82
+ </head>
83
+ <body>
84
+ @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
85
+ <div class="wrap">
86
+ <div class="crumbs"><a href="/serverless">Serverless</a> <span class="sep">/</span> Alarms</div>
87
+ <header class="page">
88
+ <div>
89
+ <h1>Alarms</h1>
90
+ <p>Arm CloudWatch alarms on function metrics so a paging error rate or latency spike notifies you before customers do.</p>
91
+ </div>
92
+ </header>
93
+
94
+ <div class="section">
95
+ <div class="panel">
96
+ <table>
97
+ <thead><tr><th>Alarm</th><th>Metric</th><th>Condition</th><th>State</th><th class="table-actions">Actions</th></tr></thead>
98
+ <tbody>
99
+ <template :for="a in rows()">
100
+ <tr>
101
+ <td class="mono"><b>{{ a.AlarmName }}</b></td>
102
+ <td class="mono" style="color:var(--txt3)">{{ a.MetricName }}</td>
103
+ <td class="mono" style="color:var(--txt2)">{{ a.ComparisonOperator }} {{ a.Threshold }}</td>
104
+ <td><span class="pill {{ toneOf(a.StateValue) }}">{{ a.StateValue }}</span></td>
105
+ <td class="table-actions">
106
+ <div class="row-actions" style="justify-content:flex-end">
107
+ <button class="btn danger sm" type="button" @click="askDelete(a.AlarmName)">Delete</button>
108
+ </div>
109
+ </td>
110
+ </tr>
111
+ </template>
112
+ <tr @show="rows().length === 0">
113
+ <td colspan="5"><div class="compact empty"><strong>No alarms armed</strong><span>Create one below to get notified when a function metric crosses a threshold.</span></div></td>
114
+ </tr>
115
+ </tbody>
116
+ </table>
117
+
118
+ <div class="op-confirm" @show="pendingDelete() !== null" style="margin-top:14px">
119
+ <span>Type <b class="mono">{{ pendingDelete() }}</b> to delete this alarm:</span>
120
+ <input class="op-confirm-input" :value="typedDelete()" @input="typedDelete.set($event.target.value)" @keydown.enter="confirmDelete()" placeholder="confirm" autocomplete="off">
121
+ <button class="btn danger sm" :disabled="!canDelete()" @click="confirmDelete()">Delete</button>
122
+ <button class="btn ghost sm" @click="cancelDelete()">Cancel</button>
123
+ </div>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="section">
128
+ <h2>Create alarm</h2>
129
+ <div class="panel">
130
+ <div class="form-grid">
131
+ <div class="field-block">
132
+ <label>Metric preset</label>
133
+ <select :value="preset()" @change="preset.set($event.target.value)">
134
+ <template :for="p in presets()">
135
+ <option value="{{ p.key }}">{{ p.label }}</option>
136
+ </template>
137
+ </select>
138
+ </div>
139
+ <div class="field-block">
140
+ <label>Threshold</label>
141
+ <input type="number" :value="threshold()" @input="threshold.set($event.target.value)" placeholder="5" autocomplete="off">
142
+ </div>
143
+ <div class="form-actions">
144
+ <button class="btn" type="button" :disabled="busy()" @click="createAlarm()">Create</button>
145
+ <span class="msg" :class="{ ok: msgOk(), bad: msg() && !msgOk() }">{{ msg() }}</span>
146
+ </div>
147
+ </div>
148
+ <p class="note">Alarms fire when the chosen metric crosses the threshold. Manage them live with <span class="mono">cloud alarms</span>.</p>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </body>
153
+ </html>
@@ -12,10 +12,33 @@ const recent = __d?.assetsRecent ?? [
12
12
  { path: '/a4d63d2/logo.svg', size: '4 KB', type: 'image/svg+xml' },
13
13
  ]
14
14
  </script>
15
+ <script client>
16
+ // assets:invalidate runs live against AWS through /api/serverless/operations/run.
17
+ const opOutput = state('')
18
+ const opShown = state(false)
19
+ const pending = state(null)
20
+ const typed = state('')
21
+ const confirmTok = derived(() => { const p = pending(); return p ? p.confirm : '' })
22
+ const confirmVerb = derived(() => { const p = pending(); return p ? p.verb : '' })
23
+ const canRun = derived(() => { const p = pending(); return !!p && typed() === p.confirm })
24
+ function askOp(operation, confirm, verb) { pending.set({ operation, confirm, verb }); typed.set('') }
25
+ function cancelOp() { pending.set(null) }
26
+ async function runOp() {
27
+ const p = pending()
28
+ if (!p || typed() !== p.confirm) return
29
+ pending.set(null); typed.set('')
30
+ opShown.set(true); opOutput.set('Running ' + p.operation + '...')
31
+ try {
32
+ const res = await fetch('/api/serverless/operations/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ operation: p.operation, confirm: p.confirm }) })
33
+ const body = await res.json()
34
+ opOutput.set((body.ok ? 'OK ' : 'FAILED ') + (body.command || p.operation) + '\n\n' + (body.stdout || '') + (body.error ? '\n' + body.error : ''))
35
+ } catch (e) { opOutput.set('FAILED ' + p.operation + '\n\n' + ((e && e.message) || e)) }
36
+ }
37
+ </script>
15
38
  <!DOCTYPE html>
16
39
  <html lang="en" data-theme="dark">
17
40
  <head>
18
- @include('../partials/head', { title: 'ts-cloud Assets & CDN' })
41
+ @include('../partials/head', { title: 'ts-cloud · Assets & CDN' })
19
42
  </head>
20
43
  <body>
21
44
  @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
@@ -27,10 +50,14 @@ const recent = __d?.assetsRecent ?? [
27
50
  <p>Versioned static assets uploaded to S3 and served via CloudFront; <span class="mono">ASSET_URL</span> is injected into the app.</p>
28
51
  </div>
29
52
  <div class="header-actions">
30
- <span class="cmd">cloud assets:invalidate</span>
53
+ <button class="btn sm" type="button" @click="askOp('assets:invalidate', 'invalidate', 'invalidate the CDN cache')">Invalidate CDN cache</button>
31
54
  </div>
32
55
  </header>
33
56
 
57
+ <div class="section">
58
+ @include('../partials/op-confirm')
59
+ </div>
60
+
34
61
  <div class="grid cards">
35
62
  <div class="panel"><h3>Files</h3><div class="stat">{{ assets.files }}</div></div>
36
63
  <div class="panel"><h3>Size</h3><div class="stat">{{ assets.sizeMb }} <small>MB</small></div></div>
@@ -17,7 +17,7 @@ const summary = __d?.costSummary ?? {
17
17
  const services = __d?.costServices ?? [
18
18
  { name: 'Lambda (compute)', usd: 41.20, note: 'GB-seconds across http/queue/cli' },
19
19
  { name: 'Provisioned concurrency', usd: 28.60, note: '2 envs always-warm' },
20
- { name: 'Aurora Serverless v2', usd: 19.80, note: '0.54 ACU' },
20
+ { name: 'Aurora Serverless v2', usd: 19.80, note: '0.5-4 ACU' },
21
21
  { name: 'API Gateway (HTTP)', usd: 11.30, note: '4.7M requests' },
22
22
  { name: 'CloudFront + S3 assets', usd: 8.10, note: 'egress + storage' },
23
23
  { name: 'ElastiCache (redis)', usd: 6.40, note: 'cache.t4g.micro' },
@@ -46,7 +46,7 @@ const maxTrend = Math.max(...trend)
46
46
  <!DOCTYPE html>
47
47
  <html lang="en" data-theme="dark">
48
48
  <head>
49
- @include('../partials/head', { title: 'ts-cloud Cost' })
49
+ @include('../partials/head', { title: 'ts-cloud - Cost' })
50
50
  </head>
51
51
  <body>
52
52
  @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
@@ -55,7 +55,7 @@ const maxTrend = Math.max(...trend)
55
55
  <header class="page">
56
56
  <div>
57
57
  <h1>Cost analysis</h1>
58
- <p>Spend by service and function {{ period.label }}.</p>
58
+ <p>Spend by service and function - {{ period.label }}.</p>
59
59
  </div>
60
60
  </header>
61
61
 
@@ -137,7 +137,7 @@ const maxTrend = Math.max(...trend)
137
137
  <h2>Optimize</h2>
138
138
  <div class="panel">
139
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>
140
+ <p class="note">• Right-size memory - over-provisioned MB inflates GB-seconds.</p>
141
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
142
  <p class="note">• Tighten Aurora <span class="mono">db-scale</span> min ACU off-peak.</p>
143
143
  <p class="note">Full breakdown: <span class="mono">cloud cost --env {{ app.env }}</span></p>
@@ -7,11 +7,39 @@ const proxy = __d?.proxy ?? { name: 'acme-production-proxy', endpoint: 'acme-pro
7
7
  const redis = __d?.redis ?? { id: 'acme-production-redis', node: 'cache.t4g.micro', engine: 'redis 7.1', hitRate: 96.4, status: 'available' }
8
8
  const efs = __d?.efs ?? { id: 'fs-0abc', mount: '/mnt/local', sizeMb: 312, status: 'available' }
9
9
  function tone(s) { return s === 'available' ? 'ok' : 'warn' }
10
+ const hasAurora = !!(__d?.aurora ?? true)
11
+ const seedMin = aurora?.minAcu ?? 0.5
12
+ const seedMax = aurora?.maxAcu ?? 4
13
+ </script>
14
+ <script client>
15
+ // db:scale runs live against AWS through /api/serverless/operations/run.
16
+ const opOutput = state('')
17
+ const opShown = state(false)
18
+ const pending = state(null)
19
+ const typed = state('')
20
+ const scaleMin = state(String(seedMin ?? ''))
21
+ const scaleMax = state(String(seedMax ?? ''))
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) { pending.set({ operation, confirm, verb }); 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 res = await fetch('/api/serverless/operations/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ operation: p.operation, confirm: p.confirm, min: Number(scaleMin()), max: Number(scaleMax()) }) })
34
+ const body = await res.json()
35
+ opOutput.set((body.ok ? 'OK ' : 'FAILED ') + (body.command || p.operation) + '\n\n' + (body.stdout || '') + (body.error ? '\n' + body.error : ''))
36
+ } catch (e) { opOutput.set('FAILED ' + p.operation + '\n\n' + ((e && e.message) || e)) }
37
+ }
10
38
  </script>
11
39
  <!DOCTYPE html>
12
40
  <html lang="en" data-theme="dark">
13
41
  <head>
14
- @include('../partials/head', { title: 'ts-cloud Data' })
42
+ @include('../partials/head', { title: 'ts-cloud · Data' })
15
43
  </head>
16
44
  <body>
17
45
  @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
@@ -20,7 +48,7 @@ function tone(s) { return s === 'available' ? 'ok' : 'warn' }
20
48
  <header class="page">
21
49
  <div>
22
50
  <h1>Data services</h1>
23
- <p>Managed data attached to the functions over a private VPC database, connection proxy, cache, and shared filesystem.</p>
51
+ <p>Managed data attached to the functions over a private VPC: database, connection proxy, cache, and shared filesystem.</p>
24
52
  </div>
25
53
  </header>
26
54
 
@@ -37,11 +65,36 @@ function tone(s) { return s === 'available' ? 'ok' : 'warn' }
37
65
  <h3>Capacity (ACU)</h3>
38
66
  <div class="stat">{{ aurora.currentAcu }} <small>now</small></div>
39
67
  <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>
68
+ <p class="note">range {{ aurora.minAcu }} to {{ aurora.maxAcu }} ACU · rescale with <span class="mono">cloud serverless:db-scale {{ aurora.minAcu }} {{ aurora.maxAcu }}</span></p>
41
69
  </div>
42
70
  </div>
43
71
  </div>
44
72
 
73
+ <div class="section">
74
+ <h2>Scale Aurora capacity</h2>
75
+ @if (hasAurora)
76
+ <div class="panel">
77
+ <div class="form-grid">
78
+ <div class="field-block">
79
+ <label>Min ACUs</label>
80
+ <input type="number" step="0.5" min="0.5" :value="scaleMin()" @input="scaleMin.set($event.target.value)" placeholder="{{ seedMin }}" autocomplete="off">
81
+ </div>
82
+ <div class="field-block">
83
+ <label>Max ACUs</label>
84
+ <input type="number" step="0.5" min="0.5" :value="scaleMax()" @input="scaleMax.set($event.target.value)" placeholder="{{ seedMax }}" autocomplete="off">
85
+ </div>
86
+ </div>
87
+ <div class="form-actions">
88
+ <button class="btn sm" type="button" @click="askOp('db:scale', 'scale', 'scale the Aurora cluster')">Scale</button>
89
+ </div>
90
+ @include('../partials/op-confirm')
91
+ <p class="note">Adjusts the Aurora Serverless v2 capacity range. New writes settle into the range within a few seconds.</p>
92
+ </div>
93
+ @else
94
+ <p class="note">Aurora Serverless is not attached to this app.</p>
95
+ @endif
96
+ </div>
97
+
45
98
  <div class="cols section">
46
99
  <div>
47
100
  <h2>RDS Proxy</h2>
@@ -10,10 +10,33 @@ const deployments = __d?.deploymentsDetail ?? [
10
10
  ]
11
11
  function tone(s) { return s === 'success' ? 'ok' : (s === 'rolled-back' ? 'warn' : 'bad') }
12
12
  </script>
13
+ <script client>
14
+ // Rollback + redeploy run live against AWS through /api/serverless/operations/run.
15
+ const opOutput = state('')
16
+ const opShown = state(false)
17
+ const pending = state(null)
18
+ const typed = state('')
19
+ const confirmTok = derived(() => { const p = pending(); return p ? p.confirm : '' })
20
+ const confirmVerb = derived(() => { const p = pending(); return p ? p.verb : '' })
21
+ const canRun = derived(() => { const p = pending(); return !!p && typed() === p.confirm })
22
+ function askOp(operation, confirm, verb) { pending.set({ operation, confirm, verb }); typed.set('') }
23
+ function cancelOp() { pending.set(null) }
24
+ async function runOp() {
25
+ const p = pending()
26
+ if (!p || typed() !== p.confirm) return
27
+ pending.set(null); typed.set('')
28
+ opShown.set(true); opOutput.set('Running ' + p.operation + '...')
29
+ try {
30
+ const res = await fetch('/api/serverless/operations/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ operation: p.operation, confirm: p.confirm }) })
31
+ const body = await res.json()
32
+ opOutput.set((body.ok ? 'OK ' : 'FAILED ') + (body.command || p.operation) + '\n\n' + (body.stdout || '') + (body.error ? '\n' + body.error : ''))
33
+ } catch (e) { opOutput.set('FAILED ' + p.operation + '\n\n' + ((e && e.message) || e)) }
34
+ }
35
+ </script>
13
36
  <!DOCTYPE html>
14
37
  <html lang="en" data-theme="dark">
15
38
  <head>
16
- @include('../partials/head', { title: 'ts-cloud Deployments' })
39
+ @include('../partials/head', { title: 'ts-cloud · Deployments' })
17
40
  </head>
18
41
  <body>
19
42
  @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
@@ -24,9 +47,9 @@ function tone(s) { return s === 'success' ? 'ok' : (s === 'rolled-back' ? 'warn'
24
47
  <h1>Deployments</h1>
25
48
  <p>Every deploy publishes an immutable artifact; activation is atomic and rollback flips back instantly.</p>
26
49
  </div>
27
- <div class="cmds header-actions">
28
- <span class="cmd">cloud serverless:rollback</span>
29
- <span class="accent cmd">cloud deploy:serverless</span>
50
+ <div class="header-actions">
51
+ <button class="btn danger sm" type="button" @click="askOp('rollback', 'rollback', 'roll back to the previous build')">Roll back</button>
52
+ <button class="btn sm" type="button" @click="askOp('redeploy', 'redeploy', 'redeploy the current build')">Redeploy</button>
30
53
  </div>
31
54
  </header>
32
55
 
@@ -40,7 +63,7 @@ function tone(s) { return s === 'success' ? 'ok' : (s === 'rolled-back' ? 'warn'
40
63
  <td class="mono">{{ d.sha }}</td>
41
64
  <td><span class="pill {{ tone(d.status) }}">{{ d.status }}</span></td>
42
65
  <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>
66
+ <td class="mono" style="font-size:11px;color:var(--txt3)">@if (d.hooks.length === 0)-@else{{ d.hooks.join(' · ') }}@endif</td>
44
67
  <td style="color:var(--txt2)">{{ d.by }}</td>
45
68
  <td class="mono">{{ d.took }}</td>
46
69
  <td style="color:var(--txt3)">{{ d.when }}</td>
@@ -48,7 +71,8 @@ function tone(s) { return s === 'success' ? 'ok' : (s === 'rolled-back' ? 'warn'
48
71
  @endforeach
49
72
  </tbody>
50
73
  </table>
51
- <p class="note">Re-activate the last build without rebuilding: <span class="mono">cloud deploy:serverless --redeploy</span></p>
74
+ @include('../partials/op-confirm')
75
+ <p class="note">Rollback restores the previous build's code and env and flips the live alias back instantly. Redeploy re-activates the current build without rebuilding.</p>
52
76
  </div>
53
77
  </div>
54
78
  </div>
@@ -13,7 +13,7 @@ function fmt(n) { return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k
13
13
  <!DOCTYPE html>
14
14
  <html lang="en" data-theme="dark">
15
15
  <head>
16
- @include('../partials/head', { title: 'ts-cloud Firewall' })
16
+ @include('../partials/head', { title: 'ts-cloud - Firewall' })
17
17
  </head>
18
18
  <body>
19
19
  @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
@@ -6,7 +6,7 @@ const app = __d?.app ?? { name: 'acme', env: 'production' }
6
6
  const functions = __d?.functionsDetail ?? [
7
7
  {
8
8
  key: 'http', name: 'acme-production-http', runtime: 'provided.al2023', arch: 'x86_64',
9
- memory: 1024, timeout: 28, ephemeral: 512, version: '$LATEST', concurrency: '',
9
+ memory: 1024, timeout: 28, ephemeral: 512, version: '$LATEST', concurrency: '-',
10
10
  invocations: 171200, errors: 21, throttles: 0, p50: 32, p95: 86, p99: 180, maxMem: 184,
11
11
  spark: [40, 52, 48, 61, 73, 66, 80, 72, 95, 88, 70, 64],
12
12
  env: ['APP_ENV', 'ASSET_URL', 'TSCLOUD_CACHE_TABLE', 'DB_HOST', 'QUEUE_CONNECTION'],
@@ -29,7 +29,7 @@ const functions = __d?.functionsDetail ?? [
29
29
  },
30
30
  {
31
31
  key: 'cli', name: 'acme-production-cli', runtime: 'provided.al2023', arch: 'x86_64',
32
- memory: 1024, timeout: 900, ephemeral: 512, version: '$LATEST', concurrency: '',
32
+ memory: 1024, timeout: 900, ephemeral: 512, version: '$LATEST', concurrency: '-',
33
33
  invocations: 1440, errors: 0, throttles: 0, p50: 800, p95: 1100, p99: 1400, maxMem: 312,
34
34
  spark: [2, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1],
35
35
  env: ['APP_ENV', 'TSCLOUD_SCHEDULER'],
@@ -41,11 +41,40 @@ const functions = __d?.functionsDetail ?? [
41
41
  ]
42
42
 
43
43
  function fmt(n) { return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k` : String(n) }
44
+ const modes = (__d?.functionsDetail ?? functions).map(f => f.key)
45
+ const initialMode = modes[0] ?? 'http'
46
+ </script>
47
+ <script client>
48
+ // Per-function config runs live against AWS through /api/serverless/functions/config.
49
+ const mode = state(initialMode)
50
+ const mem = state('1024')
51
+ const to = state('28')
52
+ const opOutput = state('')
53
+ const opShown = state(false)
54
+ const pending = state(null)
55
+ const typed = state('')
56
+ const confirmTok = derived(() => { const p = pending(); return p ? p.confirm : '' })
57
+ const confirmVerb = derived(() => { const p = pending(); return p ? p.verb : '' })
58
+ const canRun = derived(() => { const p = pending(); return !!p && typed() === p.confirm })
59
+ function askOp(operation, confirm, verb) { pending.set({ operation, confirm, verb }); typed.set('') }
60
+ function cancelOp() { pending.set(null) }
61
+ function applyConfig() { const m = mode(); askOp(m, m, 'update the ' + m + ' function') }
62
+ async function runOp() {
63
+ const p = pending()
64
+ if (!p || typed() !== p.confirm) return
65
+ pending.set(null); typed.set('')
66
+ opShown.set(true); opOutput.set('Updating ' + p.operation + '...')
67
+ try {
68
+ const res = await fetch('/api/serverless/functions/config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ mode: p.operation, memory: Number(mem()), timeout: Number(to()), confirm: p.confirm }) })
69
+ const body = await res.json()
70
+ opOutput.set((body.ok ? 'OK ' : 'FAILED ') + (body.command || p.operation) + '\n\n' + (body.stdout || '') + (body.error ? '\n' + body.error : ''))
71
+ } catch (e) { opOutput.set('FAILED ' + p.operation + '\n\n' + ((e && e.message) || e)) }
72
+ }
44
73
  </script>
45
74
  <!DOCTYPE html>
46
75
  <html lang="en" data-theme="dark">
47
76
  <head>
48
- @include('../partials/head', { title: 'ts-cloud Functions' })
77
+ @include('../partials/head', { title: 'ts-cloud · Functions' })
49
78
  </head>
50
79
  <body>
51
80
  @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
@@ -55,7 +84,7 @@ function fmt(n) { return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k
55
84
  <header class="page">
56
85
  <div>
57
86
  <h1>Functions</h1>
58
- <p>The three Lambda functions sharing one artifact HTTP, queue worker, and CLI/scheduler.</p>
87
+ <p>The three Lambda functions sharing one artifact - HTTP, queue worker, and CLI/scheduler.</p>
59
88
  </div>
60
89
  </header>
61
90
 
@@ -113,6 +142,35 @@ function fmt(n) { return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k
113
142
  </div>
114
143
  </div>
115
144
  @endforeach
145
+
146
+ <div class="section">
147
+ <h2>Configure function</h2>
148
+ <div class="panel">
149
+ <div class="form-grid">
150
+ <div class="field-block">
151
+ <label>Function</label>
152
+ <select :value="mode()" @change="mode.set($event.target.value)">
153
+ @foreach (modes as m)
154
+ <option value="{{ m }}">{{ m }}</option>
155
+ @endforeach
156
+ </select>
157
+ </div>
158
+ <div class="field-block">
159
+ <label>Memory (MB)</label>
160
+ <input type="number" min="128" max="10240" :value="mem()" @input="mem.set($event.target.value)" placeholder="1024" autocomplete="off">
161
+ </div>
162
+ <div class="field-block">
163
+ <label>Timeout (s)</label>
164
+ <input type="number" min="1" max="900" :value="to()" @input="to.set($event.target.value)" placeholder="28" autocomplete="off">
165
+ </div>
166
+ <div class="form-actions">
167
+ <button class="btn" type="button" @click="applyConfig()">Apply</button>
168
+ </div>
169
+ </div>
170
+ @include('../partials/op-confirm')
171
+ <p class="note">Memory is 128 to 10240 MB, timeout is 1 to 900 s. Changes are applied to the live function immediately via <span class="mono">cloud functions:config</span>.</p>
172
+ </div>
173
+ </div>
116
174
  </div>
117
175
  </body>
118
176
  </html>
@@ -7,7 +7,7 @@ const emptyReason = __d?.serverlessLogsEmptyReason ?? 'No function logs are avai
7
7
  </script>
8
8
  <script client>
9
9
  // Reactive aggregated CloudWatch logs across the functions, with level + function
10
- // filtering and auto-refresh seeded from build-time data (stx server→client
10
+ // filtering and auto-refresh - seeded from build-time data (stx server→client
11
11
  // bridge), then live-refreshed. Signals + directives only (no manual DOM).
12
12
  const logs = state(initialLogs)
13
13
  const level = state('all')
@@ -28,7 +28,7 @@ onMount(() => { refresh(); const id = setInterval(refresh, 8000); return () => c
28
28
  </script>
29
29
  <!DOCTYPE html>
30
30
  <html lang="en" data-theme="dark">
31
- <head>@include('../partials/head', { title: 'ts-cloud Function logs' })</head>
31
+ <head>@include('../partials/head', { title: 'ts-cloud - Function logs' })</head>
32
32
  <body>
33
33
  @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
34
34
  <div class="wrap">
@@ -16,7 +16,7 @@ function pct(v, max) { return Math.round((v / max) * 100) }
16
16
  <!DOCTYPE html>
17
17
  <html lang="en" data-theme="dark">
18
18
  <head>
19
- @include('../partials/head', { title: 'ts-cloud Metrics' })
19
+ @include('../partials/head', { title: 'ts-cloud · Metrics' })
20
20
  </head>
21
21
  <body>
22
22
  @include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
@@ -27,6 +27,10 @@ function pct(v, max) { return Math.round((v / max) * 100) }
27
27
  <h1>Metrics</h1>
28
28
  <p>CloudWatch metrics across the function trio, last {{ window }}.</p>
29
29
  </div>
30
+ <div class="header-actions">
31
+ <a class="btn ghost sm" href="/serverless/traces">Traces →</a>
32
+ <a class="btn ghost sm" href="/serverless/alarms">Alarms →</a>
33
+ </div>
30
34
  </header>
31
35
 
32
36
  <div class="grid cards">