Skip to main content

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.

Scope of this page

This page covers Score Card only. Kanban and Gantt, also built by Thanush in this same repo, have their own architecture pages.

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 display
  • MetricForm.jsx, EntriesModal.jsx - metric definition and time-series data entry
  • HealthStatusBadge.jsx / HealthStatusPill.jsx / HealthStatusSelector.jsx / HealthStatusIcon.jsx - health status shown at multiple sizes/contexts
  • HealthQuickEntryBar.jsx - combined health + quick-entry control bar (merged from two separate controls)
  • DeltaChip.jsx - period-over-period change indicator
  • AggregationInfoDrawer.jsx - explains how rolled-up numbers are computed
  • EmptyScorecardPanel.jsx / ManageDataEmptyState.jsx - empty states
  • ArchivedSection.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.

ModuleResponsibility
rollup.jsTurns raw entries into one value for a window or all-time (sum / average / latest)
delta.jsPeriod-over-period change and momentum direction
healthStatus.jsHealth status enum, severity ranking, legacy status mapping
targetPolicy.jsTarget-met detection and cross-cadence target auto-suggestion
metricPresentation.jsCombines rollup + delta + target + sparkline into what a metric card renders
momentum.jsPer-project headline-KPI momentum used by the project listing's sparkline
aggregationExplain.jsStatic formula text shown in the Manage Data info drawer - kept in sync with rollup.js by convention
cadence.jsValidates a quick-entry range against a metric's target cadence
dateUtils.jsWindow/period math: containment, intersection, day-weighted spans, Sun-Sat weeks
overlap.jsOverlap guard - no two entries of a metric may cover the same date
guards.jsCategory-delete and metric-delete/archive rules
entitlements.jsRole derivation and the restricted-category/metric visibility cascade
snapshot.js / snapshotPaginate.jsSnapshot export row-building and page-break-safe pagination
useQuickEntry.js / useProjectMomentum.jsReact 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​

LayerTechnology
FrameworkNext.js App Router
LanguageJavaScript
DataPocketBase JS SDK via store/index.js
TestsVitest + React Testing Library
Default branchdev

Next Steps​