| 1 | import { useCallback, useEffect, useRef, useState } from "react"; |
| 2 | import { app } from "../lib/bridge"; |
| 3 | import { asArray } from "../lib/array"; |
| 4 | import { useT } from "../lib/i18n"; |
| 5 | import type { HistoricalImportStatus, HistoricalSessionView, HistoricalSourceUpdateView } from "../generated/desktopContract.generated"; |
| 6 | import type { SessionRef } from "../lib/sessionRef"; |
| 7 | import { desktopHost } from "../lib/desktopHost"; |
| 8 | import { useManagementT, type ManagementKey } from "../lib/managementLocale"; |
| 9 | |
| 10 | const emptyHistoricalStatus: HistoricalImportStatus = { items: [], running: false, paused: false, remaining: 0, completed: 0, blocked: 0, failed: 0 }; |
| 11 | const historicalStatusKeys: Record<string, ManagementKey> = { |
| 12 | available: "historicalAvailable", queued: "historicalQueued", importing: "historicalImporting", imported: "historicalImported", |
| 13 | blocked: "historicalBlocked", failed: "historicalNeedsAttention", archived: "historicalArchived", deleted: "historicalDeleted", |
| 14 | }; |
| 15 | |
| 16 | export function HistoricalImportList({ active, onOpenSession }: { active: boolean; onOpenSession?: (ref: SessionRef) => Promise<void> }) { |
| 17 | const t = useT(); |
| 18 | const m = useManagementT(); |
| 19 | const [status, setStatus] = useState<HistoricalImportStatus>(emptyHistoricalStatus); |
| 20 | const [error, setError] = useState(""); |
| 21 | const [busy, setBusy] = useState(""); |
| 22 | const [query, setQuery] = useState(""); |
| 23 | const [updates, setUpdates] = useState<Record<string, HistoricalSourceUpdateView>>({}); |
| 24 | const generation = useRef(0); |
| 25 | const requests = useRef(0); |
| 26 | const reload = useCallback(async (scan = false) => { |
| 27 | const current = generation.current; |
| 28 | const request = ++requests.current; |
| 29 | if (desktopHost().kind === "none") { setStatus(emptyHistoricalStatus); return; } |
| 30 | try { |
| 31 | const next = await (scan ? app.ListHistoricalSessions?.() : app.GetHistoricalImportStatus?.()) ?? emptyHistoricalStatus; |
| 32 | if (current === generation.current && request === requests.current) setStatus({ ...next, items: asArray<HistoricalSessionView>(next.items) }); |
| 33 | } catch (err) { if (current === generation.current) setError(String(err)); } |
| 34 | }, []); |
| 35 | useEffect(() => { |
| 36 | if (!active) return; |
| 37 | setBusy(""); |
| 38 | void reload(true); |
| 39 | const timer = setInterval(() => void reload(), 2000); |
| 40 | return () => { generation.current++; clearInterval(timer); }; |
| 41 | }, [active, reload]); |
| 42 | const open = async (item: HistoricalSessionView) => { |
| 43 | if (busy) return; |
| 44 | const current = generation.current; |
| 45 | setBusy(item.id); setError(""); |
| 46 | try { |
| 47 | const ref = item.session ?? (await app.ImportHistoricalSession!(item.id)).session; |
| 48 | if (current === generation.current && onOpenSession) await onOpenSession(ref); |
| 49 | } catch (err) { if (current === generation.current) setError(String(err)); } |
| 50 | finally { if (current === generation.current) setBusy(""); await reload(); } |
| 51 | }; |
| 52 | const control = async (action: string) => { |
| 53 | const current = generation.current; |
| 54 | ++requests.current; |
| 55 | try { |
| 56 | setError(""); |
| 57 | const next = action === "start" ? await app.StartHistoricalImport!([]) : await app.ControlHistoricalImport!(action); |
| 58 | if (current === generation.current) setStatus(next); |
| 59 | } catch (err) { if (current === generation.current) setError(String(err)); } |
| 60 | }; |
| 61 | const checkUpdate = async (item: HistoricalSessionView) => { |
| 62 | if (!item.source) return; |
| 63 | setBusy(item.id); setError(""); |
| 64 | try { |
| 65 | let update = await app.CheckHistoricalSourceUpdate!({ source: item.source }); |
| 66 | while (update.status === "checking") { |
| 67 | await new Promise(resolve => setTimeout(resolve, 300)); |
| 68 | update = await app.CheckHistoricalSourceUpdate!({ source: item.source }); |
| 69 | } |
| 70 | setUpdates(current => ({ ...current, [item.id]: update })); |
| 71 | } catch (err) { setError(String(err)); } |
| 72 | finally { setBusy(""); } |
| 73 | }; |
| 74 | const prepareUpdate = async (item: HistoricalSessionView, update: HistoricalSourceUpdateView) => { |
| 75 | if (!item.source || !update.version) return; |
| 76 | setBusy(item.id); setError(""); |
| 77 | try { await app.PrepareHistoricalSourceVersion!(item.source, update.version); await reload(); } |
| 78 | catch (err) { setError(String(err)); } |
| 79 | finally { setBusy(""); } |
| 80 | }; |
| 81 | return <section className="archived-sessions" aria-label={m("historicalTitle")}> |
| 82 | <h3>{m("historicalTitle")}</h3> |
| 83 | <p>{m("historicalImportDescription")}</p> |
| 84 | <input aria-label={t("history.searchPlaceholder")} placeholder={t("history.searchPlaceholder")} value={query} onChange={event => setQuery(event.target.value)} /> |
| 85 | <div> |
| 86 | <button className="btn btn--small" disabled={status.running || !!busy} onClick={() => void control("start")}>{m("historicalImportAll")}</button> |
| 87 | {(status.running || (status.paused && status.remaining > 0)) && <> |
| 88 | <button className="btn btn--small" onClick={() => void control(status.paused ? "resume" : "pause")}>{m(status.paused ? "historicalImportResume" : "historicalImportPause")}</button> |
| 89 | <button className="btn btn--small" onClick={() => void control("cancel")}>{m("historicalImportCancel")}</button> |
| 90 | <span role="status">{m("historicalRemaining")} {status.remaining} · {m("historicalImported")} {status.completed} · {m("historicalBlocked")} {status.blocked} · {m("historicalNeedsAttention")} {status.failed}</span> |
| 91 | </>} |
| 92 | <button className="btn btn--small" onClick={() => void reload(true)}>{t("common.retry")}</button> |
| 93 | </div> |
| 94 | {error && <p role="alert">{error}</p>} |
| 95 | {status.items.filter(item => item.title.toLowerCase().includes(query.toLowerCase())).map(item => |
| 96 | <div className="archived-sessions__row" key={item.id}> |
| 97 | <span>{item.title}</span><small>{item.format}</small> |
| 98 | <span>{m(historicalStatusKeys[item.status] ?? "historicalNeedsAttention")}</span> |
| 99 | {item.errorCode && <small>{m(item.errorCode === "source_busy" ? "historicalSourceBusy" : "historicalImportFailed")}</small>} |
| 100 | <button className="btn btn--small" disabled={!!busy || item.status === "importing" || item.status === "queued" || item.status === "archived" || item.status === "deleted"} |
| 101 | onClick={() => void open(item)}>{item.session && onOpenSession ? t("history.openRestored") : m("historicalImportOpen")}</button> |
| 102 | {item.session && item.source && <button className="btn btn--small" disabled={!!busy} onClick={() => void checkUpdate(item)}>{t("common.retry")} · {m("historicalTitle")}</button>} |
| 103 | {updates[item.id]?.status === "available" && <button className="btn btn--small" disabled={!!busy} onClick={() => void prepareUpdate(item, updates[item.id])}>{m("historicalImportOpen")} · {m("branch")}</button>} |
| 104 | {updates[item.id]?.status === "unchanged" && <small>{m("historicalImported")}</small>} |
| 105 | </div>)} |
| 106 | {status.items.length === 0 && <p>{m("noHistoricalSessions")}</p>} |
| 107 | </section>; |
| 108 | } |
| 109 |