aex/ROADMAP.md
yaroslav k 5e7cd0e709 docs: second-pass character page notes
- ROADMAP: Phase 2 done-note — player card removed, plain RU headings,
  growable attunements, manual AC
- ARCHITECTURE: character tree line and route contents updated
- README: character page and sidebar lines updated (no player card, AC)
2026-08-16 13:08:03 +03:00

246 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ROADMAP — aex
A D&D 5e character sheet and campaign dashboard. One page per concern: character,
inventory, quests, notes, contacts, item reference. Built with Leptos 0.8 (SSR +
hydration), actix-web, SCSS.
## MVP definition
The MVP is done when a player can run a real session with the app:
- the app opens with last session's state (survives tab close / reload)
- the player can track HP, spell slots, hit dice, death saves, money, inventory
(add, remove, move items — including from item search)
- the player can record quests, notes, and contacts as the session happens
- the player can look up any item's stats and add it to inventory
- the player can back up the dashboard to a JSON file and restore it
## Current state (Feb 2026)
| Area | Status |
|---|---|
| Header: campaign name, image, in-game date, session steppers | Works |
| Header: Save / Load / Clear buttons | Dead (no persistence) |
| Sidebar: image, names, class/level/XP, HP, hit dice, death saves, spell slots | Works |
| Character page `/` | Stub — renders literal text "character" |
| Inventory page `/inv`: personal + common, money, item rows | Works |
| Item search (nucleo over ~5k bundled 5e.tools items) | Component exists, wired to no page |
| Item "+" (add) and "…" (details) buttons | Dead |
| Quests / Notes / Contacts | Models + mock data exist, zero UI, no routes |
| Prepared spells | Model exists, list is empty; `Spell` is a `@TODO` stub |
| Attunements | Model exists, no UI |
| Settings | Empty struct |
| Persistence | None — hardcoded `Dashboard::mock()` on every load |
| e2e tests | Example Playwright spec only |
| README | Stock Leptos template |
## Non-goals (explicitly out of MVP)
- accounts, multi-user, or sync across devices
- server-side database
- dice rolling, character builder, full rulebook
- multiple campaigns / dashboards switching
- images hosted by the app (remote URLs are fine)
## Effort legend
- S — ~0.5 day
- M — ~1-2 days
- L — ~3-5 days
---
## Phase 0 — Build hygiene (S, ~0.5-1 day)
Cheap, unblocks the rest. Can happen any time; do it first.
- [x] rewrite README (currently the stock template)
- [x] deduplicate `foundry.json` load in `src/statics/mod.rs` (loaded twice)
- [x] clear compiler warnings (unused imports, unused `qty` in `item.rs`)
- [x] decide Cargo.lock policy — currently gitignored, builds are non-reproducible; commit it
(done: committed 2026-08-04)
- [~] dependency and idiom cleanup — see `MODERNIZE.md` (drop unused deps,
`lazy_static``LazyLock`, local replacements for `num-format`/`num-traits`)
(done: `lazy_static``LazyLock` only; the rest stays pending)
- [~] decide on the under-consideration unused deps from `MODERNIZE.md`
(`seq-macro`, `leptos-use`, `http`, `console_log`, `js-sys`); the rest
(`itertools`, `thiserror`, `anyhow`, `pretty_env_logger`, `dotenvy`) stay
(decision: nothing removed; revisit later)
- [x] replace `end2end/tests/example.spec.ts` with a smoke test (app boots, nav renders, no console errors)
## Phase 1 — Persistence: localStorage + JSON export/import (L, ~3-5 days)
The foundation. Every later phase depends on it.
- [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)
- [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)
- [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 1.5 — Undo & redo (S, ~0.5-1 day)
Snapshot-based undo rides on the Phase 1 machinery: serialize → push →
`replace_state`. It is page-agnostic: the `store.track()` trigger already
fires on any mutation, so no component changes are needed and every later
phase's edits are undoable from day one.
- [x] `state::undo.rs` (hydrate-gated): undo/redo stacks of serialized
`Dashboard`, capped (e.g. 100); push coalesced on the store trigger
(one snapshot per ~1 s window → one undo step = one edit burst)
- [x] a new edit after undo clears the redo stack; Clear and import push a
snapshot first, so they are undoable too
- [x] `undo()` / `redo()` call `replace_state`; expose stack depths as
`RwSignal<usize>` for button disabled states
- [x] header buttons: ↩ Отменить / Вернуть (disabled when empty)
- [x] shortcuts: Ctrl+Z, Ctrl+Shift+Z (Ctrl+Y); ignore when focus is in an
input/textarea/select, so typing never triggers undo
- [x] unit tests for stack semantics; e2e: edit → undo → redo; reload keeps
the undone state (undo writes through autosave)
- **Acceptance:** every edit since the last window is reversible with a
keystroke; undo survives a reload.
**Done (2026-08-16):** implementation in `src/state/undo.rs`; e2e-verified
in chromium (buttons, shortcuts, reload, undoable Clear). Notes:
- Snapshots are `Dashboard` clones, not JSON: the dashboard is small.
- `HistoryHandle` is `Copy` (RwSignal fields) — context-friendly and
capture-safe under edition-2024 closure rules.
- No `std::time::Instant`: it panics on wasm32; the settle timer is the
only clock. The undo button enables with the pending burst, so a
fresh edit is undoable before the 1 s window settles.
## Phase 2 — Character page (M, ~1-2 days)
`/` currently renders the literal string "character".
- [x] player card: portrait, name, class, level, XP (data already in sidebar)
- [x] HP block: temp / current / max, `Numput`-style editing
- [x] prepared spells UI: render Vacant/Occupied slots; free-text spell name entry
(upgrade the `Spell` stub from `name`-only to name + optional level/description)
- [x] attunements UI: 3 slots, free text, clear button (model exists)
- **Acceptance:** player can set prepared spells and attunements; they persist.
**Done (2026-08-16):** page in `src/components/character/`; e2e-verified
in chromium (add/undo/redo a spell, remove, reload persistence, attunements
with clear). Notes:
- The HP block was already in the sidebar (`hit_points.rs`, editable
`type="number"` inputs via `utils::adjust_checked`) — nothing to add.
- `Spell` fields are optional, so name-only spells in old saved JSON
still parse.
- Rows are plain functions called from `For`'s `let(...)` children:
component tags are not allowed directly inside them (view macro).
**Second pass (2026-08-16):** the player card is gone (it duplicated the
sidebar); headings are plain RU (`Заготовленные заклинания`, `Атюнменты`);
attunements grew from a fixed 3-slot array to a `Vec` with a "+ слот"
button (custom campaigns); `PlayerData.ac` (manual armor class) lives in
the sidebar HP block, `#[serde(default)]` so old saves still load.
## Phase 3 — Inventory & item search end-to-end (L, ~3-5 days)
Rows are editable and move/remove works. Search exists but is orphaned; "+" and
"…" buttons are dead.
- [ ] host `ItemSearchField` on a page (Wiki page is the natural home — see Phase 7)
- [ ] "+" adds the `BookItem` to the personal inventory: merge into a row with the
same name, else new row, quantity 1
- [ ] "…" opens an item detail view: description, rarity, weight, tags, source data
(`BookItem.rest`)
- [ ] free-form add-entry UI: add a row (name/quantity/weight) without search
- [ ] edit a row: name, quantity, weight, description
- [ ] stretch: carrying capacity — sum row weights per inventory, show total vs limit
- **Acceptance:** search "potion" → click + → a row appears in the personal
inventory with the correct weight; state persists.
## Phase 4 — Quests page (M, ~1-2 days)
New route `/quests`. Model and mock data exist; zero UI.
- [ ] add a completion state to `Quest` (the model lacks one today)
- [ ] quest list: title, giver (Name), taken/task locations, dates, reward (Balance,
reuse `Numput`), description
- [ ] add / update / delete quest; toggle complete
- [ ] sort by deadline; highlight overdue
- **Acceptance:** create a quest, reload — it is still there; mark it complete.
## Phase 5 — Notes page (M, ~1-2 days)
New route `/notes`. Model and mock data exist; zero UI.
- [ ] note list: content, in-game date, session, tags; add / edit / delete
- [ ] tag chips; filter by tag or by session
- [ ] quick-add defaults to the header's current session and in-game date
- **Acceptance:** log a note mid-session, filter by tag, reload — still there.
## Phase 6 — Contacts page (M, ~1-2 days)
New route `/contacts`. Model and mock data exist; zero UI.
- [ ] contact cards: name, alias, status, location, notes, optional image
- [ ] status filter: Alive / Dead / DeadBygone / Unknown
- [ ] add / edit / delete contact
- **Acceptance:** find a contact by status filter; edit its notes; persists.
## Phase 7 — Wiki page: item reference (M, ~1-2 days)
`/wiki` currently renders "books". Search infrastructure already exists.
- [ ] host `ItemSearchField` here with results list
- [ ] item detail panel: rarity, weight, full entry text from the JSON `rest` field
- [ ] filters: rarity / category tags; lazy list or pagination — never render 5k rows
- [ ] "Add to inventory" shortcut (reuses the Phase 3 path)
- **Acceptance:** browse or search items, open a detail, add to inventory.
## Phase 8 — Polish, tests, release (L, ~2-4 days)
- [ ] responsive SCSS pass (sidebar and header on mobile widths)
- Playwright e2e: add item from search, quest CRUD, notes filter, undo
shortcuts (persistence round trip is covered by `persistence.spec.ts`)
- [ ] rewrite README: usage, dev workflow, deploy
- [ ] release build via cargo-leptos; verify SSR + hydration; pick a deploy target
- **Acceptance:** fresh clone → `cargo leptos build` → app runs; e2e green.
---
## Sequencing notes
- Phase 1 before Phases 2-7: every new page must persist. Building pages first
against mock data and persisting later costs a data-migration pass instead.
- Phases 2 and 3 can run in parallel after Phase 1.5 (different components).
- Phase 1.5 (undo) is page-agnostic: it must land before the edit-heavy
phases so their edits are undoable; nothing in Phases 2-7 depends on it.
- Phases 4-6 are independent of each other and share one CRUD-list pattern —
build the pattern once (e.g. a generic list editor) and reuse it.
- Phase 7 reuses the Phase 3 add-to-inventory path; keep that path shared.
## Known debt and risks
- **Cargo.lock is gitignored** — builds are not reproducible. Decide in Phase 0.
- **serde × Store derive** on the same structs — unverified combination;
prototype before committing to it (Phase 1).
- **~3.9 MB of item JSON** bundled via `include_str!` — parses on startup in the
browser. Fine for MVP; moving to a server function is a post-MVP option.
- **localStorage** has a ~5 MB budget — plenty for text JSON state.
- **Browser APIs on SSR** — localStorage access must be gated to `hydrate`/`csr`;
SSR renders from mock or empty state.