From 6f9bd0eecc2473133a10070e89aeb0707bdbfbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jozef=20Svr=C4=8Dek?= <24891922+jozef2svrcek@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:51:30 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Multi-source=20Phase=20C2:=20onboarding=20w?= =?UTF-8?q?izard=20=E2=80=94=20populate-vs-empty=20+=20deep-history=20choi?= =?UTF-8?q?ce=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the wizard's inline "download + import TWIC with a progress bar" step with a Sources step that keeps onboarding a simple binary and defers all detail to Maintenance → Sources (C1): - Populate the reference database (recommended) vs Start with an empty database. - Populate path: currency feeds (TWIC + Lichess Broadcasts) auto-selected, plus a deep-history choice — Free (the bulk archive, currently Ajedrez OTB), I own a commercial DB (reuses the existing local PGN import via AddGameDialog), or None. - One acknowledgment list with a per-source credit checkbox (non-commercial licences flagged), reusing the C1 credit_acked gate (sources_set_enabled with credit_acked). Enabling fires sources_sync jobs that run on the daemon in the background, so onboarding finishes immediately instead of blocking on a bar. Client-only: the commercial route and background sync both reuse existing backend job types; no new endpoints. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018gaUhu1KVkdxAjSJBLgbbk --- chess-client/src/components/SetupWizard.tsx | 301 ++++++++++++++------ 1 file changed, 208 insertions(+), 93 deletions(-) diff --git a/chess-client/src/components/SetupWizard.tsx b/chess-client/src/components/SetupWizard.tsx index f41812c..ef33bd1 100644 --- a/chess-client/src/components/SetupWizard.tsx +++ b/chess-client/src/components/SetupWizard.tsx @@ -3,9 +3,8 @@ import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { useSidecarProgress } from "../hooks/useSidecarProgress"; import AddGameDialog from "./AddGameDialog"; import { ProfileSetupForm, loadMyPlayer, saveMyPlayer } from "./MyStatsWidget"; -import { TwicCredit, useTwicAck } from "./TwicCredit"; -import { getTwicFrom, setTwicFrom } from "../twicPrefs"; -import { PlayerInfo } from "../types"; +import { getSources, setSourceEnabled, submitJob } from "../api"; +import { PlayerInfo, SourceStatus } from "../types"; interface Props { onClose: () => void; @@ -14,14 +13,14 @@ interface Props { onFinish?: () => void; } -type Step = "welcome" | "players" | "databases" | "twic" | "dedup" | "normalise" | "index" | "profile" | "done"; +type Step = "welcome" | "players" | "databases" | "sources" | "dedup" | "normalise" | "index" | "profile" | "done"; -const STEPS: Step[] = ["welcome", "players", "databases", "twic", "dedup", "normalise", "index", "profile", "done"]; +const STEPS: Step[] = ["welcome", "players", "databases", "sources", "dedup", "normalise", "index", "profile", "done"]; const STEP_LABELS: Record = { welcome: "Welcome", players: "Players", databases: "Databases", - twic: "TWIC", + sources: "Sources", dedup: "Dedup", normalise: "Names", index: "Index", @@ -29,7 +28,7 @@ const STEP_LABELS: Record = { done: "Summary", }; -const OPTIONAL_STEPS: Step[] = ["players", "databases", "twic", "dedup", "normalise", "index", "profile"]; +const OPTIONAL_STEPS: Step[] = ["players", "databases", "sources", "dedup", "normalise", "index", "profile"]; const STORAGE_KEY = "chess-setup-state"; interface PersistedState { @@ -147,7 +146,7 @@ function WelcomeStep({ onExpress, onAdvanced }: { > Express setup - Just the essentials: download and import TWIC, deduplicate, and build the + Just the essentials: populate the reference database, deduplicate, and build the position index. Skips the optional player-reference and existing-database steps. @@ -172,7 +171,7 @@ function WelcomeStep({ onExpress, onAdvanced }: { {[ "Import a player reference file", "Import your existing game collections", - "Download and import TWIC issues", + "Populate from curated reference sources", "Deduplicate the database", "Build the position index", ].map((text, i) => ( @@ -277,106 +276,221 @@ function DatabasesStep({ completed, onComplete, onRunningChange }: { completed: ); } -// ── Step: TWIC (download + import) ──────────────────────────────────────────── +// ── Step: Sources (populate the reference database) ─────────────────────────── +// +// Phase C2 (#98): onboarding is a simple binary — populate the reference +// database vs start empty — with all per-source detail deferred to +// Maintenance → Sources (C1). The "populate" path enables the chosen catalog +// sources (recording the attribution acknowledgment via the C1 `credit_acked` +// gate) and fires their sync jobs, which run on the daemon in the background; +// onboarding finishes immediately rather than blocking on a progress bar. + +type History = "free" | "commercial" | "none"; + +/** A non-commercial / restrictive licence we should visibly flag in the + * acknowledgment (e.g. CC BY-NC-SA sources like Lumbra's). Derived from the + * catalog credit line so new sources are flagged without extra metadata. */ +function isNonCommercial(credit: string): boolean { + return /non-?commercial|CC[\s-]?BY-NC|\bNC[\s-]?SA\b/i.test(credit); +} -/** Download/import progress block, shared by the two TWIC operations. - * `cancellable` is false for the fast (appender) import, which must not be - * interrupted — doing so can corrupt the database. */ -function StepProgress({ progress, label, cancellable = true }: { progress: ReturnType; label: string; cancellable?: boolean }) { +/** One acknowledgment row: tick to accept a source's attribution/licence. */ +function AckRow({ source, checked, onChange }: { source: SourceStatus; checked: boolean; onChange: (v: boolean) => void }) { + const nc = isNonCommercial(source.credit); return ( -
-
- {progress.done ? "Complete" : label} - {Math.round(progress.percent)}% -
- - {progress.done &&

✓ {progress.doneMessage}

} - {progress.running && !progress.done && cancellable && ( -
- -
- )} -
+ ); } -// Download and import TWIC in a single step (mirrors the Maintenance TWIC box). -// The step counts as complete once issues are imported into the database. -function TwicStep({ completed, onComplete, onRunningChange }: { completed: boolean; onComplete: () => void; onRunningChange: (r: boolean) => void }) { - // Shared with the Maintenance panel so the starting issue stays in sync. - const [fromIssue, setFromIssue] = useState(getTwicFrom()); +// Replaces the old inline "download + import TWIC with a progress bar" step. +// The step completes as soon as the chosen sources are enabled and their +// background sync jobs are submitted — the daemon does the work afterwards. +function SourcesStep({ completed, onComplete, onRunningChange }: { completed: boolean; onComplete: () => void; onRunningChange: (r: boolean) => void }) { const [rerunning, setRerunning] = useState(false); - const [twicAck, setTwicAck] = useTwicAck(); - const download = useSidecarProgress(); - const importProgress = useSidecarProgress(); + const [sources, setSources] = useState(null); + const [mode, setMode] = useState<"choice" | "configure">("choice"); + const [history, setHistory] = useState("free"); + const [acked, setAcked] = useState>(new Set()); + const [enabling, setEnabling] = useState(false); - useEffect(() => { if (importProgress.done) onComplete(); }, [importProgress.done]); - useEffect(() => { onRunningChange(download.running || importProgress.running); }, [download.running, importProgress.running]); + useEffect(() => { + getSources() + .then((s) => { setSources(s); setAcked(new Set(s.filter((x) => x.credit_acked).map((x) => x.key))); }) + .catch(() => setSources([])); + }, []); - // No --dir: the server downloads/imports into its own data dir - // (data_root()/twic) — passing a client path is wrong for the system daemon. - function runDownload() { - void download.run(["download", "--from", fromIssue]); + if (completed && !rerunning) { + return { setRerunning(true); setMode("choice"); }} />; } - function runImport() { - // --fast = appender-based bulk inserts (much quicker). It must not be - // interrupted, so the import progress below is rendered non-cancelable. - void importProgress.run(["import", "--fast"]); + + // Currency = the auto-updating feeds; deep history = the bulk archives. + const feeds = sources?.filter((s) => s.kind === "feed") ?? []; + const bulk = sources?.filter((s) => s.kind === "bulk") ?? []; + // Sources enabled by the primary button: the feeds always, plus the free + // historical base when chosen. (Commercial = the user's own local PGN import; + // None = feeds only.) + const selected = [...feeds, ...(history === "free" ? bulk : [])]; + const allAcked = selected.every((s) => acked.has(s.key)); + + function toggleAck(key: string, on: boolean) { + setAcked((prev) => { const next = new Set(prev); if (on) next.add(key); else next.delete(key); return next; }); } - if (completed && !rerunning) { - return setRerunning(true)} />; + function chooseEmpty() { + // Empty database: nothing to enable (sources default to off). Just finish. + onComplete(); + } + + async function enableAndImport() { + setEnabling(true); + onRunningChange(true); + try { + // Enable each chosen source (recording the acknowledgment in the same + // step, per the C1 credit_acked gate) and kick a background sync. The + // daemon runs these serially; we don't block onboarding on them. + for (const s of selected) { + await setSourceEnabled(s.key, true, true); + await submitJob({ type: "sources_sync", params: { source: s.key } }); + } + onComplete(); + } finally { + setEnabling(false); + onRunningChange(false); + } + } + + if (sources === null) { + return

Loading sources…

; + } + + // ── View 1: the binary choice ── + if (mode === "choice") { + return ( +
+

