FS Garage Score Card (FE) Architecture
System Overview
The Score Card is a per-project module: every project in FS Garage gets its own scorecard at /project/[id]/score, backed by three PocketBase collections (categories, metrics, entries) in fs-garage-base. Every roll-up, delta, target, and export calculation happens in a shared pure-function library on the frontend - see Metrics and Calculations for the exact formulas.
Architecture Diagram
graph TB
subgraph FE["Frontend: base-fs-garage-fe"]
ScoreRoute["/project/[id]/score"]
ManageData["/project/[id]/score/manage-data"]
ScorecardStore["store/index.js<br/>fetchProjectScorecard, fetchHeadlineMetrics"]
CalcLib["lib/scorecard/*.js<br/>rollup, delta, target, snapshot"]
end
subgraph BE["Backend: fs-garage-base"]
PocketBaseServer["PocketBase :8091"]
ScorecardCollections["metric_categories<br/>metrics<br/>metric_entries"]
end
ScoreRoute -->|"reads/writes"| ScorecardStore
ManageData -->|"CRUD categories/metrics/entries"| ScorecardStore
ScorecardStore -->|"REST"| PocketBaseServer
PocketBaseServer --> ScorecardCollections
ScoreRoute -->|"derives displayed values via"| CalcLib
ManageData -->|"derives displayed values via"| CalcLib
How to read this diagram
Arrows mean flow / dependency / "talks to", not button clicks.
Core Components
Score Card (components/scorecard/)
CategorySections.jsx/CategoryForm.jsx- category CRUD and grouped displayMetricForm.jsx,EntriesModal.jsx- metric definition and time-series data entryHealthStatusBadge.jsx/HealthStatusPill.jsx/HealthStatusSelector.jsx/HealthStatusIcon.jsx- health status shown at multiple sizes/contextsHealthQuickEntryBar.jsx- combined health + quick-entry control bar (merged from two separate controls)DeltaChip.jsx- period-over-period change indicatorAggregationInfoDrawer.jsx- explains how rolled-up numbers are computedEmptyScorecardPanel.jsx/ManageDataEmptyState.jsx- empty statesArchivedSection.jsx- archived categories/metrics
Calculation library (lib/scorecard/)
Every number the Score Card shows comes from a small set of pure, backend-agnostic functions (no I/O, fully unit-tested) rather than being computed inline in components. This is deliberate: the same functions run whether the caller is a live scorecard view or the snapshot exporter, so the two can never disagree. Full formulas: Metrics and Calculations.
| Module | Responsibility |
|---|---|
rollup.js | Turns raw entries into one value for a window or all-time (sum / average / latest) |
delta.js | Period-over-period change and momentum direction |
healthStatus.js | Health status enum, severity ranking, legacy status mapping |
targetPolicy.js | Target-met detection and cross-cadence target auto-suggestion |
metricPresentation.js | Combines rollup + delta + target + sparkline into what a metric card renders |
momentum.js | Per-project headline-KPI momentum used by the project listing's sparkline |
aggregationExplain.js | Static formula text shown in the Manage Data info drawer - kept in sync with rollup.js by convention |
cadence.js | Validates a quick-entry range against a metric's target cadence |
dateUtils.js | Window/period math: containment, intersection, day-weighted spans, Sun-Sat weeks |
overlap.js | Overlap guard - no two entries of a metric may cover the same date |
guards.js | Category-delete and metric-delete/archive rules |
entitlements.js | Role derivation and the restricted-category/metric visibility cascade |
snapshot.js / snapshotPaginate.js | Snapshot export row-building and page-break-safe pagination |
useQuickEntry.js / useProjectMomentum.js | React hooks wiring the above into Manage Data and the project listing |
Data Flow
sequenceDiagram
participant User
participant Frontend as Score Card UI
participant Store as store/index.js
participant PocketBase
User->>Frontend: Open /project/:id/score
Frontend->>Store: fetchProjectScorecard(projectId)
Store->>PocketBase: list metric_categories, metrics, metric_entries
PocketBase-->>Store: rows
Store-->>Frontend: raw rows
Frontend-->>Frontend: rollupWindow / computeDelta / presentMetric (lib/scorecard/*.js)
Frontend-->>User: Render categories, metrics, health, deltas
User->>Frontend: Add entry (Manage Data)
Frontend->>Store: create metric_entries row
Store->>PocketBase: POST metric_entries
PocketBase-->>Store: created row
Store-->>Frontend: Refresh scorecard
Design Decisions
Project-scoped routes, not a top-level app
Context: Score Card is a view of a single project, not a standalone app.
Decision: It lives under /project/[id]/... so it shares the project header/context, matching the refactor that moved Score Card from a flatter path into project/[id]/score.
Merge health + quick-entry into one bar
Context: Health status and quick metric entry were originally separate controls, adding UI clutter.
Decision: Combined into HealthQuickEntryBar in the manage-data header, reducing the number of distinct controls a user scans.
One shared formula library for live view and export
Context: A snapshot export that recomputed values with its own logic could silently drift from what the live page shows.
Decision: Both call the same lib/scorecard/*.js functions (rollupWindow, targetForRangeType, recentPeriodWindows), so the two can never disagree - see Metrics and Calculations.
Technology Stack
| Layer | Technology |
|---|---|
| Framework | Next.js App Router |
| Language | JavaScript |
| Data | PocketBase JS SDK via store/index.js |
| Tests | Vitest + React Testing Library |
| Default branch | dev |