diff --git a/packages/ui/pages/partials/op-confirm.stx b/packages/ui/pages/partials/op-confirm.stx index 703ff63b..ac182a84 100644 --- a/packages/ui/pages/partials/op-confirm.stx +++ b/packages/ui/pages/partials/op-confirm.stx @@ -1,7 +1,9 @@
Type {{ confirmTok() }} to {{ confirmVerb() }}: - - - + + +
+@if (output ?? true)
{{ opOutput() }}
+@endif diff --git a/packages/ui/pages/server/firewall.stx b/packages/ui/pages/server/firewall.stx index 78a5591b..5064064c 100644 --- a/packages/ui/pages/server/firewall.stx +++ b/packages/ui/pages/server/firewall.stx @@ -50,13 +50,15 @@ async function removePort(port) { setMessage('Removed port ' + port + ' from cloud config.' + applyLine(data.apply), 'ok') } -// Remove runs through a typed-confirm bar: type the port number to confirm. +// Remove runs through the shared typed-confirm bar: type the port number to +// confirm. Names match the contract partials/op-confirm.stx binds to. const pending = state(null); const typed = state('') -const confirmTok = derived(() => { const p = pending(); return p ? String(p) : '' }) +const confirmTok = derived(() => { const p = pending(); return p !== null ? String(p) : '' }) +const confirmVerb = derived(() => { const p = pending(); return p !== null ? 'remove port ' + p : '' }) const canRun = derived(() => { const p = pending(); return p !== null && typed() === String(p) }) function askRemove(port) { pending.set(port); typed.set('') } -function cancelRemove() { pending.set(null); typed.set('') } -async function runRemove() { +function cancelOp() { pending.set(null); typed.set('') } +async function runOp() { const p = pending(); if (p === null || typed() !== String(p)) return pending.set(null); typed.set('') await removePort(p) @@ -120,12 +122,7 @@ onMount(() => { load() })
No extra ports configuredOnly 22, 80, and 443 are open. Allow a port above to update infrastructure.compute.firewall.
-
- Type {{ confirmTok() }} to remove this port: - - - -
+ @include('../partials/op-confirm', { label: 'Remove', danger: true, output: false })

Ports are declarative: this page edits infrastructure.compute.firewall in cloud config and applies ufw allow / ufw delete allow live on the box. The full config is reconciled again on the next cloud deploy.