+ Populate your database with curated reference collections, or start empty and import only + your own games. You can change any of this later under Maintenance → Sources. +

+
+ + +
+
+ ); } - const filledBtn = "w-full h-10 rounded-full bg-primary text-on-primary text-label-lg hover:brightness-110 active:brightness-95 transition-all duration-short3 ease-standard"; + // ── View 2: configure the populate path ── + const filledBtn = "w-full h-10 rounded-full bg-primary text-on-primary text-label-lg hover:brightness-110 active:brightness-95 disabled:opacity-40 transition-all duration-short3 ease-standard"; + const historyOptions: { value: History; label: string; hint: string }[] = [ + { value: "free", label: "Free historical base", hint: bulk.map((s) => s.name).join(", ") || "A free public archive of older games." }, + { value: "commercial", label: "I own a commercial database", hint: "e.g. ChessBase Megabase — export to PGN and import it below." }, + { value: "none", label: "None", hint: "Just the live feeds for now." }, + ]; return ( -
-

- Download TWIC (The Week in Chess) issues and import them into your database, in one place. Issues already present or imported are skipped automatically. -

- +
+ - {/* Download */} -
-
Download
-
-
Download from issue
- { setFromIssue(e.target.value); setTwicFrom(e.target.value); }} - disabled={download.running || download.done} - className="w-32 h-10 px-3 rounded-sm bg-transparent text-on-surface text-body-md font-mono border border-outline focus:outline-none focus:border-primary disabled:opacity-50 transition-colors duration-short3 ease-standard" - /> -

