@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
|
@@ -10,7 +10,7 @@ const sites = __d?.sitesDetail ?? [
|
|
|
10
10
|
function tone(s) { return s === 'live' ? 'ok' : (s === 'failed' ? 'bad' : 'warn') }
|
|
11
11
|
</script>
|
|
12
12
|
<script client>
|
|
13
|
-
// Reactive site management
|
|
13
|
+
// Reactive site management - add, per-site deploy/edit/remove - via signals +
|
|
14
14
|
// directives. Mutations re-render the server-built page (location.reload) once
|
|
15
15
|
// the cloud config has changed.
|
|
16
16
|
const addName = state(''); const addRoot = state(''); const addDomain = state(''); const addPath = state('')
|
|
@@ -53,12 +53,15 @@ async function runOp() {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Inline per-site edit (TLS / domain / env).
|
|
56
|
-
const editing = state(''); const editSsl = state(''); const editDomain = state(''); const editEnv = state('')
|
|
57
|
-
function toggleEdit(name) { editing.set(editing() === name ? '' : name); editSsl.set(''); editDomain.set(''); editEnv.set('') }
|
|
56
|
+
const editing = state(''); const editSsl = state(''); const editDomain = state(''); const editEnv = state(''); const editPhp = state(''); const editAliases = state(''); const editRedirects = state('')
|
|
57
|
+
function toggleEdit(name) { editing.set(editing() === name ? '' : name); editSsl.set(''); editDomain.set(''); editEnv.set(''); editPhp.set(''); editAliases.set(''); editRedirects.set('') }
|
|
58
58
|
async function saveEdit(name) {
|
|
59
59
|
const payload = { name }
|
|
60
60
|
if (editSsl() === 'false') payload.ssl = false; else if (editSsl()) payload.ssl = { provider: editSsl() }
|
|
61
61
|
if (editDomain()) payload.domain = editDomain()
|
|
62
|
+
if (editPhp()) payload.php = editPhp()
|
|
63
|
+
if (editAliases().trim()) payload.aliases = editAliases().split(/[\s,]+/).filter(Boolean)
|
|
64
|
+
if (editRedirects().trim()) { payload.redirects = {}; for (const line of editRedirects().split('\n')) { const i = line.indexOf('='); if (i > 0) payload.redirects[line.slice(0, i).trim()] = line.slice(i + 1).trim() } }
|
|
62
65
|
if (editEnv()) { payload.env = {}; for (const line of editEnv().split('\n')) { const i = line.indexOf('='); if (i > 0) payload.env[line.slice(0, i).trim()] = line.slice(i + 1).trim() } }
|
|
63
66
|
opShown.set(true); opOutput.set('Updating ' + name + '...')
|
|
64
67
|
try {
|
|
@@ -112,6 +115,12 @@ async function saveEdit(name) {
|
|
|
112
115
|
@if (site.envKeys && site.envKeys.length)
|
|
113
116
|
<div class="kv"><span>env vars</span> <b class="mono">{{ site.envKeys.join(', ') }}</b></div>
|
|
114
117
|
@endif
|
|
118
|
+
@if (site.aliases && site.aliases.length)
|
|
119
|
+
<div class="kv"><span>aliases</span> <b class="mono">{{ site.aliases.join(', ') }}</b></div>
|
|
120
|
+
@endif
|
|
121
|
+
@if (site.redirects && Object.keys(site.redirects).length)
|
|
122
|
+
<div class="kv"><span>redirects</span> <b class="mono">{{ Object.keys(site.redirects).length }}</b></div>
|
|
123
|
+
@endif
|
|
115
124
|
@if (site.shadowedBy)
|
|
116
125
|
<div class="kv"><span>shadowed by</span> <b class="mono">{{ site.shadowedBy }}</b></div>
|
|
117
126
|
@endif
|
|
@@ -132,6 +141,18 @@ async function saveEdit(name) {
|
|
|
132
141
|
</select>
|
|
133
142
|
</div>
|
|
134
143
|
<div class="field-block"><label>Domain</label><input placeholder="{{ site.domain }}" :value="editDomain()" @input="editDomain.set($event.target.value)"></div>
|
|
144
|
+
<div class="field-block">
|
|
145
|
+
<label>PHP version</label>
|
|
146
|
+
<select :value="editPhp()" @change="editPhp.set($event.target.value)">
|
|
147
|
+
<option value="">(unchanged)</option>
|
|
148
|
+
<option value="8.1">8.1</option>
|
|
149
|
+
<option value="8.2">8.2</option>
|
|
150
|
+
<option value="8.3">8.3</option>
|
|
151
|
+
<option value="8.4">8.4</option>
|
|
152
|
+
</select>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="field-block wide"><label>Domain aliases (space or comma separated)</label><input placeholder="www.example.com, cdn.example.com" :value="editAliases()" @input="editAliases.set($event.target.value)"></div>
|
|
155
|
+
<div class="field-block wide"><label>Redirects (from = to per line)</label><textarea rows="2" placeholder="/old = /new" :value="editRedirects()" @input="editRedirects.set($event.target.value)"></textarea></div>
|
|
135
156
|
<div class="field-block wide"><label>Env vars (KEY=value per line)</label><textarea rows="3" placeholder="NODE_ENV=production" :value="editEnv()" @input="editEnv.set($event.target.value)"></textarea></div>
|
|
136
157
|
<div class="form-actions"><button class="btn" type="submit">Save changes</button></div>
|
|
137
158
|
</form>
|
|
@@ -5,7 +5,7 @@ const server = __d?.server ?? { name: 'acme-production-app' }
|
|
|
5
5
|
const initialKeys = __d?.sshKeys ?? []
|
|
6
6
|
</script>
|
|
7
7
|
<script client>
|
|
8
|
-
// Reactive SSH key management
|
|
8
|
+
// Reactive SSH key management - list + add + remove, via signals + directives.
|
|
9
9
|
// Seeded from build-time data (stx server→client bridge); load() reconciles
|
|
10
10
|
// against the live cloud config (same describeSshKeys shape).
|
|
11
11
|
const keys = state(initialKeys)
|
|
@@ -42,7 +42,7 @@ onMount(() => { load() })
|
|
|
42
42
|
</script>
|
|
43
43
|
<!DOCTYPE html>
|
|
44
44
|
<html lang="en" data-theme="dark">
|
|
45
|
-
<head>@include('../partials/head', { title: 'ts-cloud
|
|
45
|
+
<head>@include('../partials/head', { title: 'ts-cloud - SSH Keys' })</head>
|
|
46
46
|
<body>
|
|
47
47
|
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
48
48
|
<div class="wrap">
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
let __d = null
|
|
3
|
+
try { __d = JSON.parse(process.env.TSCLOUD_DASHBOARD_DATA || '') } catch { __d = null }
|
|
4
|
+
const server = __d?.server ?? { name: 'stacks-production-app' }
|
|
5
|
+
</script>
|
|
6
|
+
<script client>
|
|
7
|
+
// Web terminal: a WebSocket to /api/terminal streams a persistent shell running
|
|
8
|
+
// on the dashboard host (the box, in box mode). Line-oriented: type a command
|
|
9
|
+
// and press Enter; output streams back. Not a full PTY (no vim/htop).
|
|
10
|
+
const buf = state('')
|
|
11
|
+
const status = state('connecting')
|
|
12
|
+
const cmd = state('')
|
|
13
|
+
let ws = null
|
|
14
|
+
function scrollBottom() {
|
|
15
|
+
const el = document.getElementById('term-out')
|
|
16
|
+
if (el) el.scrollTop = el.scrollHeight
|
|
17
|
+
}
|
|
18
|
+
function connect() {
|
|
19
|
+
status.set('connecting')
|
|
20
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
21
|
+
ws = new WebSocket(proto + '//' + location.host + '/api/terminal')
|
|
22
|
+
ws.onopen = () => status.set('connected')
|
|
23
|
+
ws.onclose = () => status.set('disconnected')
|
|
24
|
+
ws.onerror = () => status.set('error')
|
|
25
|
+
ws.onmessage = (e) => { buf.set((buf() + e.data).slice(-40000)); scrollBottom() }
|
|
26
|
+
}
|
|
27
|
+
function send() {
|
|
28
|
+
if (!ws || ws.readyState !== 1) return
|
|
29
|
+
ws.send(cmd() + '\n')
|
|
30
|
+
cmd.set('')
|
|
31
|
+
}
|
|
32
|
+
function reconnect() { try { if (ws) ws.close() } catch {} buf.set(''); connect() }
|
|
33
|
+
function clearOut() { buf.set('') }
|
|
34
|
+
function statusTone(s) { return s === 'connected' ? 'ok' : (s === 'disconnected' || s === 'error' ? 'bad' : 'warn') }
|
|
35
|
+
onMount(() => { connect(); return () => { try { if (ws) ws.close() } catch {} } })
|
|
36
|
+
</script>
|
|
37
|
+
<!DOCTYPE html>
|
|
38
|
+
<html lang="en" data-theme="dark">
|
|
39
|
+
<head>@include('../partials/head', { title: 'ts-cloud · Terminal' })</head>
|
|
40
|
+
<body>
|
|
41
|
+
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
42
|
+
<div class="wrap">
|
|
43
|
+
<div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Terminal</div>
|
|
44
|
+
<header class="page">
|
|
45
|
+
<div><h1>Terminal</h1><p>A live shell on the dashboard host. Type a command and press Enter; output streams back.</p></div>
|
|
46
|
+
<div class="header-actions">
|
|
47
|
+
<span class="pill" :class="statusTone(status())">{{ status() }}</span>
|
|
48
|
+
<button class="btn ghost sm" type="button" @click="clearOut()">Clear</button>
|
|
49
|
+
<button class="btn ghost sm" type="button" @click="reconnect()">Reconnect</button>
|
|
50
|
+
</div>
|
|
51
|
+
</header>
|
|
52
|
+
|
|
53
|
+
<div class="section">
|
|
54
|
+
<div class="panel">
|
|
55
|
+
<pre id="term-out" class="logs" style="max-height:520px; white-space:pre-wrap; overflow-wrap:anywhere;">{{ buf() }}</pre>
|
|
56
|
+
<div class="field" style="margin-top:12px;">
|
|
57
|
+
<input :value="cmd()" @input="cmd.set($event.target.value)" @keydown.enter="send()" placeholder="e.g. systemctl status rpx-gateway, tail -n 50 /var/log/syslog" autocomplete="off" spellcheck="false" style="font-family:ui-monospace, SFMono-Regular, Menlo, monospace;">
|
|
58
|
+
<button class="btn" type="button" @click="send()">Run</button>
|
|
59
|
+
</div>
|
|
60
|
+
<p class="note">The shell runs on the machine serving this dashboard (in box mode, the provisioned server) and is protected by the same Basic auth. It is line-oriented, so full-screen programs like vim or htop are not supported. Disable it with <span class="mono">TS_CLOUD_DASHBOARD_TERMINAL=0</span>.</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
|
@@ -39,7 +39,7 @@ async function runOp() {
|
|
|
39
39
|
</script>
|
|
40
40
|
<!DOCTYPE html>
|
|
41
41
|
<html lang="en" data-theme="dark">
|
|
42
|
-
<head>@include('../partials/head', { title: 'ts-cloud
|
|
42
|
+
<head>@include('../partials/head', { title: 'ts-cloud - Workers' })</head>
|
|
43
43
|
<body>
|
|
44
44
|
@include('../partials/nav', { active: 'server', name: server.name, env: 'production' })
|
|
45
45
|
<div class="wrap">
|
|
@@ -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
|
|
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
|
-
<
|
|
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.5
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 }}
|
|
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
|
|
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="
|
|
28
|
-
<
|
|
29
|
-
<
|
|
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)
|
|
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
|
-
|
|
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
|
|
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 })
|