BERTH 06 — DATA & ANALYTICS

Life System V2

React 18 · Vite · Zustand · Local-first · No backend

A personal operating console for time. Renders a human lifespan as navigable grids of weeks, months, years, and decades. Weekly logging follows a fixed flow — mood, mission, note — and everything persists locally. No account, no backend, no sync.

Prototype React 18 Zustand Framer Motion Cloudflare Pages
ARCHITECTURE

The app is a client-only React SPA with no backend, no database, and no account system. A single persisted Zustand store is the center of the architecture — configuration, notes, moods, missions, sealed archives, and UI flow state all live there. Derived life metrics are computed from three durable inputs: birthDate, lifespan, and a set of keyed records.

Record keys are string-encoded by time unit: w123 for week 123, m42 for month 42, similar patterns for years. The lifecycle grid, sheet navigation, counters, and derived metrics all build on these keys. The encoding made multi-scale navigation straightforward early in the project; it also made key parsing load-bearing across the app, which created friction as later features needed to reason about key shape.

COMPONENTS / HOOKS LifecycleGrid · WeekSheet InstrumentCluster · LogFlow SealedArchive · SearchPanel Framer Motion sheets SVG instruments (CSS Modules) DOM ticker (direct update) ZUSTAND STORE birthDate · lifespan · config notes · moods · missions sealed archives · UI flow keys: w123 · m42 · y8 · d2 persist middleware → localStorage versioned JSON + CSV export LIFECYCLE HELPERS weekNumber · lifePercent · grids pure TypeScript · no store reads SIGNAL HELPERS moon phase · illumination zodiac · season · local only
ARCHITECTURE PIVOT — EXTERNAL FEEDS → LOCAL SIGNALS
REMOVED

An earlier version of the app used the Guardian API for modern historical context and the Wikipedia REST API for older eras. The idea was that a logged week becomes richer when you can see what was happening in the world at the same time. That version worked technically. The product question it exposed: was the app about world context, or about building a coherent internal instrument?

External feeds were replaced with locally computed celestial signals — moon phase, illumination, zodiac, season. The computation is deterministic, requires no network call, has no API dependency, and works indefinitely. The architectural side effects: simpler codebase, no API keys, no rate limits, no breaking changes from upstream providers. The product side effect: the app became quieter and more self-contained.

STACK
CORE
React 18 · Vite · TypeScript strict
Zustand (persist middleware)
Framer Motion (sheets + animation)
CSS Modules · CSS custom properties
SVG (instruments, grid, arc dials)
DOMPurify (sanitization)
TESTING & SECURITY
Vitest · Testing Library · JSDOM
MSW (mock service worker)
Cloudflare _headers CSP policy
Live header validation script
Playwright-oriented pre-release audit
tsc --noEmit as type-check gate
DATA MODEL
String-keyed records: w123 m42 y8
All state derives from birthDate + lifespan
Versioned JSON backup
CSV export
Schema validation on import
FileReader / Blob / object URL
INFRASTRUCTURE
Cloudflare Pages (static deploy)
No backend · No database · No auth
localStorage persistence via Zustand
Local moon/zodiac/season computation
No external API calls at runtime
ENGINEERING DECISIONS
NOTABLE PROBLEMS
Key parsing became load-bearing

String-encoded record keys (w123, m42) seemed like a lightweight choice early. As the app grew, key parsing appeared in more and more places — the grid, sheets, counters, sealed archive logic, import validation, migration paths. A future record that needed different semantics from the key string created backward-compatibility concerns immediately. The encoding that was convenient in week one became a constraint that later features had to design around.

Validating import data for a local-first app

An app with no backend that stores personal history has one serious failure mode: a bad import overwrites data the user can't recover. Import validation checks schema version, key shape, entry count, file type, and file size before touching the store. An import that passes all checks but references an incompatible schema version is rejected and surfaced with a migration prompt. Getting this right took several iterations because the edge cases are not obvious until you test them with real backup files from earlier versions of the app.

Grid scale without virtualization

A full lifespan at week resolution can involve thousands of cells. React re-renders on hover or search state would produce visible lag at that count without careful memoization. The solution was to remove those behaviors from React state entirely — CSS handles hover via sibling selector, search results are toggled as classes. The tradeoff is less React-idiomatic code in those paths, but the interaction is fast and the rendering model is simpler than the memoization alternative would have been.

Documentation drifting from implementation

Planning documents were written early and were accurate at the time. As the codebase moved — the external feed pivot, the sealed archive addition, UI refactors — the documents didn't always follow. In a solo project this is survivable but creates friction when you want to explain, extend, or hand off the work. The cost of documentation drift compounds when the project becomes something you want to reason about over a longer time span.

CURRENT STATUS
WORKING Setup flow, weekly logging (mood → mission → note), mission tracking, instrument cluster (ArcDial, YearRing, split-flap counters, LoggingTracker), lifecycle grids (week/month/year/decade), sheet navigation, sealed future archives, local system signals (moon, zodiac, season), search, versioned backup and import/export. Deployed on Cloudflare Pages with CSP headers.
ROUGH EDGES Monolithic Zustand store mixes domain and UI state. Key parsing is fragile under schema changes. Lifecycle math is spread across multiple locations. Some planning documents describe an older version of the app.
NEXT Formalize the domain model. Separate persisted domain state from transient UI flow state. Strengthen schema versioning and migration guardrails. Finish design token governance. The roadmap isn't about adding features — it's about hardening the foundation under what already works.