ReviewOS

also looking at this

stacks/ts-cloud

fix(dashboard): confirm destructive actions that ran on first click

#130
Merged glennmichael123 wants to merge fix/dashboard-destructive-confirms into main
4 files +99 -10
packages/ui/pages/serverless.stxmodified+27-4
Changes to packages/ui/pages/serverless.stx
@@ -87,12 +87,29 @@ const appCmd = state('')
8787const cmdOut = state('')
8888const cmdShown = state(false)
8989const cmdBusy = state(false)
90async function runAppCmd() {
90// The API already requires a typed confirmation here ('Type "run" to execute
91// this command.'), but the UI supplied the token itself, so the gate always
92// passed and Enter ran an arbitrary app command (migrate:fresh, queue:flush)
93// against production. Stage it, make the operator type the word, send what they
94// actually typed.
95const cmdPending = state(null)
96const cmdTyped = state('')
97const cmdCanRun = derived(() => cmdPending() !== null && cmdTyped().trim() === 'run')
98function askAppCmd() {
9199 const c = appCmd().trim()
92100 if (!c) return
101 cmdPending.set(c); cmdTyped.set('')
102}
103function cancelAppCmd() { cmdPending.set(null); cmdTyped.set('') }
104
105async function runAppCmd() {
106 const c = cmdPending()
107 const confirm = cmdTyped().trim()
108 if (!c || confirm !== 'run') return
109 cmdPending.set(null); cmdTyped.set('')
93110 cmdBusy.set(true); cmdShown.set(true); cmdOut.set('Running ' + c + '...')
94111 try {
95 const res = await fetch('/api/serverless/command', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ command: c, confirm: 'run' }) })
112 const res = await fetch('/api/serverless/command', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ command: c, confirm }) })
96113 const b = await res.json()
97114 cmdOut.set((b.ok ? 'OK ' : 'FAILED ') + (b.command || c) + '\n\n' + (b.stdout || '') + (b.error ? '\n' + b.error : ''))
98115 } catch (e) { cmdOut.set('FAILED ' + c + '\n\n' + ((e && e.message) || e)) }
@@ -273,8 +290,14 @@ async function runAppCmd() {
273290 <h2>Run a command</h2>
274291 <div class="panel">
275292 <div class="field">
276 <input :value="appCmd()" @input="appCmd.set($event.target.value)" @keydown.enter="runAppCmd()" placeholder="e.g. migrate --force, cache:clear, queue:retry all" autocomplete="off">
277 <button class="btn" type="button" :disabled="cmdBusy()" @click="runAppCmd()">Run</button>
293 <input :value="appCmd()" @input="appCmd.set($event.target.value)" @keydown.enter="askAppCmd()" placeholder="e.g. migrate --force, cache:clear, queue:retry all" autocomplete="off" aria-label="App command to run">
294 <button class="btn" type="button" :disabled="cmdBusy()" @click="askAppCmd()">Run</button>
295 </div>
296 <div class="op-confirm" @show="cmdPending() !== null" style="margin-top:14px">
297 <span>Type <b class="mono">run</b> to execute <b class="mono">{{ cmdPending() }}</b> against production:</span>
298 <input class="op-confirm-input" :value="cmdTyped()" @input="cmdTyped.set($event.target.value)" @keydown.enter="runAppCmd()" placeholder="confirm" autocomplete="off" aria-label="Type run to confirm executing this command">
299 <button type="button" class="btn danger sm" :disabled="!cmdCanRun()" @click="runAppCmd()">Run command</button>
300 <button type="button" class="btn ghost sm" @click="cancelAppCmd()">Cancel</button>
278301 </div>
279302 <pre class="action-output" @show="cmdShown()">{{ cmdOut() }}</pre>
280303 <p class="note">Invokes the app command through the CLI Lambda function (RequestResponse), the same path as <span class="mono">cloud command "..."</span>. Output is clamped.</p>