Issues are available from 920 onwards.

+ {/* Currency — the auto-updating feeds, always part of populate. */} +
+
Keep current automatically
+

+ Recent tournament games, refreshed in the background. Acknowledge each source to enable it. +

+
+ {feeds.map((s) => ( + toggleAck(s.key, v)} /> + ))} +
+
+ + {/* Deep history — Free / Commercial / None. */} +
+
Deep history
+

+ An optional historical base beneath the live feeds. Overlap is deduplicated automatically. +

+
+ {historyOptions.map((opt) => ( + + ))}
- {!download.running && !download.done && ( - twicAck ? ( - - ) : ( -

Tick “I've read this” above to enable the download.

- ) + {history === "free" && bulk.length > 0 && ( +
+ {bulk.map((s) => ( + toggleAck(s.key, v)} /> + ))} +
)} - {(download.running || download.done) && } -
+ {history === "commercial" && ( +
+

+ Export your database to PGN, then import it here. You can also do this later, and cap the + live feeds to start after its cutoff under Maintenance → Sources. +

+ { /* wizard handles navigation */ }} + onImported={() => { /* optional — the button below still finishes the step */ }} + onRunningChange={onRunningChange} + /> +
+ )} + - {/* Import */} -
-
Import into database
- {!importProgress.running && !importProgress.done && ( - download.done ? ( - - ) : ( -

Download issues first to enable the import.

- ) +
+ + {!allAcked && ( +

Acknowledge each selected source above to continue.

)} - {(importProgress.running || importProgress.done) && } +

+ ⓘ Importing runs on the daemon — you can finish setup and even close LPDO; it keeps going. +

); @@ -611,7 +725,8 @@ function DoneStep() {

Your chess database is ready to use.

- Download newer TWIC issues and run maintenance operations at any time from the Setup panel in the header. + Manage your reference sources and run maintenance operations at any time from + Maintenance → Sources, or re-open this wizard from the header.

); @@ -658,7 +773,7 @@ export default function SetupWizard({ onClose, onFinish }: Props) { function back() { if (stepIndex > 0) setStepIndex((i) => i - 1); } // Welcome choices. Advanced walks every step; Express marks the two optional - // import steps (players, databases) as skipped and jumps straight to TWIC. + // import steps (players, databases) as skipped and jumps straight to Sources. function startAdvanced() { markComplete("welcome"); next(); @@ -666,7 +781,7 @@ export default function SetupWizard({ onClose, onFinish }: Props) { function startExpress() { markComplete("welcome"); setSkippedSteps((prev) => new Set([...prev, "players", "databases"])); - setStepIndex(STEPS.indexOf("twic")); + setStepIndex(STEPS.indexOf("sources")); } const stepProps = (s: Step) => ({ @@ -719,7 +834,7 @@ export default function SetupWizard({ onClose, onFinish }: Props) { {step === "welcome" && } {step === "players" && } {step === "databases" && } - {step === "twic" && } + {step === "sources" && } {step === "dedup" && } {step === "normalise" && } {step === "index" && } From 9dc14a9a3e60a965ad76e6396ce699988f4101d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jozef=20Svr=C4=8Dek?= <24891922+jozef2svrcek@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:07:55 +0200 Subject: [PATCH 2/4] Multi-source C2 review fixes: surface failures + fix commercial-mode guards (#98) Address the high-effort code review of the new SourcesStep: - enableAndImport now catches a failing setSourceEnabled/submitJob, surfaces the error, and keeps the user on the step instead of silently dropping the failure (earlier sources may already be enabled; re-clicking is safe). - getSources failures show a distinct error + Retry rather than collapsing into an empty catalog that looks like "zero sources". - The primary button is gated on a non-empty selection, so an empty selection can no longer "complete" a no-op populate while reporting success. - The step's busy state is reported as (enabling || importing) via one effect, so the brief enable loop no longer clears the close guard while a commercial PGN import is still running. - Commercial mode: the embedded import now completes the step on success, and the primary button is relabelled "Enable live feeds & finish" with copy clarifying the commercial DB is imported separately. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018gaUhu1KVkdxAjSJBLgbbk --- chess-client/src/components/SetupWizard.tsx | 68 ++++++++++++++++++--- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/chess-client/src/components/SetupWizard.tsx b/chess-client/src/components/SetupWizard.tsx index ef33bd1..8763fb3 100644 --- a/chess-client/src/components/SetupWizard.tsx +++ b/chess-client/src/components/SetupWizard.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { useSidecarProgress } from "../hooks/useSidecarProgress"; import AddGameDialog from "./AddGameDialog"; @@ -317,16 +317,29 @@ function AckRow({ source, checked, onChange }: { source: SourceStatus; checked: function SourcesStep({ completed, onComplete, onRunningChange }: { completed: boolean; onComplete: () => void; onRunningChange: (r: boolean) => void }) { const [rerunning, setRerunning] = useState(false); const [sources, setSources] = useState(null); + const [loadError, setLoadError] = useState(null); const [mode, setMode] = useState<"choice" | "configure">("choice"); const [history, setHistory] = useState("free"); const [acked, setAcked] = useState>(new Set()); const [enabling, setEnabling] = useState(false); + const [importing, setImporting] = useState(false); + const [submitError, setSubmitError] = useState(null); - useEffect(() => { + const loadSources = useCallback(() => { + setLoadError(null); + setSources(null); getSources() .then((s) => { setSources(s); setAcked(new Set(s.filter((x) => x.credit_acked).map((x) => x.key))); }) - .catch(() => setSources([])); + .catch((e: unknown) => setLoadError(String(e))); }, []); + useEffect(() => { loadSources(); }, [loadSources]); + + // Report this step's combined busy state to the wizard: the brief enable + // loop OR an in-progress commercial PGN import. Reporting the OR (rather than + // letting each writer set the shared flag directly) stops the enable loop's + // completion from clearing the "operation in progress" close guard while a + // commercial import is still running. + useEffect(() => { onRunningChange(enabling || importing); }, [enabling, importing, onRunningChange]); if (completed && !rerunning) { return { setRerunning(true); setMode("choice"); }} />; @@ -351,8 +364,8 @@ function SourcesStep({ completed, onComplete, onRunningChange }: { completed: bo } async function enableAndImport() { + setSubmitError(null); setEnabling(true); - onRunningChange(true); try { // Enable each chosen source (recording the acknowledgment in the same // step, per the C1 credit_acked gate) and kick a background sync. The @@ -362,12 +375,29 @@ function SourcesStep({ completed, onComplete, onRunningChange }: { completed: bo await submitJob({ type: "sources_sync", params: { source: s.key } }); } onComplete(); + } catch (e: unknown) { + // Surface the failure and stay on the step rather than silently dropping + // it. Earlier sources in the loop may already be enabled + sync-submitted; + // re-clicking is safe (enabling is idempotent and overlap is deduplicated). + setSubmitError(`Couldn’t enable the selected sources: ${String(e)} — some may already be enabled; you can retry.`); } finally { setEnabling(false); - onRunningChange(false); } } + if (loadError !== null) { + return ( +
+

Couldn’t load the source catalog: {loadError}

+ +
+ ); + } if (sources === null) { return

Loading sources…

; } @@ -474,19 +504,37 @@ function SourcesStep({ completed, onComplete, onRunningChange }: { completed: bo allowedModes={["file"]} bulk onClose={() => { /* wizard handles navigation */ }} - onImported={() => { /* optional — the button below still finishes the step */ }} - onRunningChange={onRunningChange} + onImported={onComplete} + onRunningChange={setImporting} />
)}
- - {!allAcked && ( + {selected.length === 0 ? ( +

+ No reference sources to enable.{history === "commercial" ? " Import your database above to populate it." : ""} +

+ ) : !allAcked ? (

Acknowledge each selected source above to continue.

+ ) : null} + {submitError &&

{submitError}

} + {history === "commercial" && ( +

+ This enables the live feeds only — import your commercial database separately above (now or later). +

)}

ⓘ Importing runs on the daemon — you can finish setup and even close LPDO; it keeps going. From 41620119fe6578dd7fa0e11d69fc7411f57c40a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jozef=20Svr=C4=8Dek?= <24891922+jozef2svrcek@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:39:19 +0200 Subject: [PATCH 3/4] C2 wizard: drop the explicit sources_sync, rely on C3 auto-sync (#98) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With Phase C3 (#99), enabling a source is enough — the daemon's scheduler picks up enabled-but-not-yet-synced sources and imports them in the background. So the onboarding wizard no longer submits a sources_sync per source; it just enables them (with the credit acknowledgment) and finishes. This resolves the C2 review follow-ups #6/#7 (no completion-before-data race from the wizard's side, and no redundant/duplicate sync jobs). Stacked on the C3 branch. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018gaUhu1KVkdxAjSJBLgbbk --- chess-client/src/components/SetupWizard.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/chess-client/src/components/SetupWizard.tsx b/chess-client/src/components/SetupWizard.tsx index 8763fb3..06d4175 100644 --- a/chess-client/src/components/SetupWizard.tsx +++ b/chess-client/src/components/SetupWizard.tsx @@ -3,7 +3,7 @@ import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { useSidecarProgress } from "../hooks/useSidecarProgress"; import AddGameDialog from "./AddGameDialog"; import { ProfileSetupForm, loadMyPlayer, saveMyPlayer } from "./MyStatsWidget"; -import { getSources, setSourceEnabled, submitJob } from "../api"; +import { getSources, setSourceEnabled } from "../api"; import { PlayerInfo, SourceStatus } from "../types"; interface Props { @@ -367,18 +367,18 @@ function SourcesStep({ completed, onComplete, onRunningChange }: { completed: bo setSubmitError(null); setEnabling(true); try { - // Enable each chosen source (recording the acknowledgment in the same - // step, per the C1 credit_acked gate) and kick a background sync. The - // daemon runs these serially; we don't block onboarding on them. + // Just enable each chosen source (recording the acknowledgment in the same + // step, per the C1 credit_acked gate). The daemon's scheduler picks up + // enabled-but-not-yet-synced sources and imports them in the background + // (#40 C3), so onboarding doesn't submit or wait on any sync itself. for (const s of selected) { await setSourceEnabled(s.key, true, true); - await submitJob({ type: "sources_sync", params: { source: s.key } }); } onComplete(); } catch (e: unknown) { // Surface the failure and stay on the step rather than silently dropping - // it. Earlier sources in the loop may already be enabled + sync-submitted; - // re-clicking is safe (enabling is idempotent and overlap is deduplicated). + // it. Earlier sources in the loop may already be enabled; re-clicking is + // safe (enabling is idempotent). setSubmitError(`Couldn’t enable the selected sources: ${String(e)} — some may already be enabled; you can retry.`); } finally { setEnabling(false); From 130ebee79972dcd7ed8404d9e4e619c3c41c5599 Mon Sep 17 00:00:00 2001 From: jozef2svrcek Date: Tue, 30 Jun 2026 23:43:54 +0200 Subject: [PATCH 4/4] C2 wizard: simplify steps, split sources, automatic maintenance (#98) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce the setup wizard from 9 steps to 5 and align it with the async, daemon-driven import model: - Welcome: drop the Express/Advanced fork for a single "Get started" intro. - Remove the Players-import and own-database (PGN) import steps. - Split the old Sources step into two: Deep history (first) then Live feeds. - Fix the feed-selection bug: each source is now an independent checkbox where ticking IS the licence acknowledgment (enable + credit_acked in one gesture). Feeds are no longer forced — any subset (or none) can be chosen. - Drop the manual Dedup/Names/Index steps. With imports running asynchronously on the daemon, firing them at wizard time would run them before the games arrived. They are now surfaced on the Summary screen as automatic background maintenance (import -> dedup -> index -> normalise), with a pointer to the header activity indicator. The efficient, prompt, transparent first-run maintenance pipeline this Summary copy describes is tracked separately in #109. Bumps the wizard localStorage key to -v3 so stale persisted state from the old step set is discarded rather than mis-mapped. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LNz6kobBkfpYCxLgfTykcd --- chess-client/src/components/SetupWizard.tsx | 818 ++++++-------------- 1 file changed, 255 insertions(+), 563 deletions(-) diff --git a/chess-client/src/components/SetupWizard.tsx b/chess-client/src/components/SetupWizard.tsx index 06d4175..f0b0777 100644 --- a/chess-client/src/components/SetupWizard.tsx +++ b/chess-client/src/components/SetupWizard.tsx @@ -1,6 +1,4 @@ import { useState, useEffect, useCallback } from "react"; -import { open as openDialog } from "@tauri-apps/plugin-dialog"; -import { useSidecarProgress } from "../hooks/useSidecarProgress"; import AddGameDialog from "./AddGameDialog"; import { ProfileSetupForm, loadMyPlayer, saveMyPlayer } from "./MyStatsWidget"; import { getSources, setSourceEnabled } from "../api"; @@ -13,23 +11,25 @@ interface Props { onFinish?: () => void; } -type Step = "welcome" | "players" | "databases" | "sources" | "dedup" | "normalise" | "index" | "profile" | "done"; +type Step = "welcome" | "history" | "feeds" | "profile" | "done"; -const STEPS: Step[] = ["welcome", "players", "databases", "sources", "dedup", "normalise", "index", "profile", "done"]; +const STEPS: Step[] = ["welcome", "history", "feeds", "profile", "done"]; const STEP_LABELS: Record = { welcome: "Welcome", - players: "Players", - databases: "Databases", - sources: "Sources", - dedup: "Dedup", - normalise: "Names", - index: "Index", + history: "History", + feeds: "Feeds", profile: "Profile", done: "Summary", }; -const OPTIONAL_STEPS: Step[] = ["players", "databases", "sources", "dedup", "normalise", "index", "profile"]; -const STORAGE_KEY = "chess-setup-state"; +const OPTIONAL_STEPS: Step[] = ["history", "feeds", "profile"]; +// Bumped to -v3 with the Phase-C2 step restructure: welcome fork + players/ +// databases steps removed, Sources split into History+Feeds, and the manual +// Dedup/Names/Index steps dropped (the daemon now does that maintenance +// automatically after import — surfaced on the Summary screen). Bumping the key +// discards stale persisted state from any earlier step set rather than +// mis-mapping it. +const STORAGE_KEY = "chess-setup-state-v3"; interface PersistedState { stepIndex: number; @@ -51,58 +51,6 @@ function saveState(state: PersistedState) { // ── Shared UI ───────────────────────────────────────────────────────────────── -function ProgressBar({ value }: { value: number }) { - return ( -

-
-
- ); -} - -function FolderInput({ value, onChange, placeholder, disabled, directory = false, extensions }: { - value: string; onChange: (v: string) => void; placeholder?: string; disabled?: boolean; - /** Pick a folder instead of a file. */ - directory?: boolean; - /** Restrict the file picker to these extensions (ignored when directory). */ - extensions?: string[]; -}) { - async function browse() { - try { - const picked = await openDialog({ - multiple: false, - directory, - filters: !directory && extensions ? [{ name: extensions.map((e) => e.toUpperCase()).join("/"), extensions }] : undefined, - }); - // Returns a string path (or null if cancelled); never an array here. - if (typeof picked === "string") onChange(picked); - } catch { - /* user cancelled / dialog unavailable — leave the field unchanged */ - } - } - - return ( -
- onChange(e.target.value)} - placeholder={placeholder} - disabled={disabled} - className="flex-1 h-10 px-3 rounded-sm bg-transparent text-on-surface text-body-md font-mono border border-outline focus:outline-none focus:border-primary placeholder:text-on-surface-variant disabled:opacity-50 transition-colors duration-short3 ease-standard" - /> - {/* M3 outlined button */} - -
- ); -} - function OptionalBadge() { /* M3 assist chip */ return Optional; @@ -127,53 +75,22 @@ function CompletedBanner({ summary, onRerun }: { summary: string; onRerun: () => // ── Step: Welcome ───────────────────────────────────────────────────────────── -function WelcomeStep({ onExpress, onAdvanced }: { - onExpress: () => void; - onAdvanced: () => void; -}) { +function WelcomeStep({ onStart }: { onStart: () => void }) { return (

- This wizard will guide you through setting up your chess database. - You can stop at any time and continue later — completed steps are remembered. + This wizard sets up your chess reference database. You'll pick an optional historical + base and the live tournament feeds to keep current — then LPDO downloads, imports and + prepares everything for you in the background. You can stop at any time and continue + later; completed steps are remembered, and you can change everything afterwards under + Maintenance → Sources.

-
- {/* Express — recommended; primary-tinted to draw the eye. */} - - - {/* Advanced — every step, including the optional imports. */} - -
-
    {[ - "Import a player reference file", - "Import your existing game collections", - "Populate from curated reference sources", - "Deduplicate the database", - "Build the position index", + "Choose a deep historical base", + "Choose the live tournament feeds to follow", + "Set your player profile", ].map((text, i) => (
  • {i + 1}. @@ -181,121 +98,39 @@ function WelcomeStep({ onExpress, onAdvanced }: {
  • ))}
-
- ); -} - -// ── Step: Import players file ───────────────────────────────────────────────── - -function PlayersStep({ completed, onComplete, onRunningChange }: { completed: boolean; onComplete: () => void; onRunningChange: (r: boolean) => void }) { - const [path, setPath] = useState(""); - const [rerunning, setRerunning] = useState(false); - const progress = useSidecarProgress(); - - useEffect(() => { if (progress.done) onComplete(); }, [progress.done]); - useEffect(() => { onRunningChange(progress.running); }, [progress.running]); - function run() { - void progress.run(["players", "import", path]); - } - - if (completed && !rerunning) { - return setRerunning(true)} />; - } - - return ( -
-
-

- Import a pre-built player reference file containing canonical names and FIDE IDs. - This significantly improves player search and name consistency. -

- -
-
-
Player reference file
- -
- {path && !progress.running && !progress.done && ( - - )} - {(progress.running || progress.done) && ( -
-
- {progress.done ? "Complete" : "Importing…"} - {Math.round(progress.percent)}% -
- - {progress.done &&

✓ {progress.doneMessage}

} - {progress.running && !progress.done && ( -
- -
- )} -
- )} -
- ); -} - -// ── Step: Import pre-owned databases ───────────────────────────────────────── - -function DatabasesStep({ completed, onComplete, onRunningChange }: { completed: boolean; onComplete: () => void; onRunningChange: (r: boolean) => void }) { - const [rerunning, setRerunning] = useState(false); - - if (completed && !rerunning) { - return setRerunning(true)} />; - } - - return ( -
-
-

- Import any PGN files you already have — your own games, a tournament you follow, a repertoire. - Pick a preset to get started. -

- -
- { /* no-op: wizard handles step navigation */ }} - onImported={onComplete} - onRunningChange={onRunningChange} - /> +
); } -// ── Step: Sources (populate the reference database) ─────────────────────────── +// ── Sources (multi-source import catalog, #40) ──────────────────────────────── // -// Phase C2 (#98): onboarding is a simple binary — populate the reference -// database vs start empty — with all per-source detail deferred to -// Maintenance → Sources (C1). The "populate" path enables the chosen catalog -// sources (recording the attribution acknowledgment via the C1 `credit_acked` -// gate) and fires their sync jobs, which run on the daemon in the background; -// onboarding finishes immediately rather than blocking on a progress bar. - -type History = "free" | "commercial" | "none"; - -/** A non-commercial / restrictive licence we should visibly flag in the - * acknowledgment (e.g. CC BY-NC-SA sources like Lumbra's). Derived from the - * catalog credit line so new sources are flagged without extra metadata. */ +// Phase C2 (#98): onboarding is split into two simple steps — pick a deep +// historical base (this step), then pick the live feeds — with all per-source +// detail (date windows, timeline, manual sync) deferred to Maintenance → Sources +// (C1). Selecting a source IS its acknowledgment: a ticked row enables the +// source and records the attribution acknowledgment via the C1 `credit_acked` +// gate in one gesture, and sources are independently selectable (no feed is +// forced). The daemon's scheduler picks up enabled-but-not-yet-synced sources +// and imports them in the background (#40 C3), so each step finishes immediately. + +/** A non-commercial / restrictive licence we should visibly flag (e.g. + * CC BY-NC-SA sources). Derived from the catalog credit line so new sources are + * flagged without extra metadata. */ function isNonCommercial(credit: string): boolean { return /non-?commercial|CC[\s-]?BY-NC|\bNC[\s-]?SA\b/i.test(credit); } -/** One acknowledgment row: tick to accept a source's attribution/licence. */ -function AckRow({ source, checked, onChange }: { source: SourceStatus; checked: boolean; onChange: (v: boolean) => void }) { +/** One selectable source row: tick to include the source (which also accepts its + * attribution/licence). The credit line is shown right here so the tick is an + * informed acknowledgment. */ +function SourceRow({ source, checked, onChange }: { source: SourceStatus; checked: boolean; onChange: (v: boolean) => void }) { const nc = isNonCommercial(source.credit); return (
); } -// ── Step: Deduplicate ───────────────────────────────────────────────────────── +// ── Step: Live feeds (auto-updating sources) ────────────────────────────────── -function DedupStep({ completed, onComplete, onRunningChange }: { completed: boolean; onComplete: () => void; onRunningChange: (r: boolean) => void }) { +function FeedsStep({ completed, onComplete, onRunningChange }: { completed: boolean; onComplete: () => void; onRunningChange: (r: boolean) => void }) { const [rerunning, setRerunning] = useState(false); - const progress = useSidecarProgress(); + const { sources, loadError, reload } = useSourceCatalog(); + const [selected, setSelected] = useState>(new Set()); + const [enabling, setEnabling] = useState(false); + const [submitError, setSubmitError] = useState(null); - useEffect(() => { if (progress.done) onComplete(); }, [progress.done]); - useEffect(() => { onRunningChange(progress.running); }, [progress.running]); + // Pre-select feeds already enabled (TWIC defaults on; and on re-run). + useEffect(() => { + if (!sources) return; + setSelected(new Set(sources.filter((s) => s.kind === "feed" && s.enabled).map((s) => s.key))); + }, [sources]); - function run() { - void progress.run(["games", "dedup"]); - } + useEffect(() => { onRunningChange(enabling); }, [enabling, onRunningChange]); if (completed && !rerunning) { - return setRerunning(true)} />; + return setRerunning(true)} />; } + if (loadError !== null) return ; + if (sources === null) return

