merge testing->tomerge/main #7

Open
anti wants to merge 242 commits from testing into tomerge/main
2 changed files with 44 additions and 7 deletions
Showing only changes of commit a63301c7a3 - Show all commits

View File

@@ -23,6 +23,13 @@ const SwarmDeckies: React.FC = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [tearingDown, setTearingDown] = useState<string | null>(null); const [tearingDown, setTearingDown] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Two-click arm/commit replaces window.confirm() — browsers silently
// suppress confirm() after the "prevent additional dialogs" opt-out.
const [armed, setArmed] = useState<string | null>(null);
const arm = (key: string) => {
setArmed(key);
setTimeout(() => setArmed((prev) => (prev === key ? null : prev)), 4000);
};
const fetch = async () => { const fetch = async () => {
try { try {
@@ -43,7 +50,9 @@ const SwarmDeckies: React.FC = () => {
}, []); }, []);
const handleTeardown = async (s: DeckyShard) => { const handleTeardown = async (s: DeckyShard) => {
if (!window.confirm(`Tear down decky ${s.decky_name} on ${s.host_name}?`)) return; const key = `td:${s.host_uuid}:${s.decky_name}`;
if (armed !== key) { arm(key); return; }
setArmed(null);
setTearingDown(s.decky_name); setTearingDown(s.decky_name);
try { try {
await api.post(`/swarm/hosts/${s.host_uuid}/teardown`, { decky_id: s.decky_name }); await api.post(`/swarm/hosts/${s.host_uuid}/teardown`, { decky_id: s.decky_name });
@@ -110,7 +119,12 @@ const SwarmDeckies: React.FC = () => {
onClick={() => handleTeardown(s)} onClick={() => handleTeardown(s)}
title="Stop this decky on its host" title="Stop this decky on its host"
> >
<PowerOff size={14} /> {tearingDown === s.decky_name ? 'Tearing down…' : 'Teardown'} <PowerOff size={14} />{' '}
{tearingDown === s.decky_name
? 'Tearing down…'
: armed === `td:${s.host_uuid}:${s.decky_name}`
? 'Click again to confirm'
: 'Teardown'}
</button> </button>
</td> </td>
</tr> </tr>

View File

@@ -25,6 +25,15 @@ const SwarmHosts: React.FC = () => {
const [decommissioning, setDecommissioning] = useState<string | null>(null); const [decommissioning, setDecommissioning] = useState<string | null>(null);
const [tearingDown, setTearingDown] = useState<string | null>(null); const [tearingDown, setTearingDown] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Two-click arm/commit replaces window.confirm(). Browsers silently
// suppress confirm() after the "prevent additional dialogs" opt-out,
// which manifests as a dead button — no network request, no console
// error. Key format: "<action>:<uuid>".
const [armed, setArmed] = useState<string | null>(null);
const arm = (key: string) => {
setArmed(key);
setTimeout(() => setArmed((prev) => (prev === key ? null : prev)), 4000);
};
const fetchHosts = async () => { const fetchHosts = async () => {
try { try {
@@ -45,7 +54,9 @@ const SwarmHosts: React.FC = () => {
}, []); }, []);
const handleTeardownAll = async (host: SwarmHost) => { const handleTeardownAll = async (host: SwarmHost) => {
if (!window.confirm(`Tear down ALL deckies on ${host.name}? The host stays enrolled.`)) return; const key = `teardown:${host.uuid}`;
if (armed !== key) { arm(key); return; }
setArmed(null);
setTearingDown(host.uuid); setTearingDown(host.uuid);
try { try {
await api.post(`/swarm/hosts/${host.uuid}/teardown`, {}); await api.post(`/swarm/hosts/${host.uuid}/teardown`, {});
@@ -58,7 +69,9 @@ const SwarmHosts: React.FC = () => {
}; };
const handleDecommission = async (host: SwarmHost) => { const handleDecommission = async (host: SwarmHost) => {
if (!window.confirm(`Decommission ${host.name} (${host.address})? This removes certs and decky mappings.`)) return; const key = `decom:${host.uuid}`;
if (armed !== key) { arm(key); return; }
setArmed(null);
setDecommissioning(host.uuid); setDecommissioning(host.uuid);
try { try {
await api.delete(`/swarm/hosts/${host.uuid}`); await api.delete(`/swarm/hosts/${host.uuid}`);
@@ -112,19 +125,29 @@ const SwarmHosts: React.FC = () => {
<td>{new Date(h.enrolled_at).toLocaleString()}</td> <td>{new Date(h.enrolled_at).toLocaleString()}</td>
<td> <td>
<button <button
className="control-btn" className={`control-btn${armed === `teardown:${h.uuid}` ? ' danger' : ''}`}
disabled={tearingDown === h.uuid || h.status !== 'active'} disabled={tearingDown === h.uuid || h.status !== 'active'}
onClick={() => handleTeardownAll(h)} onClick={() => handleTeardownAll(h)}
title="Stop all deckies on this host (keeps it enrolled)" title="Stop all deckies on this host (keeps it enrolled)"
> >
<PowerOff size={14} /> {tearingDown === h.uuid ? 'Tearing down…' : 'Teardown all'} <PowerOff size={14} />{' '}
{tearingDown === h.uuid
? 'Tearing down…'
: armed === `teardown:${h.uuid}`
? 'Click again to confirm'
: 'Teardown all'}
</button> </button>
<button <button
className="control-btn danger" className="control-btn danger"
disabled={decommissioning === h.uuid} disabled={decommissioning === h.uuid}
onClick={() => handleDecommission(h)} onClick={() => handleDecommission(h)}
> >
<Trash2 size={14} /> {decommissioning === h.uuid ? 'Decommissioning…' : 'Decommission'} <Trash2 size={14} />{' '}
{decommissioning === h.uuid
? 'Decommissioning…'
: armed === `decom:${h.uuid}`
? 'Click again to confirm'
: 'Decommission'}
</button> </button>
</td> </td>
</tr> </tr>