Skip to content

feat: add mod preset persistence (ADR-008)#154

Merged
ushkinaz merged 8 commits intonextfrom
feat/109-mods-preset
Apr 7, 2026
Merged

feat: add mod preset persistence (ADR-008)#154
ushkinaz merged 8 commits intonextfrom
feat/109-mods-preset

Conversation

@ushkinaz
Copy link
Copy Markdown
Owner

@ushkinaz ushkinaz commented Apr 7, 2026

Summary

Implements mod preset persistence via localStorage. When users select mods in the ModSelector, they are saved as a default. Visiting the guide without a mods query parameter automatically loads the saved preset via replaceState, maintaining the URL as the source of truth.

Why

Users often play with a consistent set of mods (e.g. aftershock, magiclysm). Manually re-selecting them on every visit or bookmarking complex URLs is tiresome. This feature provides a "set once, forget" experience for the default view.

What Changed

  • preferences.svelte.ts: Added defaultMods to UserPreferences with localStorage sync.
  • navigation.svelte.ts: Injected saved mods during bootstrapApplication when no mods are present in the URL. Added persistence trigger to changeMods.
  • docs/routing.md: Updated documentation to reflect mod persistence.
  • docs/adr/ADR-008_mod_preset_persistence.md: New ADR documenting the architectural decision.
  • Unit tests: Added coverage for preferences.svelte.ts and bootstrap injection in navigation.test.ts.

What's Improved

  • Better UX for returning users with complex mod setups.

Compromises / Trade-offs

  • Adds a new dependency on localStorage.
  • Clearing browser data or using private mode will bypass the preset.

Behavior Changes

  • Bare URLs (e.g. /stable/) now redirect to /stable/?mods=... if a preset is saved.
  • Shared URLs with explicit mods are NOT affected.

Reviewer Notes / Potential Triggers

  • Check the redirect logic in bootstrapApplication. It ensures that URL parameters always take precedence over the local preset.

Critical Path For Manual Testing

  1. Go to /nightly/
  2. Select some mods (e.g. aftershock) and click Apply.
  3. Reload the page at /nightly/. It should automatically append ?mods=aftershock.
  4. Go to /nightly/ directly. It should redirect to ?mods=aftershock.
  5. Go to /nightly/?mods= (empty). It should STAY empty and NOT load the preset.
  6. Click "Apply" with NO mods selected. The preset should be cleared.

Verification

  • Ran unit tests: src/preferences.svelte.test.ts and src/navigation.test.ts.
  • Note: pnpm check and pnpm lint were recommended by project rules but not verified in this PR session due to local tool issues.

ushkinaz added 3 commits April 7, 2026 02:04
closes #109

- Add support for saving and clearing default mod presets.
- Persist default mods via `preferences.svelte.ts` and inject into bare URLs during bootstrap.
- Update tests to cover mod persistence behavior.
- Document the feature with ADR-008: Mod Preset Persistence.
…resolution

- Introduce `resolveVersionedPath` for handling versioned routes.
- Simplify bootstrap navigation logic to include preferences.
- Remove outdated `canonicalizeMalformedVersionURL` method.
- Update tests to reflect the new routing approach.
…ith `mods` and `tileset`

- Rename APIs and properties for clarity: `defaultMods` -> `mods`, `preferredTileset` -> `tileset`.
- Update localStorage keys to `cbn-guide:mods` and `cbn-guide:tileset`.
- Refactor persistence methods and related tests.
- Update ADR-008 to reflect key renaming.
@ushkinaz ushkinaz changed the base branch from main to next April 7, 2026 00:13
@ushkinaz ushkinaz marked this pull request as ready for review April 7, 2026 00:15
@ushkinaz ushkinaz marked this pull request as draft April 7, 2026 11:53
@ushkinaz ushkinaz marked this pull request as ready for review April 7, 2026 11:54
@ushkinaz
Copy link
Copy Markdown
Owner Author

ushkinaz commented Apr 7, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

Caution

Review failed

Pull request was closed or merged during review