Loading sources…

; - return ( -
-

- Detect and remove duplicate games that may result from overlapping collections. -

- {!progress.running && !progress.done && ( - - )} - {(progress.running || progress.done) && ( -
-
- {progress.done ? "Complete" : "Scanning…"} - {Math.round(progress.percent)}% -
- - {progress.done &&

✓ {progress.doneMessage}

} - {progress.running && !progress.done && ( -
- -
- )} -
- )} -
- ); -} - -// ── Step: Normalise player names ────────────────────────────────────────────── - -const NORMALISE_LIMIT = 500; + const feeds = sources.filter((s) => s.kind === "feed"); -function NormaliseStep({ completed, onComplete, onRunningChange }: { completed: boolean; onComplete: () => void; onRunningChange: (r: boolean) => void }) { - const [rerunning, setRerunning] = useState(false); - const progress = useSidecarProgress(); - - useEffect(() => { if (progress.done) onComplete(); }, [progress.done]); - useEffect(() => { onRunningChange(progress.running); }, [progress.running]); - - function run() { - // --stop-on-errors: abort immediately on 10 consecutive FIDE errors rather - // than pausing for hours, so the wizard never appears to hang. - void progress.run(["players", "normalise", "--limit", String(NORMALISE_LIMIT), "--stop-on-errors"]); + function toggle(key: string, on: boolean) { + setSelected((prev) => { const next = new Set(prev); if (on) next.add(key); else next.delete(key); return next; }); } - if (completed && !rerunning) { - return setRerunning(true)} />; + async function apply() { + setSubmitError(null); + setEnabling(true); + try { + await applySourceSelection(feeds, selected); + onComplete(); + } catch (e: unknown) { + setSubmitError(`Couldn't apply your choice: ${String(e)} — some feeds may already be enabled; you can retry.`); + } finally { + setEnabling(false); + } } return ( -
-

