Skip to content

One JSON document for storage; read-only dashboard; import asks before wiping - #5

Open
Grantmiller99 wants to merge 3 commits into
999owen:mainfrom
Grantmiller99:feat/single-document-storage
Open

One JSON document for storage; read-only dashboard; import asks before wiping#5
Grantmiller99 wants to merge 3 commits into
999owen:mainfrom
Grantmiller99:feat/single-document-storage

Conversation

@Grantmiller99

@Grantmiller99 Grantmiller99 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #1, #2 and #4. Rebuilds the storage layer around a single JSON document, makes the dashboard a read-only view, and gives import a choice instead of always destroying what you have. The format is documented in DATA.md.

1. Import asks whether to wipe your entries

Import always replaced everything, so restoring a backup silently destroyed any entries added since it was taken. It now asks.

The file is parsed and validated before anything is stored, so the dialog shows real numbers — "This file holds 12 entries. You currently have 4 stored." — and an unusable file can never damage what is already there.

Answer Effect
Yes — wipe and replace Discard everything stored, entries and settings, keeping only the backup. The old behaviour, now chosen deliberately.
No — keep mine and add Append the file's entries to what is stored. Existing entries and existing settings are left alone.
Cancel Nothing is touched. Backdrop click and Escape do the same.

With nothing stored yet there is nothing to wipe, so the question is skipped and the file imports directly.

On add, an entry already present (same id — what happens when the same file is imported twice) is counted as a duplicate and skipped rather than added again.

Native confirm() only offers two answers, so this is a small in-page dialog styled to match the settings panels. Every outcome reports what actually happened: entries added or restored, duplicates skipped, entries dropped for a bad date or amount, and how many of your own entries were wiped.

2. Storage is one JSON document

All data lives in one document under one key — think of it as a single JSON file. Every operation reads or writes the whole thing, so nothing writes a partial record.

Call What it does
read() Parse the document. Corrupt data returns an empty one instead of breaking the page.
write(doc) Serialise the whole document back over the key.
clear() Delete the key outright. A later read does not recreate it, so cleared stays cleared.
parseBackup(text) Validate a backup without storing it.
replaceWith(doc) / mergeWith(doc) Wipe, or keep and add.
exportToFile() Write the document out verbatim.

Export re-imported with wipe returns identical data, ids included. One validateEntry gate covers both typed input and imported files, so a form and a backup cannot disagree about what a valid entry is.

3. Dashboard is a read-only view

"Incoming Paychecks" and "Weekly Spending" mirror the Entries page — same Date / Amount / Notes columns, same newest-first order — with no add forms and no delete buttons. Each panel links to Entries for editing, so entries are managed in exactly one place. Both listings now show every entry, scrolling inside the panel with a sticky header, rather than silently cutting off after five rows.

Totals, charts and listings all refresh from the stored document, so entries added on Entries or Groceries and data restored by an import both land on the dashboard correctly.

Verified

Driven through the real UI against a local server, no console errors:

Check Result
Dialog contents "This file holds 2 entries. You currently have 2 stored." + three buttons
Cancel / Escape / backdrop Dialog closed, entries untouched
No — keep and add "Added 2 entries to your existing ones — 4 total. Your settings were kept." Own settings (35%) survived the file's 40%
Same file added twice "Added 0 entries… 2 entries were already present and skipped." Total stayed 4
Yes — wipe and replace "1 entry restored… Your previous 2 entries were wiped." File's settings (40%) applied
Empty store No question asked; imported directly, no misleading "wiped" line
Invalid file Rejected before anything was stored
Clear Key is null; a later read does not recreate it
Add on Entries page Dashboard totals, listing and charts updated live in another tab
Export → clear → import Entries, settings and ids identical
Dashboard read-only 0 forms, 0 inputs, 0 delete buttons, 2 "Manage on Entries" links

Rebuilds the storage layer around a single JSON document rather than a loose
collection of helpers, and documents the whole format in DATA.md.

Storage model:
- All data is one document under one localStorage key. Every operation reads or
  writes the whole document, so what is stored is always complete and valid;
  nothing writes a partial record.
