@stacksjs/ts-cloud 0.5.35 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aws/cloudwatch.d.ts +2 -0
- package/dist/aws/xray.d.ts +45 -0
- package/dist/bin/cli.js +718 -701
- package/dist/deploy/dashboard-database.d.ts +25 -0
- package/dist/deploy/dashboard-operations.d.ts +7 -0
- package/dist/deploy/firewall-config-editor.d.ts +15 -0
- package/dist/deploy/firewall-config-editor.test.d.ts +1 -0
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/management-dashboard.d.ts +26 -3
- package/dist/deploy/serverless-operations.d.ts +150 -0
- package/dist/deploy/serverless-operations.test.d.ts +1 -0
- package/dist/deploy/site-config-editor.d.ts +6 -0
- package/dist/deploy/ssh-config-editor.d.ts +1 -0
- package/dist/deploy/terminal-session.d.ts +23 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1003 -33
- package/dist/ui/index.html +4 -4
- package/dist/ui/server/actions.html +34 -5
- package/dist/ui/server/activity.html +1 -1
- package/dist/ui/server/backups.html +4 -4
- package/dist/ui/server/database.html +27 -5
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +1 -1
- package/dist/ui/server/firewall.html +1233 -0
- package/dist/ui/server/logs.html +4 -4
- package/dist/ui/server/metrics.html +1 -1
- package/dist/ui/server/security.html +1 -1
- package/dist/ui/server/services.html +4 -4
- package/dist/ui/server/sites.html +24 -5
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/terminal.html +1191 -0
- package/dist/ui/server/workers.html +4 -4
- package/dist/ui/serverless/alarms.html +1236 -0
- package/dist/ui/serverless/assets.html +53 -1
- package/dist/ui/serverless/cost.html +1 -1
- package/dist/ui/serverless/data.html +58 -1
- package/dist/ui/serverless/deployments.html +53 -1
- package/dist/ui/serverless/firewall.html +1 -1
- package/dist/ui/serverless/functions.html +63 -1
- package/dist/ui/serverless/logs.html +4 -4
- package/dist/ui/serverless/metrics.html +1 -1
- package/dist/ui/serverless/queues.html +94 -1
- package/dist/ui/serverless/scheduler.html +55 -1
- package/dist/ui/serverless/secrets.html +99 -1
- package/dist/ui/serverless/traces.html +1177 -0
- package/dist/ui/serverless.html +87 -1
- package/dist/ui-src/pages/index.stx +1 -1
- package/dist/ui-src/pages/partials/head.stx +47 -35
- package/dist/ui-src/pages/partials/nav.stx +4 -3
- package/dist/ui-src/pages/server/actions.stx +34 -1
- package/dist/ui-src/pages/server/backups.stx +1 -1
- package/dist/ui-src/pages/server/database.stx +32 -9
- package/dist/ui-src/pages/server/deployments.stx +2 -2
- package/dist/ui-src/pages/server/firewall.stx +134 -0
- package/dist/ui-src/pages/server/logs.stx +2 -2
- package/dist/ui-src/pages/server/metrics.stx +5 -5
- package/dist/ui-src/pages/server/security.stx +1 -1
- package/dist/ui-src/pages/server/services.stx +1 -1
- package/dist/ui-src/pages/server/sites.stx +24 -3
- package/dist/ui-src/pages/server/ssh-keys.stx +2 -2
- package/dist/ui-src/pages/server/terminal.stx +65 -0
- package/dist/ui-src/pages/server/workers.stx +1 -1
- package/dist/ui-src/pages/serverless/alarms.stx +153 -0
- package/dist/ui-src/pages/serverless/assets.stx +29 -2
- package/dist/ui-src/pages/serverless/cost.stx +4 -4
- package/dist/ui-src/pages/serverless/data.stx +56 -3
- package/dist/ui-src/pages/serverless/deployments.stx +30 -6
- package/dist/ui-src/pages/serverless/firewall.stx +1 -1
- package/dist/ui-src/pages/serverless/functions.stx +62 -4
- package/dist/ui-src/pages/serverless/logs.stx +2 -2
- package/dist/ui-src/pages/serverless/metrics.stx +5 -1
- package/dist/ui-src/pages/serverless/queues.stx +90 -26
- package/dist/ui-src/pages/serverless/scheduler.stx +31 -2
- package/dist/ui-src/pages/serverless/secrets.stx +94 -14
- package/dist/ui-src/pages/serverless/traces.stx +88 -0
- package/dist/ui-src/pages/serverless.stx +87 -24
- package/package.json +3 -3
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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">
|
|
@@ -6,15 +6,67 @@ const queues = __d?.queuesDetail ?? [
|
|
|
6
6
|
{ name: 'acme-production-default', short: 'default', visible: 0, inFlight: 2, delayed: 0, processed: 12830, oldestSec: 0, concurrency: 10, tries: 3, visTimeout: 720, dlq: 0 },
|
|
7
7
|
{ name: 'acme-production-emails', short: 'emails', visible: 4, inFlight: 1, delayed: 0, processed: 9210, oldestSec: 14, concurrency: 5, tries: 3, visTimeout: 720, dlq: 1 },
|
|
8
8
|
]
|
|
9
|
-
const dlqItems = __d?.dlqItems ?? [
|
|
10
|
-
{ id: 'b1f3…9a', queue: 'emails', job: 'App\\Jobs\\SendInvoice', failed: '2h ago', attempts: 3, error: 'SMTP timeout' },
|
|
11
|
-
]
|
|
12
9
|
function fmt(n) { return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k` : String(n) }
|
|
10
|
+
const initialQueues = queues
|
|
11
|
+
const queueNames = queues.map(q => q.short)
|
|
12
|
+
</script>
|
|
13
|
+
<script client>
|
|
14
|
+
// Live dead-letter queue: peek at messages, redrive them back onto a source
|
|
15
|
+
// queue, or purge the DLQ. Per-queue purge runs the queue:purge:<name> op.
|
|
16
|
+
const rows = state(initialQueues)
|
|
17
|
+
const dlq = state([])
|
|
18
|
+
const dlqLoaded = state(false)
|
|
19
|
+
const dlqMsg = state('')
|
|
20
|
+
const target = state(queueNames[0] || '')
|
|
21
|
+
const busy = state(false)
|
|
22
|
+
|
|
23
|
+
// Op runner (redrive / purge / per-queue purge) - typed-confirm bar.
|
|
24
|
+
const opOutput = state('')
|
|
25
|
+
const opShown = state(false)
|
|
26
|
+
const pending = state(null)
|
|
27
|
+
const typed = state('')
|
|
28
|
+
const confirmTok = derived(() => { const p = pending(); return p ? p.confirm : '' })
|
|
29
|
+
const confirmVerb = derived(() => { const p = pending(); return p ? p.verb : '' })
|
|
30
|
+
const canRun = derived(() => { const p = pending(); return !!p && typed() === p.confirm })
|
|
31
|
+
function askOp(kind, confirm, verb) { pending.set({ kind, confirm, verb }); typed.set('') }
|
|
32
|
+
function cancelOp() { pending.set(null) }
|
|
33
|
+
|
|
34
|
+
async function loadDlq() {
|
|
35
|
+
dlqMsg.set('Loading dead-letter queue...')
|
|
36
|
+
try {
|
|
37
|
+
const r = await fetch('/api/serverless/dlq?max=10')
|
|
38
|
+
const x = await r.json()
|
|
39
|
+
if (x.ok) { dlq.set(x.messages || []); dlqMsg.set('') } else { dlq.set([]); dlqMsg.set(x.error || 'DLQ unavailable.') }
|
|
40
|
+
} catch (e) { dlqMsg.set('Failed to load DLQ: ' + ((e && e.message) || e)) }
|
|
41
|
+
finally { dlqLoaded.set(true) }
|
|
42
|
+
}
|
|
43
|
+
onMount(() => { loadDlq() })
|
|
44
|
+
|
|
45
|
+
async function runOp() {
|
|
46
|
+
const p = pending()
|
|
47
|
+
if (!p || typed() !== p.confirm) return
|
|
48
|
+
pending.set(null); typed.set('')
|
|
49
|
+
opShown.set(true); opOutput.set('Running...')
|
|
50
|
+
busy.set(true)
|
|
51
|
+
try {
|
|
52
|
+
let res
|
|
53
|
+
if (p.kind === 'redrive')
|
|
54
|
+
res = await fetch('/api/serverless/dlq/redrive', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: 'redrive', targetQueue: target(), max: 25 }) })
|
|
55
|
+
else if (p.kind === 'purge-dlq')
|
|
56
|
+
res = await fetch('/api/serverless/dlq/purge', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ confirm: 'purge' }) })
|
|
57
|
+
else
|
|
58
|
+
res = await fetch('/api/serverless/operations/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ operation: 'queue:purge:' + p.kind.replace('purge-queue:', ''), confirm: p.confirm }) })
|
|
59
|
+
const body = await res.json()
|
|
60
|
+
opOutput.set((body.ok ? 'OK ' : 'FAILED ') + (body.command || '') + '\n\n' + (body.stdout || '') + (body.error ? '\n' + body.error : ''))
|
|
61
|
+
if (body.ok) loadDlq()
|
|
62
|
+
} catch (e) { opOutput.set('FAILED\n\n' + ((e && e.message) || e)) }
|
|
63
|
+
finally { busy.set(false) }
|
|
64
|
+
}
|
|
13
65
|
</script>
|
|
14
66
|
<!DOCTYPE html>
|
|
15
67
|
<html lang="en" data-theme="dark">
|
|
16
68
|
<head>
|
|
17
|
-
@include('../partials/head', { title: 'ts-cloud
|
|
69
|
+
@include('../partials/head', { title: 'ts-cloud · Queues' })
|
|
18
70
|
</head>
|
|
19
71
|
<body>
|
|
20
72
|
@include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
|
|
@@ -30,7 +82,7 @@ function fmt(n) { return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k
|
|
|
30
82
|
<div class="section">
|
|
31
83
|
<div class="panel">
|
|
32
84
|
<table>
|
|
33
|
-
<thead><tr><th>Queue</th><th>Visible</th><th>In-flight</th><th>Oldest</th><th>Concurrency</th><th>Tries</th><th>Vis. timeout</th><th>Processed</th><th>DLQ</th></tr></thead>
|
|
85
|
+
<thead><tr><th>Queue</th><th>Visible</th><th>In-flight</th><th>Oldest</th><th>Concurrency</th><th>Tries</th><th>Vis. timeout</th><th>Processed</th><th>DLQ</th><th class="table-actions">Action</th></tr></thead>
|
|
34
86
|
<tbody>
|
|
35
87
|
@foreach (queues as q)
|
|
36
88
|
<tr>
|
|
@@ -43,6 +95,7 @@ function fmt(n) { return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k
|
|
|
43
95
|
<td class="mono">{{ q.visTimeout }}s</td>
|
|
44
96
|
<td style="color:var(--txt2)">{{ fmt(q.processed) }}</td>
|
|
45
97
|
<td>@if (q.dlq > 0)<span class="bad pill">{{ q.dlq }}</span>@else<span style="color:var(--txt3)">0</span>@endif</td>
|
|
98
|
+
<td class="table-actions"><button class="btn danger sm" type="button" @click="askOp('purge-queue:{{ q.short }}', '{{ q.short }}', 'purge the {{ q.short }} queue')">Purge</button></td>
|
|
46
99
|
</tr>
|
|
47
100
|
@endforeach
|
|
48
101
|
</tbody>
|
|
@@ -53,27 +106,38 @@ function fmt(n) { return n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k
|
|
|
53
106
|
<div class="section">
|
|
54
107
|
<h2>Dead-letter queue</h2>
|
|
55
108
|
<div class="panel">
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
109
|
+
<div class="filters">
|
|
110
|
+
<div class="select-filter">
|
|
111
|
+
<span>Redrive into</span>
|
|
112
|
+
<select :value="target()" @change="target.set($event.target.value)">
|
|
113
|
+
<template :for="n in queueNames">
|
|
114
|
+
<option :value="n">{{ n }}</option>
|
|
115
|
+
</template>
|
|
116
|
+
</select>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="row-actions">
|
|
119
|
+
<button class="btn ghost sm" type="button" :disabled="busy()" @click="loadDlq()">Refresh</button>
|
|
120
|
+
<button class="btn ghost sm" type="button" :disabled="busy()" @click="askOp('redrive', 'redrive', 'move DLQ messages back onto the source queue')">Redrive</button>
|
|
121
|
+
<button class="btn danger sm" type="button" :disabled="busy()" @click="askOp('purge-dlq', 'purge', 'permanently purge the dead-letter queue')">Purge DLQ</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<table @show="dlq().length > 0">
|
|
126
|
+
<thead><tr><th>Message ID</th><th>Body</th></tr></thead>
|
|
127
|
+
<tbody>
|
|
128
|
+
<template :for="m in dlq()">
|
|
129
|
+
<tr>
|
|
130
|
+
<td class="mono" style="font-size:11px">{{ m.id }}</td>
|
|
131
|
+
<td class="mono" style="font-size:11px;color:var(--txt2);max-width:640px;overflow:hidden;text-overflow:ellipsis">{{ m.body }}</td>
|
|
132
|
+
</tr>
|
|
133
|
+
</template>
|
|
134
|
+
</tbody>
|
|
135
|
+
</table>
|
|
136
|
+
<div class="compact empty" @show="dlqLoaded() && dlq().length === 0 && !dlqMsg()"><strong>No failed messages</strong><span>The dead-letter queue is empty.</span></div>
|
|
137
|
+
<p class="note" @show="dlqMsg() !== ''">{{ dlqMsg() }}</p>
|
|
138
|
+
|
|
139
|
+
@include('../partials/op-confirm')
|
|
140
|
+
<p class="note">Redrive receives messages from the DLQ and re-sends them to the selected source queue (peeked messages stay put). Purge is irreversible.</p>
|
|
77
141
|
</div>
|
|
78
142
|
</div>
|
|
79
143
|
</div>
|
|
@@ -3,7 +3,7 @@ let __d = null
|
|
|
3
3
|
try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
|
|
4
4
|
const app = __d?.app ?? { name: 'acme', env: 'production' }
|
|
5
5
|
const scheduler = { ...{ enabled: true, mode: 'sub-minute', expression: 'rate(1 minute)', lastRun: '36s ago', nextRun: '24s' }, ...(__d?.scheduler ?? {}) }
|
|
6
|
-
// Tasks Laravel's schedule:run would fire (illustrative
|
|
6
|
+
// Tasks Laravel's schedule:run would fire (illustrative - discovered from the app).
|
|
7
7
|
const tasks = __d?.schedulerTasks ?? [
|
|
8
8
|
{ command: 'emails:digest', cron: '0 8 * * *', desc: 'Daily 08:00', lastRun: '8h ago', lastStatus: 'ok' },
|
|
9
9
|
{ command: 'metrics:rollup', cron: '*/5 * * * *', desc: 'Every 5 min', lastRun: '3m ago', lastStatus: 'ok' },
|
|
@@ -12,10 +12,33 @@ const tasks = __d?.schedulerTasks ?? [
|
|
|
12
12
|
]
|
|
13
13
|
function tone(s) { return s === 'ok' ? 'ok' : (s === 'failed' ? 'bad' : 'warn') }
|
|
14
14
|
</script>
|
|
15
|
+
<script client>
|
|
16
|
+
// Scheduler control runs live against AWS through /api/serverless/scheduler.
|
|
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/scheduler', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 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
|
|
41
|
+
@include('../partials/head', { title: 'ts-cloud · Scheduler' })
|
|
19
42
|
</head>
|
|
20
43
|
<body>
|
|
21
44
|
@include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
|
|
@@ -26,6 +49,11 @@ function tone(s) { return s === 'ok' ? 'ok' : (s === 'failed' ? 'bad' : 'warn')
|
|
|
26
49
|
<h1>Scheduler</h1>
|
|
27
50
|
<p>An EventBridge rule invokes the CLI function with <span class="mono">schedule:run</span> every minute.</p>
|
|
28
51
|
</div>
|
|
52
|
+
<div class="header-actions">
|
|
53
|
+
<button class="btn sm" type="button" @click="askOp('run', 'run', 'run the scheduler now')">Run now</button>
|
|
54
|
+
<button class="btn sm" type="button" @click="askOp('enable', 'enable', 'enable the scheduler')">Enable</button>
|
|
55
|
+
<button class="btn danger sm" type="button" @click="askOp('disable', 'disable', 'disable the scheduler')">Disable</button>
|
|
56
|
+
</div>
|
|
29
57
|
</header>
|
|
30
58
|
|
|
31
59
|
<div class="grid cards">
|
|
@@ -52,6 +80,7 @@ function tone(s) { return s === 'ok' ? 'ok' : (s === 'failed' ? 'bad' : 'warn')
|
|
|
52
80
|
@endforeach
|
|
53
81
|
</tbody>
|
|
54
82
|
</table>
|
|
83
|
+
@include('../partials/op-confirm')
|
|
55
84
|
<p class="note">Sub-minute mode loops <span class="mono">schedule:run</span> within each invocation. Run manually: <span class="mono">cloud command "schedule:run"</span></p>
|
|
56
85
|
</div>
|
|
57
86
|
</div>
|
|
@@ -2,18 +2,68 @@
|
|
|
2
2
|
let __d = null
|
|
3
3
|
try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
|
|
4
4
|
const app = __d?.app ?? { name: 'acme', env: 'production' }
|
|
5
|
-
// Secret keys (names only
|
|
5
|
+
// Secret keys (names only - values never leave Secrets Manager).
|
|
6
6
|
const secrets = __d?.secretsList ?? [
|
|
7
7
|
{ key: 'APP_KEY', source: 'acme/production/app-key', updated: '14d ago' },
|
|
8
8
|
{ key: 'DB_PASSWORD', source: 'acme/production/db', updated: '14d ago' },
|
|
9
9
|
{ key: 'STRIPE_SECRET', source: 'acme/production/stripe', updated: '3d ago' },
|
|
10
10
|
{ key: 'MAIL_PASSWORD', source: 'acme/production/mail', updated: '21d ago' },
|
|
11
11
|
]
|
|
12
|
+
const initialSecrets = secrets
|
|
13
|
+
</script>
|
|
14
|
+
<script client>
|
|
15
|
+
// Secrets editor: set (create/update) a value in Secrets Manager, or delete one.
|
|
16
|
+
// The value is write-only - it is sent to the server and never read back.
|
|
17
|
+
const rows = state(initialSecrets)
|
|
18
|
+
const secretId = state('')
|
|
19
|
+
const value = state('')
|
|
20
|
+
const msg = state('')
|
|
21
|
+
const msgOk = state(false)
|
|
22
|
+
const busy = state(false)
|
|
23
|
+
|
|
24
|
+
// Delete flow uses a typed-confirm (the secret id) before firing.
|
|
25
|
+
const pendingDelete = state(null)
|
|
26
|
+
const typedDelete = state('')
|
|
27
|
+
const canDelete = derived(() => { const p = pendingDelete(); return !!p && typedDelete() === p })
|
|
28
|
+
|
|
29
|
+
async function refresh() {
|
|
30
|
+
try { const r = await fetch('/api/serverless/secrets'); if (!r.ok) return; const x = await r.json(); if (Array.isArray(x.secrets)) rows.set(x.secrets) } catch {}
|
|
31
|
+
}
|
|
32
|
+
onMount(() => { refresh() })
|
|
33
|
+
|
|
34
|
+
async function setSecret() {
|
|
35
|
+
const id = secretId().trim()
|
|
36
|
+
if (!id) { msg.set('Enter the secret id (source path).'); msgOk.set(false); return }
|
|
37
|
+
if (!value()) { msg.set('Enter a value.'); msgOk.set(false); return }
|
|
38
|
+
busy.set(true); msg.set('Saving ' + id + '...'); msgOk.set(false)
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch('/api/serverless/secrets', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ secretId: id, value: value() }) })
|
|
41
|
+
const body = await res.json()
|
|
42
|
+
msg.set(body.ok ? (body.stdout || 'Saved.') : (body.error || 'Failed to save.')); msgOk.set(!!body.ok)
|
|
43
|
+
if (body.ok) { value.set(''); refresh() }
|
|
44
|
+
} catch (e) { msg.set('Failed: ' + ((e && e.message) || e)); msgOk.set(false) }
|
|
45
|
+
finally { busy.set(false) }
|
|
46
|
+
}
|
|
47
|
+
function pickSource(src) { secretId.set(src) }
|
|
48
|
+
function askDelete(src) { pendingDelete.set(src); typedDelete.set('') }
|
|
49
|
+
function cancelDelete() { pendingDelete.set(null); typedDelete.set('') }
|
|
50
|
+
async function confirmDelete() {
|
|
51
|
+
const id = pendingDelete()
|
|
52
|
+
if (!id || typedDelete() !== id) return
|
|
53
|
+
pendingDelete.set(null); typedDelete.set('')
|
|
54
|
+
msg.set('Deleting ' + id + '...'); msgOk.set(false)
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch('/api/serverless/secrets', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ secretId: id, confirm: id }) })
|
|
57
|
+
const body = await res.json()
|
|
58
|
+
msg.set(body.ok ? (body.stdout || 'Deleted.') : (body.error || 'Failed to delete.')); msgOk.set(!!body.ok)
|
|
59
|
+
if (body.ok) refresh()
|
|
60
|
+
} catch (e) { msg.set('Failed: ' + ((e && e.message) || e)); msgOk.set(false) }
|
|
61
|
+
}
|
|
12
62
|
</script>
|
|
13
63
|
<!DOCTYPE html>
|
|
14
64
|
<html lang="en" data-theme="dark">
|
|
15
65
|
<head>
|
|
16
|
-
@include('../partials/head', { title: 'ts-cloud
|
|
66
|
+
@include('../partials/head', { title: 'ts-cloud · Secrets' })
|
|
17
67
|
</head>
|
|
18
68
|
<body>
|
|
19
69
|
@include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
|
|
@@ -22,32 +72,62 @@ const secrets = __d?.secretsList ?? [
|
|
|
22
72
|
<header class="page">
|
|
23
73
|
<div>
|
|
24
74
|
<h1>Secrets</h1>
|
|
25
|
-
<p>Stored in AWS Secrets Manager and injected into the functions at deploy. Values are never displayed.</p>
|
|
75
|
+
<p>Stored in AWS Secrets Manager and injected into the functions at deploy. Values are write-only and never displayed.</p>
|
|
26
76
|
</div>
|
|
27
77
|
</header>
|
|
28
78
|
|
|
29
79
|
<div class="section">
|
|
30
80
|
<div class="panel">
|
|
31
81
|
<table>
|
|
32
|
-
<thead><tr><th>Key</th><th>Secret source</th><th>Value</th><th>Updated</th><th
|
|
82
|
+
<thead><tr><th>Key</th><th>Secret source</th><th>Value</th><th>Updated</th><th class="table-actions">Actions</th></tr></thead>
|
|
33
83
|
<tbody>
|
|
34
|
-
|
|
84
|
+
<template :for="s in rows()">
|
|
35
85
|
<tr>
|
|
36
86
|
<td class="mono"><b>{{ s.key }}</b></td>
|
|
37
|
-
<td class="mono" style="color:var(--txt3)">{{ s.source }}</td>
|
|
87
|
+
<td class="mono" style="color:var(--txt3)">{{ s.source ?? s.secretId }}</td>
|
|
38
88
|
<td class="mono" style="letter-spacing:2px;color:var(--txt3)">••••••••</td>
|
|
39
|
-
<td style="color:var(--txt3)">{{ s.updated }}</td>
|
|
40
|
-
<td
|
|
89
|
+
<td style="color:var(--txt3)">{{ s.updated ?? '-' }}</td>
|
|
90
|
+
<td class="table-actions">
|
|
91
|
+
<div class="row-actions" style="justify-content:flex-end">
|
|
92
|
+
<button class="btn ghost sm" type="button" @click="pickSource(s.source ?? s.secretId)">Set value</button>
|
|
93
|
+
<button class="btn danger sm" type="button" @click="askDelete(s.source ?? s.secretId)">Delete</button>
|
|
94
|
+
</div>
|
|
95
|
+
</td>
|
|
41
96
|
</tr>
|
|
42
|
-
|
|
97
|
+
</template>
|
|
98
|
+
<tr @show="rows().length === 0">
|
|
99
|
+
<td colspan="5"><div class="compact empty"><strong>No secrets referenced</strong><span>Add secrets under <span class="mono">app.secrets</span> in your cloud config, then set their values here.</span></div></td>
|
|
100
|
+
</tr>
|
|
43
101
|
</tbody>
|
|
44
102
|
</table>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<span class="
|
|
48
|
-
<
|
|
103
|
+
|
|
104
|
+
<div class="op-confirm" @show="pendingDelete() !== null" style="margin-top:14px">
|
|
105
|
+
<span>Type <b class="mono">{{ pendingDelete() }}</b> to delete this secret:</span>
|
|
106
|
+
<input class="op-confirm-input" :value="typedDelete()" @input="typedDelete.set($event.target.value)" @keydown.enter="confirmDelete()" placeholder="confirm" autocomplete="off">
|
|
107
|
+
<button class="btn danger sm" :disabled="!canDelete()" @click="confirmDelete()">Delete</button>
|
|
108
|
+
<button class="btn ghost sm" @click="cancelDelete()">Cancel</button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div class="section">
|
|
114
|
+
<h2>Set a secret value</h2>
|
|
115
|
+
<div class="panel">
|
|
116
|
+
<div class="form-grid">
|
|
117
|
+
<div class="field-block">
|
|
118
|
+
<label>Secret id (source path)</label>
|
|
119
|
+
<input :value="secretId()" @input="secretId.set($event.target.value)" placeholder="acme/production/stripe" autocomplete="off">
|
|
120
|
+
</div>
|
|
121
|
+
<div class="field-block">
|
|
122
|
+
<label>Value (write-only)</label>
|
|
123
|
+
<input type="password" :value="value()" @input="value.set($event.target.value)" placeholder="new secret value" autocomplete="off">
|
|
124
|
+
</div>
|
|
125
|
+
<div class="form-actions">
|
|
126
|
+
<button class="btn" type="button" :disabled="busy()" @click="setSecret()">Save secret</button>
|
|
127
|
+
<span class="msg" :class="{ ok: msgOk(), bad: msg() && !msgOk() }">{{ msg() }}</span>
|
|
128
|
+
</div>
|
|
49
129
|
</div>
|
|
50
|
-
<p class="note">
|
|
130
|
+
<p class="note">Setting a value creates the Secrets Manager entry if it does not exist. The new value is injected into the functions on the next <span class="mono">cloud deploy:serverless</span>.</p>
|
|
51
131
|
</div>
|
|
52
132
|
</div>
|
|
53
133
|
</div>
|
|
@@ -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 app = __d?.app ?? { name: 'acme', env: 'production' }
|
|
5
|
+
const initialTraces = __d?.traces ?? [
|
|
6
|
+
{ id: '1-6630a1b2-abc123', method: 'GET', url: 'https://acme.com/', httpStatus: 200, responseMs: 84, durationMs: 86, status: 'ok' },
|
|
7
|
+
{ id: '1-6630a1c4-def456', method: 'POST', url: 'https://acme.com/checkout', httpStatus: 500, responseMs: 240, durationMs: 512, status: 'fault' },
|
|
8
|
+
{ id: '1-6630a1d9-9a0f21', method: 'GET', url: 'https://acme.com/api/orders', httpStatus: 429, responseMs: 12, durationMs: 12, status: 'throttle' },
|
|
9
|
+
]
|
|
10
|
+
</script>
|
|
11
|
+
<script client>
|
|
12
|
+
// X-Ray traces for the HTTP function, refreshed over /api/serverless/traces.
|
|
13
|
+
const traces = state(initialTraces)
|
|
14
|
+
const minutes = state(30)
|
|
15
|
+
const note = state('')
|
|
16
|
+
const loading = state(false)
|
|
17
|
+
const filter = state('all')
|
|
18
|
+
const shown = derived(() => traces().filter(t => filter() === 'all' || t.status === filter()))
|
|
19
|
+
function tone(s) { return s === 'ok' ? 'ok' : (s === 'throttle' ? 'warn' : 'bad') }
|
|
20
|
+
async function load() {
|
|
21
|
+
loading.set(true); note.set('')
|
|
22
|
+
try {
|
|
23
|
+
const r = await fetch('/api/serverless/traces?minutes=' + minutes())
|
|
24
|
+
const d = await r.json()
|
|
25
|
+
if (d.ok) { traces.set(d.traces || []); if (!(d.traces || []).length) note.set('No traces in the last ' + minutes() + ' minutes. Enable active tracing on the functions to populate this view.') }
|
|
26
|
+
else { note.set(d.error || 'Traces unavailable.') }
|
|
27
|
+
} catch (e) { note.set('Failed to load traces: ' + ((e && e.message) || e)) }
|
|
28
|
+
finally { loading.set(false) }
|
|
29
|
+
}
|
|
30
|
+
function setWindow(m) { minutes.set(m); load() }
|
|
31
|
+
onMount(() => { load() })
|
|
32
|
+
</script>
|
|
33
|
+
<!DOCTYPE html>
|
|
34
|
+
<html lang="en" data-theme="dark">
|
|
35
|
+
<head>@include('../partials/head', { title: 'ts-cloud · Traces' })</head>
|
|
36
|
+
<body>
|
|
37
|
+
@include('../partials/nav', { active: 'serverless', name: app.name, env: app.env })
|
|
38
|
+
<div class="wrap">
|
|
39
|
+
<div class="crumbs"><a href="/serverless">Serverless</a> <span class="sep">/</span> Traces</div>
|
|
40
|
+
<header class="page">
|
|
41
|
+
<div><h1>Traces</h1><p>Distributed traces from AWS X-Ray for the HTTP function, newest first.</p></div>
|
|
42
|
+
<div class="header-actions">
|
|
43
|
+
<button class="btn ghost sm" type="button" :disabled="loading()" @click="load()">Refresh</button>
|
|
44
|
+
</div>
|
|
45
|
+
</header>
|
|
46
|
+
|
|
47
|
+
<div class="section">
|
|
48
|
+
<div class="panel">
|
|
49
|
+
<div class="filters">
|
|
50
|
+
<div class="segmented" role="group" aria-label="Time window">
|
|
51
|
+
<button type="button" :class="{ active: minutes() === 15 }" @click="setWindow(15)">15m</button>
|
|
52
|
+
<button type="button" :class="{ active: minutes() === 30 }" @click="setWindow(30)">30m</button>
|
|
53
|
+
<button type="button" :class="{ active: minutes() === 60 }" @click="setWindow(60)">1h</button>
|
|
54
|
+
<button type="button" :class="{ active: minutes() === 180 }" @click="setWindow(180)">3h</button>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="segmented" role="group" aria-label="Status filter">
|
|
57
|
+
<button type="button" :class="{ active: filter() === 'all' }" @click="filter.set('all')">All</button>
|
|
58
|
+
<button type="button" :class="{ active: filter() === 'ok' }" @click="filter.set('ok')">OK</button>
|
|
59
|
+
<button type="button" :class="{ active: filter() === 'error' }" @click="filter.set('error')">Error</button>
|
|
60
|
+
<button type="button" :class="{ active: filter() === 'fault' }" @click="filter.set('fault')">Fault</button>
|
|
61
|
+
<button type="button" :class="{ active: filter() === 'throttle' }" @click="filter.set('throttle')">Throttle</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<table @show="shown().length > 0">
|
|
66
|
+
<thead><tr><th>Trace</th><th>Method</th><th>URL</th><th>HTTP</th><th>Response</th><th>Duration</th><th>Status</th></tr></thead>
|
|
67
|
+
<tbody>
|
|
68
|
+
<template :for="t in shown()">
|
|
69
|
+
<tr>
|
|
70
|
+
<td class="mono" style="font-size:11px">{{ t.id }}</td>
|
|
71
|
+
<td class="mono">{{ t.method }}</td>
|
|
72
|
+
<td class="mono" style="font-size:11px;color:var(--txt2);max-width:420px;overflow:hidden;text-overflow:ellipsis">{{ t.url }}</td>
|
|
73
|
+
<td class="mono">{{ t.httpStatus }}</td>
|
|
74
|
+
<td class="mono">{{ t.responseMs }}ms</td>
|
|
75
|
+
<td class="mono">{{ t.durationMs }}ms</td>
|
|
76
|
+
<td><span class="pill" :class="tone(t.status)">{{ t.status }}</span></td>
|
|
77
|
+
</tr>
|
|
78
|
+
</template>
|
|
79
|
+
</tbody>
|
|
80
|
+
</table>
|
|
81
|
+
<div class="compact empty" @show="shown().length === 0"><strong>No traces</strong><span>{{ note() || 'No traces match this filter.' }}</span></div>
|
|
82
|
+
<p class="note" @show="note() !== '' && shown().length > 0">{{ note() }}</p>
|
|
83
|
+
<p class="note">Traces need active tracing enabled on the Lambda functions. Set tracing in your serverless app config and redeploy.</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|