diff --git a/src/App.css b/src/App.css index 854018e..883a5c1 100644 --- a/src/App.css +++ b/src/App.css @@ -906,6 +906,30 @@ textarea { resize: vertical; min-height: 60px; line-height: 1.5; } border-color: #4ade8033; } .pill-status.ps-on .dot { background: var(--ok); box-shadow: 0 0 6px var(--ok); } +.pill-status.ps-err { + color: var(--err); + background: #fb71851a; + border-color: #fb718540; +} +.pill-status.ps-err .dot { background: var(--err); box-shadow: 0 0 6px #fb718566; } +.status-stack { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} +.status-error { + appearance: none; + border: 0; + background: transparent; + color: var(--err); + cursor: pointer; + padding: 0; + font-size: 11px; + text-decoration: underline; + text-underline-offset: 2px; +} +.status-error:hover { color: #fda4af; } .proxy-cell { display: flex; align-items: center; gap: 8px; diff --git a/src/App.tsx b/src/App.tsx index 585118e..a127e93 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1017,6 +1017,7 @@ function BrowsersView() { // both as a truthy flag (any number = running) and as the anchor for the // ticking uptime display in the Status column. const [running, setRunning] = useState>({}); + const [launchErrors, setLaunchErrors] = useState>({}); // Re-render trigger so the uptime label ticks every second without // re-fetching the process list (which polls every 2s). const [, setUptimeTick] = useState(0); @@ -1091,6 +1092,18 @@ function BrowsersView() { const list = await invoke<{ profile_id: string; pid: number; uptime_ms: number }[]>("process_list"); if (cancelled) return; const now = Date.now(); + if (list.length > 0) { + setLaunchErrors((errors) => { + let next: Record | null = null; + for (const r of list) { + if (errors[r.profile_id]) { + next ??= { ...errors }; + delete next[r.profile_id]; + } + } + return next ?? errors; + }); + } setRunning((prev) => { const next: Record = {}; for (const r of list) { @@ -1172,10 +1185,18 @@ function BrowsersView() { // state for the whole window is what the user sees as "did it work?". // On failure we unlock immediately and toast the error. const [startBusy, setStartBusy] = useState>(new Set()); + const clearLaunchError = (id: string) => + setLaunchErrors((errors) => { + if (!(id in errors)) return errors; + const next = { ...errors }; + delete next[id]; + return next; + }); const startStop = async (p: ProfileMeta) => { if (running[p.id]) { try { await invoke("process_kill", { profileId: p.id }); + clearLaunchError(p.id); } catch (e) { toast.err(String(e)); } @@ -1183,12 +1204,15 @@ function BrowsersView() { } if (startBusy.has(p.id)) return; setStartBusy((s) => new Set([...s, p.id])); + clearLaunchError(p.id); try { await invoke("launch", { profileId: p.id }); // Don't optimistically flip `running` here; the 2s poll above picks // up the new child immediately and anchors the uptime clock. } catch (e) { - toast.err(String(e)); + const msg = String(e); + setLaunchErrors((errors) => ({ ...errors, [p.id]: msg })); + toast.err(`Launch failed: ${msg}`); } finally { setStartBusy((s) => { const n = new Set(s); @@ -1324,11 +1348,26 @@ function BrowsersView() { }; const bulkLaunch = async () => { + let failed = 0; for (const id of selected) { if (running[id]) continue; - try { await invoke("launch", { profileId: id }); } catch {} + setStartBusy((s) => new Set([...s, id])); + clearLaunchError(id); + try { + await invoke("launch", { profileId: id }); + } catch (e) { + failed += 1; + setLaunchErrors((errors) => ({ ...errors, [id]: String(e) })); + } finally { + setStartBusy((s) => { + const n = new Set(s); + n.delete(id); + return n; + }); + } } setSelected(new Set()); + if (failed > 0) toast.err(`Failed to launch ${failed} profile${failed === 1 ? "" : "s"}`); }; const bulkStop = async () => { @@ -1608,6 +1647,7 @@ function BrowsersView() { {paged.map((p) => { const px = p.proxy_id ? proxyMap[p.proxy_id] : null; const isRunning = !!running[p.id]; + const launchError = launchErrors[p.id]; const isExpanded = expanded === p.id; const isSel = selected.has(p.id); return ( @@ -1653,10 +1693,21 @@ function BrowsersView() {
{p.id.slice(0, 8)}
- - - {isRunning ? "Running" : "Idle"} - +
+ + + {isRunning ? "Running" : launchError ? "Launch failed" : "Idle"} + + {launchError && !isRunning && ( + + )} +
setQuickEdit({ kind: "proxy", profile: p })} title="Change proxy"> {px ? ( @@ -1696,7 +1747,13 @@ function BrowsersView() { className={`btn-launch ${isRunning ? "btn-launch-stop" : ""}`} onClick={() => startStop(p)} disabled={!isRunning && startBusy.has(p.id)} - title={!isRunning && startBusy.has(p.id) ? "Starting (UDP probe + geo + spawn)…" : undefined} + title={ + !isRunning && startBusy.has(p.id) + ? "Starting (UDP probe + geo + spawn)…" + : launchError + ? launchError + : undefined + } > {isRunning ? ( <>Stop