- Update player names to their FIDE-canonical form by looking up each player's FIDE ID. - This improves search results and name consistency. -

- {/* Names already in our cache are resolved instantly (one request, no - limit); only the slow FIDE lookups for the rest are capped. */} -
-

- Names in our shared cache are resolved instantly. To keep setup quick, the - remaining online FIDE lookups are capped at{" "} - {NORMALISE_LIMIT} for this step. -

-

- For a complete pass — recommended especially if you didn't import a player reference file — - run chess-db players normalise from the command line. +

+
+

+ Choose which live tournament feeds to follow. They refresh automatically in the + background to keep recent games current. Ticking a feed accepts its source/licence. + You can change this later under Maintenance → Sources.

+
- {/* Run (also shown again after a stop, so the user can retry or skip). */} - {!progress.running && !progress.done && ( - - )} - {(progress.running || progress.done) && ( + + {feeds.length === 0 ? ( +

No live feeds are available right now.

+ ) : (
-
- {progress.done ? "Complete" : "Looking up FIDE names…"} - {Math.round(progress.percent)}% -
- - {progress.done &&

✓ {progress.doneMessage}

} - {progress.running && !progress.done && ( -
- -
- )} -
- )} - {/* Backend log: the "limited to N of M" warning, and any "stopped after N - consecutive errors" message. Kept visible after the run stops. */} - {progress.log.length > 0 && ( -
- {progress.log.slice(-6).map((l, i) =>
{l}
)} + {feeds.map((s) => ( + toggle(s.key, v)} /> + ))}
)} -
- ); -} - -// ── Step: Index positions ───────────────────────────────────────────────────── -function IndexStep({ completed, onComplete, onRunningChange }: { completed: boolean; onComplete: () => void; onRunningChange: (r: boolean) => void }) { - const [rerunning, setRerunning] = useState(false); - const progress = useSidecarProgress(); - - useEffect(() => { if (progress.done) onComplete(); }, [progress.done]); - useEffect(() => { onRunningChange(progress.running); }, [progress.running]); - - function run() { - // --fast = appender-based inserts (much quicker than row-by-row - // transactional inserts, which crawl on a full-database rebuild). Like the - // import step it must not be interrupted, so the progress below renders no - // Cancel button. - void progress.run(["index-positions", "--fast"]); - } - - if (completed && !rerunning) { - return setRerunning(true)} />; - } - - return ( -
-