diff --git a/packages/ui/pages/server/ssh-keys.stx b/packages/ui/pages/server/ssh-keys.stx index 91ff2548..557391d2 100644 --- a/packages/ui/pages/server/ssh-keys.stx +++ b/packages/ui/pages/server/ssh-keys.stx @@ -41,15 +41,17 @@ async function removeKey(n) { // Removal runs through a typed-confirm bar (same pattern as firewall/team): // type the key name to confirm. Removing the wrong key can lock every operator // out of the box, so it should never be one misclick away. -const pendingRemove = state(null) -const typedRemove = state('') -const canRemove = derived(() => { const k = pendingRemove(); return k !== null && typedRemove() === String(k.name) }) -function askRemove(key) { pendingRemove.set(key); typedRemove.set('') } -function cancelRemove() { pendingRemove.set(null); typedRemove.set('') } -async function confirmRemove() { - const k = pendingRemove() - if (!k || typedRemove() !== String(k.name)) return - pendingRemove.set(null); typedRemove.set('') +const pending = state(null) +const typed = state('') +const canRun = derived(() => { const k = pending(); return k !== null && typed() === String(k.name) }) +const confirmTok = derived(() => { const k = pending(); return k ? String(k.name) : '' }) +const confirmVerb = derived(() => 'remove this key from the box') +function askRemove(key) { pending.set(key); typed.set('') } +function cancelOp() { pending.set(null); typed.set('') } +async function runOp() { + const k = pending() + if (!k || typed() !== String(k.name)) return + pending.set(null); typed.set('') await removeKey(k.name) } @@ -100,12 +102,7 @@ onMount(() => { load() })
No SSH keys configuredAdd a public key above to update infrastructure.compute.sshKeys.
-
- Type {{ pendingRemove()?.name }} to remove this key from the box: - - - -
+ @include('../partials/op-confirm', { label: 'Remove', danger: true, output: false })

Keys are declarative: this page edits infrastructure.compute.sshKeys in cloud config. Run cloud deploy to apply changes to the box. Connect with cloud server:ssh {{ server.name }}.

diff --git a/packages/ui/pages/server/team.stx b/packages/ui/pages/server/team.stx index cda7a270..8aa01c48 100644 --- a/packages/ui/pages/server/team.stx +++ b/packages/ui/pages/server/team.stx @@ -67,19 +67,21 @@ async function revoke(name) { load() } -// Removal runs through a typed-confirm bar (same pattern as firewall/secrets): -// type the username to confirm. Revoking pulls someone's access to every site -// on the box at once and invalidates their password, so it should never be one -// misclick away. -const pendingRevoke = state(null) -const typedRevoke = state('') -const canRevoke = derived(() => { const u = pendingRevoke(); return u !== null && typedRevoke() === String(u.username) }) -function askRevoke(user) { pendingRevoke.set(user); typedRevoke.set('') } -function cancelRevoke() { pendingRevoke.set(null); typedRevoke.set('') } -async function confirmRevoke() { - const u = pendingRevoke() - if (!u || typedRevoke() !== String(u.username)) return - pendingRevoke.set(null); typedRevoke.set('') +// Removal runs through the shared typed-confirm bar: type the username to +// confirm. Revoking pulls someone's access to every site on the box at once and +// invalidates their password, so it should never be one misclick away. Names +// match the contract partials/op-confirm.stx binds to. +const pending = state(null) +const typed = state('') +const confirmTok = derived(() => { const u = pending(); return u ? String(u.username) : '' }) +const confirmVerb = derived(() => { const u = pending(); return u ? 'remove ' + u.name + ' from every site' : '' }) +const canRun = derived(() => { const u = pending(); return u !== null && typed() === String(u.username) }) +function askRevoke(user) { pending.set(user); typed.set('') } +function cancelOp() { pending.set(null); typed.set('') } +async function runOp() { + const u = pending() + if (!u || typed() !== String(u.username)) return + pending.set(null); typed.set('') await revoke(u.username) } @@ -157,12 +159,7 @@ onMount(() => { load() })
No one invited yetInvite someone above to give them access to a site.
-
- Type {{ pendingRevoke()?.username }} to remove {{ pendingRevoke()?.name }} from every site: - - - -
+ @include('../partials/op-confirm', { label: 'Remove', danger: true, output: false })

Box owners reach everything on this server. Members reach only the sites listed here, and never the shell, SSH keys, firewall or databases. Access is checked on every request.

diff --git a/packages/ui/pages/serverless/alarms.stx b/packages/ui/pages/serverless/alarms.stx index 3d909c26..0a050ade 100644 --- a/packages/ui/pages/serverless/alarms.stx +++ b/packages/ui/pages/serverless/alarms.stx @@ -29,9 +29,11 @@ const msgOk = state(false) const busy = state(false) // Delete flow uses a typed-confirm (the alarm name) before firing. -const pendingDelete = state(null) -const typedDelete = state('') -const canDelete = derived(() => { const p = pendingDelete(); return !!p && typedDelete() === p }) +const pending = state(null) +const typed = state('') +const canRun = derived(() => { const p = pending(); return !!p && typed() === p }) +const confirmTok = derived(() => pending() ?? '') +const confirmVerb = derived(() => 'delete this alarm') function toneOf(s) { return s === 'ALARM' ? 'bad' : (s === 'OK' ? 'ok' : 'warn') } @@ -60,12 +62,12 @@ async function createAlarm() { finally { busy.set(false) } } -function askDelete(name) { pendingDelete.set(name); typedDelete.set('') } -function cancelDelete() { pendingDelete.set(null); typedDelete.set('') } -async function confirmDelete() { - const name = pendingDelete() - if (!name || typedDelete() !== name) return - pendingDelete.set(null); typedDelete.set('') +function askDelete(name) { pending.set(name); typed.set('') } +function cancelOp() { pending.set(null); typed.set('') } +async function runOp() { + const name = pending() + if (!name || typed() !== name) return + pending.set(null); typed.set('') msg.set('Deleting ' + name + '...'); msgOk.set(false) try { const res = await fetch('/api/serverless/alarms', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, confirm: name }) }) @@ -115,12 +117,7 @@ async function confirmDelete() { -
- Type {{ pendingDelete() }} to delete this alarm: - - - -
+ @include('../partials/op-confirm', { label: 'Delete', danger: true, output: false }) diff --git a/packages/ui/pages/serverless/secrets.stx b/packages/ui/pages/serverless/secrets.stx index e1445c03..00aa1ef4 100644 --- a/packages/ui/pages/serverless/secrets.stx +++ b/packages/ui/pages/serverless/secrets.stx @@ -22,9 +22,11 @@ const msgOk = state(false) const busy = state(false) // Delete flow uses a typed-confirm (the secret id) before firing. -const pendingDelete = state(null) -const typedDelete = state('') -const canDelete = derived(() => { const p = pendingDelete(); return !!p && typedDelete() === p }) +const pending = state(null) +const typed = state('') +const canRun = derived(() => { const p = pending(); return !!p && typed() === p }) +const confirmTok = derived(() => pending() ?? '') +const confirmVerb = derived(() => 'delete this secret') async function refresh() { 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 {} @@ -45,12 +47,12 @@ async function setSecret() { finally { busy.set(false) } } function pickSource(src) { secretId.set(src) } -function askDelete(src) { pendingDelete.set(src); typedDelete.set('') } -function cancelDelete() { pendingDelete.set(null); typedDelete.set('') } -async function confirmDelete() { - const id = pendingDelete() - if (!id || typedDelete() !== id) return - pendingDelete.set(null); typedDelete.set('') +function askDelete(src) { pending.set(src); typed.set('') } +function cancelOp() { pending.set(null); typed.set('') } +async function runOp() { + const id = pending() + if (!id || typed() !== id) return + pending.set(null); typed.set('') msg.set('Deleting ' + id + '...'); msgOk.set(false) try { const res = await fetch('/api/serverless/secrets', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ secretId: id, confirm: id }) }) @@ -101,12 +103,7 @@ async function confirmDelete() { -
- Type {{ pendingDelete() }} to delete this secret: - - - -
+ @include('../partials/op-confirm', { label: 'Delete', danger: true, output: false })