Summary by CodeRabbit

  • New Features

    • Mod presets are now persisted in the browser and auto-applied on load when no explicit mods query is present.
    • Saving an empty modset clears the stored preset.
  • Behavior

    • URL bootstrapping now injects saved mods only when appropriate and avoids unnecessary URL replacements.
  • Documentation

    • Added an architecture decision record and updated routing/preferences docs describing mod-preset persistence.

Walkthrough

Adds localStorage-backed mod-preset persistence, injects saved mods into bare URLs during bootstrap via replaceState, renames tileset APIs, replaces pathname-based version resolver, and updates tests/docs to match new preference and routing behavior.

Changes

Cohort / File(s) Summary
Architecture Records
docs/adr/ADR-008_mod_preset_persistence.md, docs/adr/README.md
New ADR describing cbn-guide:mods localStorage key, bootstrap injection semantics, and ADR index updated.
Routing Docs
docs/routing.md
Docs updated to include persisted default mod preset alongside tileset; clarified locale non-persistence.
Preferences Module
src/preferences.svelte.ts, src/preferences.svelte.test.ts
Public API and state shape changed: preferredTilesettileset, added mods: string[], exported TILESET_STORAGE_KEY and new MODS_STORAGE_KEY, added setTileset, setMods, clearSavedMods, robust init/persistence with malformed-storage and storage-failure tolerance; tests updated/expanded.
Navigation Bootstrap
src/navigation.svelte.ts, src/navigation.test.ts
Bootstrap now uses resolveVersionedPath and composes canonical URL including saved mods; only calls replaceState when URL differs; changeMods calls setMods before navigation; tests cover injection/avoidance scenarios.
Routing Utilities & Tests
src/routing.svelte.ts, src/routing.url.test.ts, src/routing.test.ts
Removed canonicalizeMalformedVersionURL; added resolveVersionedPath(pathname, builds) and hasVersionlessHomePath; popstate no longer performs automatic rewrite/navigation; tests updated to assert resolver semantics.
Misc / Cleanup
src/schema.test.ts
Removed unused import.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Prefs as Preferences
    participant Storage as localStorage
    participant Nav as Navigation
    participant History as HistoryAPI

    App->>Prefs: initializePreferences()
    Prefs->>Storage: getItem('cbn-guide:mods')
    Storage-->>Prefs: JSON or null
    Prefs->>Prefs: parse & validate → mods[]
    Prefs-->>App: preferences{tileset, mods}

    App->>Nav: bootstrapApplication()
    Nav->>Nav: resolveVersionedPath(location.pathname, builds)
    Nav->>Nav: compute canonical URL (include prefs.mods if ?mods absent)

    alt bare URL and prefs.mods != []
        Nav->>History: replaceState(canonical URL)
        History-->>Nav: URL replaced
    end

    Nav->>Nav: updatePageState()
    Nav-->>App: route resolved

    App->>Nav: changeMods(mods)
    Nav->>Prefs: setMods(mods)
    Prefs->>Storage: setItem('cbn-guide:mods', JSON)
    Prefs->>Prefs: update preferences.mods
    Nav->>History: navigate(new URL)
Loading

Possibly related issues

  • #109 — Implements "Save Mod Preset": adds MODS_STORAGE_KEY, setMods/clearSavedMods, and bootstrap injection behavior matching the issue objective.

Possibly related PRs

  • #149 — Overlaps prefs/navigation changes (setMods, MODS_STORAGE_KEY, bootstrap integration).
  • #148 — Related routing refactor touching canonicalization/resolver used by navigation changes.
  • #153 — Overlaps routing/navigation updates and mod-preset bootstrap behavior.

Suggested labels

documentation

Poem

