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/ssh-keys.stxmodified+22-1
Changes to packages/ui/pages/server/ssh-keys.stx
@@ -38,6 +38,21 @@ async function removeKey(n) {
3838 keys.set(data.keys || [])
3939 setMessage('Removed from cloud config. Run cloud deploy to apply.', 'ok')
4040}
41// Removal runs through a typed-confirm bar (same pattern as firewall/team):
42// type the key name to confirm. Removing the wrong key can lock every operator
43// out of the box, so it should never be one misclick away.
44const pendingRemove = state(null)
45const typedRemove = state('')
46const canRemove = derived(() => { const k = pendingRemove(); return k !== null && typedRemove() === String(k.name) })
47function askRemove(key) { pendingRemove.set(key); typedRemove.set('') }
48function cancelRemove() { pendingRemove.set(null); typedRemove.set('') }
49async function confirmRemove() {
50 const k = pendingRemove()
51 if (!k || typedRemove() !== String(k.name)) return
52 pendingRemove.set(null); typedRemove.set('')
53 await removeKey(k.name)
54}
55
4156onMount(() => { load() })
4257</script>
4358<!DOCTYPE html>
@@ -79,12 +94,18 @@ onMount(() => { load() })
7994 <td class="mono" style="color:var(--txt3)">{{ k.type }}</td>
8095 <td class="mono">{{ k.fingerprint }}</td>
8196 <td style="color:var(--txt3)">{{ k.added }}</td>
82 <td class="table-actions"><button type="button" class="btn danger sm" @click="removeKey(k.name)">Remove</button></td>
97 <td class="table-actions"><button type="button" class="btn danger sm" :aria-label="'Remove SSH key ' + k.name" @click="askRemove(k)">Remove</button></td>
8398 </tr>
8499 </template>
85100 </tbody>
86101 </table>
87102 <div class="compact empty" @show="keys().length === 0"><strong>No SSH keys configured</strong><span>Add a public key above to update <span class="mono">infrastructure.compute.sshKeys</span>.</span></div>
103 <div class="op-confirm" @show="pendingRemove() !== null" style="margin-top:14px">
104 <span>Type <b class="mono">{{ pendingRemove()?.name }}</b> to remove this key from the box:</span>
105 <input class="op-confirm-input" :value="typedRemove()" @input="typedRemove.set($event.target.value)" @keydown.enter="confirmRemove()" placeholder="confirm" autocomplete="off" aria-label="Type the key name to confirm removal">
106 <button type="button" class="btn danger sm" :disabled="!canRemove()" @click="confirmRemove()">Remove</button>
107 <button type="button" class="btn ghost sm" @click="cancelRemove()">Cancel</button>
108 </div>
88109 <p class="note">Keys are declarative: this page edits <span class="mono">infrastructure.compute.sshKeys</span> in cloud config. Run <span class="mono">cloud deploy</span> to apply changes to the box. Connect with <span class="mono">cloud server:ssh {{ server.name }}</span>.</p>
89110 </div>
90111 </div>