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/server/actions.stxmodified+27-4
Changes to packages/ui/pages/server/actions.stx
@@ -39,12 +39,29 @@ const serverCmd = state('')
3939const cmdOut = state('')
4040const cmdShown = state(false)
4141const cmdBusy = state(false)
42async function runServerCmd() {
42// The API already requires a typed confirmation for shell commands it answers
43// 'Type "run" to execute this command on the server.' The UI never implemented
44// that step and supplied the token itself, so the gate always passed and Enter
45// in the box ran arbitrary shell on the production server. Stage the command,
46// make the operator type the word, and send what they actually typed.
47const cmdPending = state(null)
48const cmdTyped = state('')
49const cmdCanRun = derived(() => cmdPending() !== null && cmdTyped().trim() === 'run')
50function askServerCmd() {
4351 const c = serverCmd().trim()
4452 if (!c) return
53 cmdPending.set(c); cmdTyped.set('')
54}
55function cancelServerCmd() { cmdPending.set(null); cmdTyped.set('') }
56
57async function runServerCmd() {
58 const c = cmdPending()
59 const confirm = cmdTyped().trim()
60 if (!c || confirm !== 'run') return
61 cmdPending.set(null); cmdTyped.set('')
4562 cmdBusy.set(true); cmdShown.set(true); cmdOut.set('Running ' + c + '...')
4663 try {
47 const res = await fetch('/api/server/command', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ command: c, confirm: 'run' }) })
64 const res = await fetch('/api/server/command', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ command: c, confirm }) })
4865 const b = await res.json()
4966 cmdOut.set((b.ok ? 'OK ' : 'FAILED ') + (b.command || c) + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
5067 } catch (e) { cmdOut.set('FAILED ' + c + '\n\n' + ((e && e.message) || e)) }
@@ -81,8 +98,14 @@ async function runServerCmd() {
8198 <h2>Run a command on the server</h2>
8299 <div class="panel">
83100 <div class="field">
84 <input :value="serverCmd()" @input="serverCmd.set($event.target.value)" @keydown.enter="runServerCmd()" placeholder="e.g. systemctl status rpx-gateway, or df -h" autocomplete="off">
85 <button class="btn" type="button" :disabled="cmdBusy()" @click="runServerCmd()">Run</button>
101 <input :value="serverCmd()" @input="serverCmd.set($event.target.value)" @keydown.enter="askServerCmd()" placeholder="e.g. systemctl status rpx-gateway, or df -h" autocomplete="off" aria-label="Command to run on the server">
102 <button class="btn" type="button" :disabled="cmdBusy()" @click="askServerCmd()">Run</button>
103 </div>
104 <div class="op-confirm" @show="cmdPending() !== null" style="margin-top:14px">
105 <span>Type <b class="mono">run</b> to execute <b class="mono">{{ cmdPending() }}</b> on {{ server.name }}:</span>
106 <input class="op-confirm-input" :value="cmdTyped()" @input="cmdTyped.set($event.target.value)" @keydown.enter="runServerCmd()" placeholder="confirm" autocomplete="off" aria-label="Type run to confirm executing this command">
107 <button type="button" class="btn danger sm" :disabled="!cmdCanRun()" @click="runServerCmd()">Run command</button>
108 <button type="button" class="btn ghost sm" @click="cancelServerCmd()">Cancel</button>
86109 </div>
87110 <pre class="action-output" @show="cmdShown()">{{ cmdOut() }}</pre>
88111 <p class="note">Runs on the app box over the active driver (SSH/SSM). Use it for one-off recipes, service inspection, or installing software. Prefer declarative config for anything you want reproduced on the next deploy.</p>