diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5e0d851..8b12a47 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -99,9 +99,10 @@ JSON as-is. ## Component tree -``` App -├─ Header name, image, date steppers, session steppers, Save/Load/Clear (dead) +├─ Header name, image, date steppers, session steppers, +│ Save (download)/Load (file import)/Clear (wipe) +├─ Modal overlay dialog: confirm (action) or error (close) ├─ Sidebar │ ├─ Image portrait │ ├─ Names first / alias «» / last @@ -125,6 +126,13 @@ closures by index; "move" transfers a row between the personal and common inventories, "remove" deletes it. The "…" (details) button and the search "+" button are unwired. +Persistence lives in `state::persist`: on hydration a deferred timer loads +the saved dashboard from localStorage (SSR markup stays authoritative), +then a debounced (500 ms) effect writes on any store change via +`store.track()`. `parse_dashboard` version-checks the JSON against +`Settings::VERSION` (the migration hook). `ModalState` holds title/message/ +action as signals; the confirm button reads the action at click time. + `Numput` is the shared numeric input: `+n`/`-n` adjust relative to the current value, a plain number sets absolutely, invalid input is reverted, and the value renders with thousands separators. `NumputChange` is the same @@ -148,12 +156,11 @@ currently mounted on any page. ## Current gaps -- No persistence; state is recreated from `mock()` each load -- `Character` and `Wiki` pages render placeholders - Quests, notes, contacts: models and mock data only +- `Character` and `Wiki` pages render placeholders - Prepared spells list is empty; `Spell` is a name-only stub; attunements - have no UI; `Settings` is empty -- Save/Load/Clear buttons and the item "+"/"…" buttons are dead + have no UI; `Settings` has only `version` +- The item "+"/"…" buttons are dead - `foundry.json` was loaded twice (fixed); ~3.9 MB of JSON is bundled into the WASM and parsed on startup - Several unused dependencies (see `MODERNIZE.md`); kept for now by diff --git a/MODERNIZE.md b/MODERNIZE.md index ec95bae..74df88b 100644 --- a/MODERNIZE.md +++ b/MODERNIZE.md @@ -71,9 +71,11 @@ under consideration. `usize`. `Numput` keeps its generics. 4. **`serde` (direct) → remove now, re-add in ROADMAP phase 1.** - No derives exist today; `serde_json::Value` works without naming `serde` - in `Cargo.toml`. Phase 1 (persistence) re-adds + No derives existed at the time; `serde_json::Value` works without naming + `serde` in `Cargo.toml`. Phase 1 (persistence) re-added `serde = { version = "1", features = ["derive"] }`. + **APPLIED 2026-08-16:** persistence is in; `serde` is used by every entity + derive — keep it. Result: dropping the under-consideration items takes `[dependencies]` from 27 entries to about 18. If the five kept deps are dropped later too, it goes diff --git a/README.md b/README.md index 2aebf90..1187f36 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ game. The UI is in Russian. ## What works today -- Campaign header: name, in-game date steppers, session counter +- Campaign header: name, in-game date steppers, session counter, and + Save / Load / Clear: download `dashboard.json`, import a file back, + wipe to a blank campaign - Character sidebar: portrait, names, class, level, XP, hit points with temp HP bar, hit dice, death saves, spell slots - Inventories page (`/inv`): personal and party-common inventories, @@ -14,8 +16,9 @@ game. The UI is in Russian. - Bundled 5e.tools item data (~5k items) with fuzzy search components (search is not mounted on a page yet) -State is mock data: nothing is persisted, and several buttons and pages are -stubs. The phased plan to fix that is in [ROADMAP.md](./ROADMAP.md). +State autosaves to localStorage (debounced) and survives reloads; export/ +import round-trips through a JSON file. The phased plan is in +[ROADMAP.md](./ROADMAP.md). ## Tech stack @@ -48,13 +51,14 @@ cargo check --target wasm32-unknown-unknown --no-default-features --features hyd ## Tests -End-to-end smoke tests live in `end2end/` (Playwright). With the dev server -running on port 3000: +End-to-end tests live in `end2end/` (Playwright): a smoke spec and a +persistence spec (reload keeps state; export/import round trip). With the +dev server running: ```sh cd end2end npm install -npx playwright test +npx playwright test --project=chromium ``` or let cargo-leptos manage the server: `cargo leptos end2end`. diff --git a/ROADMAP.md b/ROADMAP.md index 39f808a..e9aee6a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -72,24 +72,32 @@ Cheap, unblocks the rest. Can happen any time; do it first. The foundation. Every later phase depends on it. -- [ ] derive `serde::Serialize` / `Deserialize` on all entities in `src/entities/mod.rs` +- [x] derive `serde::Serialize` / `Deserialize` on all entities in `src/entities/mod.rs` (Dashboard, PlayerData, Balance, Inventory, InventoryEntry, InventoryItem, QuestBook, Quest, NoteBook, Note, ContactBook, Contact, SpellSlots, Attunements, PreparedSpells, Spell, HitDice, DeathSaveThrows, Name, Settings; enums: InventoryKind, ContactStatus, PreparedSpell) -- [ ] verify `Store` derive and `serde` derive coexist on the same structs (prototype first) -- [ ] add a `version` field to `Settings`; add a migration hook for future format changes -- [ ] serialize Dashboard to JSON; save to localStorage via `web_sys::Storage` +- [x] verify `Store` derive and `serde` derive coexist on the same structs (prototype first) +- [x] add a `version` field to `Settings`; add a migration hook for future format changes +- [x] serialize Dashboard to JSON; save to localStorage via `web_sys::Storage` (feature-gate to `hydrate`/`csr`; SSR renders mock/empty) -- [ ] auto-save on change — debounced effect over the `Store` — plus manual Save button -- [ ] Load button: restore from localStorage with an overwrite confirm -- [ ] Clear button: wipe localStorage with a confirm -- [ ] export: download `dashboard.json` as a blob -- [ ] import: file input → parse → validate version → replace state -- [ ] on startup, hydrate from localStorage; keep `mock()` as the fallback / "New dashboard" +- [x] auto-save on change — debounced effect over the `Store` — plus manual Save button +- [x] Load button: restore from localStorage with an overwrite confirm +- [x] Clear button: wipe localStorage with a confirm +- [x] export: download `dashboard.json` as a blob +- [x] import: file input → parse → validate version → replace state +- [x] on startup, hydrate from localStorage; keep `mock()` as the fallback / "New dashboard" - **Acceptance:** close the tab, reopen — state is intact. Export → import into a fresh browser — identical state. +**Done (2026-08-16):** implementation in `src/state/persist.rs` + header buttons + +`components/modal.rs`; e2e-verified in chromium (reload persistence and +export/import round trip). Notes: +- Save = download; Load = file import; Clear = wipe (merged per user decision). +- Clear writes the blank dashboard back to storage; mock stays the first-visit fallback. +- Version check is the migration hook: `parse_dashboard` returns + `ImportError::{Parse, NewerVersion, OldVersion}` (RU messages). + ## Phase 2 — Character page (M, ~1-2 days) `/` currently renders the literal string "character".