🔒 Presets stashed in browser tide,
Bootstrap scans, injects what’s inside.
Routes reconciled, preferences bound,
State written, empty clears the ground.
Mission: persistent, deterministic, deployed.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title directly correlates to the primary changeset objective—mod preset persistence with ADR documentation—and concisely encapsulates the feature addition.
Description check ✅ Passed Description is comprehensive and substantively aligned with changeset modifications, providing rationale, scope, behavioral specifications, and manual verification protocols.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/109-mods-preset

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/preferences.svelte.test.ts`:
- Around line 80-81: Test titles refer to removed/renamed API symbols and
incorrect expected values, causing misleading CI output; update the test
descriptions to match the current API and assertions. Rename titles that mention
setPreferredTileset to reference setTileset (e.g., "setTileset rejects invalid
values without mutating the preference"), change any titles mentioning
setDefaultMods or clearSavedDefaultMods to the current functions (e.g.,
setDefaultMods, clearDefaultMods or whatever the code now uses), and fix titles
that say "returns null" to state "returns []" where the assertion expects an
empty array; ensure each test title matches the assertion and the actual
function name (referencing setTileset, setDefaultMods,
clearSavedDefaultMods/getSavedDefaultMods as applicable).

In `@src/preferences.svelte.ts`:
- Around line 130-134: Update the stale docstring for the _resetPreferences test
helper to reference the renamed fields `tileset` and `mods` instead of
`preferredTileset` and `defaultPreferences.preferredTileset`; the comment should
state that the function resets `tileset` and `mods` to the defaults (e.g.,
`defaultPreferences.tileset`/`defaultPreferences.mods`) and still does not touch
localStorage so the docstring accurately matches the _resetPreferences
implementation.
- Around line 115-128: Add JSDoc comments above the exported functions setMods
and clearSavedMods describing their public API, parameters, return type, side
effects (that they persist to storage via persistMods/clearStoredDefaultMods and
update the preferences.mods state), behavior when mods is empty, and how
failures are handled or surfaced (e.g., whether persistence errors are thrown or
swallowed). Ensure each JSDoc includes a short summary, `@param` for the mods
array in setMods, `@returns` void, and an `@remarks` or `@throws` clause clarifying
persistence failure semantics so callers know expected behavior.
- Line 24: preferences.mods is being assigned by reference from
defaultPreferences.mods and from caller input (e.g., in setMods), which allows
downstream in-place mutations to mutate shared state; fix by always copying the
mods array/object on assignment and when returning from setters—replace direct
assignments of defaultPreferences.mods or incoming mods with a shallow clone
(e.g., spread or Array.from) or a deep clone if nested, and ensure any functions
that set or reset mods (such as the initializer that sets mods, the setMods
implementation, and any reset-to-default path) create a new array/object instead
of reusing the original reference so defaultPreferences cannot be mutated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 07a362cd-6789-468f-9ba0-9030e404f8a9

📥 Commits

Reviewing files that changed from the base of the PR and between f9ff880 and d9206ea.

📒 Files selected for processing (11)
  • docs/adr/ADR-008_mod_preset_persistence.md
  • docs/adr/README.md
  • docs/routing.md
  • src/navigation.svelte.ts
  • src/navigation.test.ts
  • src/preferences.svelte.test.ts
  • src/preferences.svelte.ts
  • src/routing.svelte.ts
  • src/routing.test.ts
  • src/routing.url.test.ts
  • src/schema.test.ts
💤 Files with no reviewable changes (1)
  • src/schema.test.ts

Comment thread src/preferences.svelte.test.ts Outdated
Comment thread src/preferences.svelte.ts Outdated
Comment thread src/preferences.svelte.ts
Comment thread src/preferences.svelte.ts
ushkinaz added 4 commits April 7, 2026 17:36
- Remove duplicate `hasVersionlessHomePath` implementation from `navigation.svelte.ts`.
- Centralize logic in `routing.svelte.ts` for improved maintainability.
… API naming

- Update test descriptions from `defaultMods` and `preferredTileset` to `mods` and `tileset`.
- Ensure test cases align with recent property and method renaming for clarity.
…rator usage

- Update assignments of `mods` to use spread operator for immutability.
- Refactor functions to consistently apply array cloning for preferences.
@ushkinaz ushkinaz linked an issue Apr 7, 2026 that may be closed by this pull request
5 tasks
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routing.svelte.ts (1)

228-234: ⚠️ Potential issue | 🔴 Critical

Behavioral delta confirmed: popstate handler bypasses URL canonicalization.

Current implementation routes popstate events through updatePageState()parseRoute(), which directly parses the URL state without invoking resolveVersionedPath(). Canonicalization occurs only at bootstrap via buildCanonicalBootstrapURL(). Malformed URLs in browser history will surface unchanged after back/forward navigation.

Test coverage gap: Existing popstate tests validate state synchronization only. No test scenarios cover navigation through browser history containing malformed URLs—recommend adding coverage for this edge case before production deployment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routing.svelte.ts` around lines 228 - 234, The popstate handler currently
calls updatePageState() which leads to parseRoute() without applying URL
canonicalization, so modify the flow so popstateHandler (or updatePageState)
first passes the current location through resolveVersionedPath() (or ensure
parseRoute calls resolveVersionedPath at its entry) before parsing; update tests
to include a popstate navigation scenario with a malformed/historic URL and
assert that the resolved/canonical URL (as produced by
buildCanonicalBootstrapURL()/resolveVersionedPath) is used for routing and state
sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/preferences.svelte.test.ts`:
- Around line 110-121: The "mods" test block only clears MODS_STORAGE_KEY but
initializePreferences() uses both keys, so update the beforeEach and afterEach
in the describe("mods", ...) block to also remove TILESET_STORAGE_KEY (alongside
MODS_STORAGE_KEY); keep calls to installMockStorage() and _resetPreferences()
as-is and ensure both localStorage.removeItem?.(MODS_STORAGE_KEY) and
localStorage.removeItem?.(TILESET_STORAGE_KEY) are invoked to prevent tileset
state leaking between tests.
- Around line 44-55: The tests only clear TILESET_STORAGE_KEY but
initializePreferences() also reads MODS_STORAGE_KEY, risking cross-test
contamination; update the beforeEach and afterEach in the "preferences" describe
block (where installMockStorage() and _resetPreferences() are used) to also
remove MODS_STORAGE_KEY from localStorage (i.e., call
localStorage.removeItem?.(MODS_STORAGE_KEY)) so both TILESET_STORAGE_KEY and
MODS_STORAGE_KEY are purged before and after each test to ensure full isolation.

In `@src/preferences.svelte.ts`:
- Around line 131-139: Update the docstring for _resetPreferences to accurately
describe its behavior: state that it resets both preferences.tileset and
preferences.mods to the corresponding values from defaultPreferences (and that
it does not touch localStorage), or alternatively change the implementation to
only reset tileset if that was intended; locate the function _resetPreferences
and adjust either the comment text or the assignment statements
(preferences.tileset and preferences.mods) so docstring and implementation
match.

---

Outside diff comments:
In `@src/routing.svelte.ts`:
- Around line 228-234: The popstate handler currently calls updatePageState()
which leads to parseRoute() without applying URL canonicalization, so modify the
flow so popstateHandler (or updatePageState) first passes the current location
through resolveVersionedPath() (or ensure parseRoute calls resolveVersionedPath
at its entry) before parsing; update tests to include a popstate navigation
scenario with a malformed/historic URL and assert that the resolved/canonical
URL (as produced by buildCanonicalBootstrapURL()/resolveVersionedPath) is used
for routing and state sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 102046aa-0241-4e0a-b033-b9ad88a9c711

📥 Commits

Reviewing files that changed from the base of the PR and between d9206ea and 708ca27.

📒 Files selected for processing (4)
  • src/navigation.svelte.ts
  • src/preferences.svelte.test.ts
  • src/preferences.svelte.ts
  • src/routing.svelte.ts

Comment thread src/preferences.svelte.test.ts Outdated
Comment thread src/preferences.svelte.test.ts
Comment thread src/preferences.svelte.ts
…nsistency

- Rename test suite from "preferences" to "tileset" for alignment with updated terminology.
- Revise helper documentation to generalize preference reset behavior.
@ushkinaz ushkinaz merged commit f2549a5 into next Apr 7, 2026
1 of 2 checks passed
@ushkinaz ushkinaz deleted the feat/109-mods-preset branch April 7, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Save Mod Preset

1 participant