- Build the position index — required for the move explorer. Each game is replayed and every position is recorded with its Zobrist hash. -

- {!progress.running && !progress.done && ( - - )} - {(progress.running || progress.done) && ( -
-
- {progress.done ? "Complete" : "Indexing…"} - {Math.round(progress.percent)}% -
- - {progress.done &&

✓ {progress.doneMessage}

} - {/* No Cancel button: --fast (appender) indexing must not be - interrupted — doing so can corrupt the database. */} -
- )} + {submitError &&

{submitError}

} +

+ ⓘ Imports run on the daemon — you can finish setup and even close LPDO; it keeps going. +

+
); } @@ -770,10 +456,32 @@ function DoneStep() {

Setup complete

-

Your chess database is ready to use.

+

+ Your database is now being prepared in the background — no further steps needed. +

+ + {/* The maintenance the wizard used to ask the user to run by hand + (deduplicate / index / normalise) now happens automatically once the + chosen sources import. We surface it here so the process is visible, + rather than as manual steps that — with imports running asynchronously + on the daemon — would otherwise run before the games arrived. */} +
+

What happens now, automatically

+
    +
  1. Your selected sources download and import — the daemon keeps going even if you close LPDO.
  2. +
  3. Imported games are deduplicated.
  4. +
  5. The position index is built — this powers the move explorer.
  6. +
  7. Player names are normalised to their FIDE-canonical form.
  8. +
+

+ You don't need to run any of these by hand. After a first-time import these can take + a while; follow progress any time from the activity indicator in the header. +

+
+

- Manage your reference sources and run maintenance operations at any time from + Manage your reference sources and run maintenance manually at any time from Maintenance → Sources, or re-open this wizard from the header.

@@ -820,18 +528,6 @@ export default function SetupWizard({ onClose, onFinish }: Props) { function skip() { setSkippedSteps((prev) => new Set([...prev, step])); next(); } function back() { if (stepIndex > 0) setStepIndex((i) => i - 1); } - // Welcome choices. Advanced walks every step; Express marks the two optional - // import steps (players, databases) as skipped and jumps straight to Sources. - function startAdvanced() { - markComplete("welcome"); - next(); - } - function startExpress() { - markComplete("welcome"); - setSkippedSteps((prev) => new Set([...prev, "players", "databases"])); - setStepIndex(STEPS.indexOf("sources")); - } - const stepProps = (s: Step) => ({ completed: completedSteps.has(s), onComplete: () => markComplete(s), @@ -879,13 +575,9 @@ export default function SetupWizard({ onClose, onFinish }: Props) { {/* Body */}
- {step === "welcome" && } - {step === "players" && } - {step === "databases" && } - {step === "sources" && } - {step === "dedup" && } - {step === "normalise" && } - {step === "index" && } + {step === "welcome" && { markComplete("welcome"); next(); }} />} + {step === "history" && } + {step === "feeds" && } {step === "profile" && markComplete("profile")} />} {step === "done" && }
@@ -910,7 +602,7 @@ export default function SetupWizard({ onClose, onFinish }: Props) {
) : step === "welcome" ? ( - // Navigation for Welcome is the Express / Advanced choice in the body. + // Navigation for Welcome is the "Get started" button in the body. null ) : completedSteps.has(step) ? (