- read() parses it, write() serialises it back, clear() deletes the key
  outright, replaceFromJSON() overwrites it with an imported file, and
  exportToFile() writes it out verbatim.
- Clear now genuinely wipes the file. A read afterwards returns a fresh empty
  document without recreating the key, so cleared stays cleared.
- Import replaces rather than merges. Entries and settings are both taken from
  the file, and the confirmation says how many entries are about to be
  overwritten instead of a vague warning.
- Export re-imported returns identical data, ids included.
- One validateEntry gate now covers both typed input and imported files, so a
  form and a backup cannot disagree about what a valid entry is.

Dashboard:
- "Incoming Paychecks" and "Weekly Spending" are read-only listings mirroring
  the Entries page: same Date / Amount / Notes columns, same newest-first order,
  no add forms and no delete buttons. Each panel links to Entries for editing.
- Both listings show every entry, scrolling inside the panel with a sticky
  header, rather than silently cutting off after five rows.
- Totals, charts and listings all refresh from the stored document, so entries
  added on the Entries or Groceries page and data restored by an import both
  land on the dashboard correctly.
- Removed the form and delete-button CSS the dashboard no longer uses.

Also adds DATA.md describing the document, the entry rules, every operation,
how totals are derived, and why writes re-read before saving.
Import always replaced everything, so restoring a backup silently destroyed any
entries added since it was taken. It now asks.

The file is parsed and validated BEFORE anything is stored, so the dialog can
show real numbers ("This file holds 12 entries. You currently have 4 stored.")
and an unusable file can never damage what is already there.

Three answers:
- Yes, wipe and replace - discard everything stored, entries and settings, and
  keep only the backup. The previous behaviour, now chosen deliberately.
- No, keep mine and add - append the file's entries to what is stored. Existing
  entries and existing settings are left alone. An entry already present, same
  id, which is what happens when the same file is imported twice, is counted as
  a duplicate and skipped rather than added again.
- Cancel - nothing is touched. The backdrop and Escape do the same.

With nothing stored yet there is nothing to wipe, so the question is skipped and
the file imports directly.

Native confirm() only offers two answers, so this is a small in-page dialog
styled to match the settings panels. Every outcome reports what actually
happened: how many entries were added or restored, how many duplicates were
skipped, how many were dropped for a bad date or amount, and how many of your
own entries were wiped.

Splits the store's import into parseBackup (validate without storing),
replaceWith (wipe) and mergeWith (keep), and updates DATA.md to match.
@Grantmiller99 Grantmiller99 changed the title Store all data as one JSON document; make the dashboard read-only One JSON document for storage; read-only dashboard; import asks before wiping Aug 9, 2026
… data loss

Four defects found reviewing the data layer, each reproduced before fixing.

Duplicate ids deleted the wrong entry. validateEntry trusted any id string in a
backup and never checked uniqueness, so two entries could share one; delete
works by id and removes the first match. Importing a file with two spend entries
both carrying id "dup" and clicking the trash on the top row removed the other
one. Ids are now unique across the whole document, with a repeated one reissued
on the way in. This also stops "keep mine and add" silently discarding a
legitimate entry whose id happened to collide.

Clearing a settings field rewrote it to the minimum. Number('') is 0, which
clamped an emptied percentage box to 1% and an emptied grocery limit to $0.01,
then redisplayed the new value as though it had been typed. Blank input now
keeps the current setting: clampNumber treats empty as "no value", and
validateSettings takes the fallbacks to use, so editing falls back to the
current settings while importing falls back to the defaults.

Exporting an empty tracker downloaded a file and then said there was nothing to
export, because the anchor was clicked before the count was returned. It now
returns early with the counts and downloads nothing.

Loading silently destroyed invalid entries. read() rewrote storage whenever any
entry failed validation, so a hand-edited document lost those rows permanently
just by opening a page, with no message. Load now only rewrites for a genuine
upgrade (missing ids, repeated ids, older schema) and logs a warning about
anything it ignored, leaving the data on disk.

Also corrects the store's header comment, which still claimed import always
replaces the document and predated the wipe/keep choice, and removes
replaceFromJSON, unused since the import dialog landed.
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.

1 participant