You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/webui/docs/migration/stats-migration-plan.md

11 KiB

WebUI Stats Migration Plan

Snapshot date: 2026-05-14

Status

  • Completed on 2026-05-14.
  • stats is now React-owned in the shell route manifest.
  • The legacy stats HTML, JS, and CSS path has been removed.
  • The global Chart.js import was removed and replaced with route-local Recharts.
  • Legacy playback and artist-detail handoffs now go through the explicit shell bridge.
  • A local seed script exists for realistic UI testing without production listening history: tools/seed_stats_ui_scenarios.py.

Goal

  • Migrate stats from the legacy shell to the React route host.
  • Replace the global Chart.js CDN script with route-local React chart components.
  • Use the issues route slice as the structural reference, but add a few stronger conventions for data-heavy read-only pages.

Why stats Is The Right Next Route

  • The route is shell-local today.
  • The activation path is narrow.
  • The page has real async data loading and interaction.
  • The page is complex enough to validate query conventions, search-param state, and route-local chart components.
  • The page does not currently drive broad shell-global workflows.

This route has now validated those assumptions successfully.

Current Legacy Shape

Page surface in webui/index.html:

  • Header
    • time range buttons
    • last synced label
    • manual sync action
  • Overview cards
  • Left column
    • listening activity chart
    • genre breakdown chart
    • recently played list
  • Right column
    • top artists
    • top albums
    • top tracks
  • Full-width sections
    • library health
    • library disk usage
    • database storage
  • Empty state

Legacy JS responsibilities in webui/static/stats-automations.js:

  • page initialization
  • range switch handling
  • data fetch orchestration
  • formatting helpers
  • chart instantiation and teardown
  • ranked list rendering
  • cross-page deep links into library / artist detail
  • playback handoff for recent and top tracks

Backend endpoints already split cleanly:

  • GET /api/stats/cached
  • GET /api/stats/db-storage
  • GET /api/stats/library-disk-usage
  • POST /api/listening-stats/sync
  • GET /api/listening-stats/status

There are also narrower stats endpoints in the backend, but the current page already gets most of its main payload from the cached route.

Library Choice

Recommended charting library:

  • recharts

Reasoning:

  • React-native component model
  • good fit for bar + doughnut-style dashboards
  • easy to split into small route-local components
  • easier to theme from CSS variables than raw imperative chart setup
  • easier to test than a canvas-first imperative path

Not recommended for this migration:

  • react-chartjs-2
    • better for parity-only migration
    • still keeps the mental model close to Chart.js
  • visx
    • stronger for bespoke visualization systems
    • more work than this page needs

Proposed Route Slice

webui/src/routes/stats/
  route.tsx
  -stats.types.ts
  -stats.api.ts
  -stats.helpers.ts
  -stats.api.test.ts
  -stats.helpers.test.ts
  -ui/
    stats-page.tsx
    stats-page.module.css
    stats-header.tsx
    stats-overview-cards.tsx
    stats-empty-state.tsx
    stats-ranked-list.tsx
    stats-recent-plays.tsx
    stats-library-health.tsx
    stats-disk-usage.tsx
    stats-activity-chart.tsx
    stats-genre-chart.tsx
    stats-db-storage-chart.tsx

Proposed Route Responsibilities

route.tsx

  • declare /stats
  • validate search params
  • gate route through bridge.isPageAllowed('stats')
  • preload the shell context
  • load the main cached stats payload plus listening-status payload
  • optionally preload disk usage and db storage if we want zero-layout-shift first render

-stats.types.ts

  • search param schema
  • response payload types
  • normalized display shapes
  • chart row types

-stats.api.ts

  • query keys
  • fetchers for:
    • cached stats
    • listening status
    • db storage
    • library disk usage
  • mutation helper for manual sync
  • invalidation helpers

-stats.helpers.ts

  • range labels
  • numeric and duration formatters
  • disk size formatters
  • chart data shaping
  • legend shaping
  • safe fallbacks for empty server responses

-ui/stats-page.tsx

  • page composition
  • search-param driven range selection
  • section layout
  • empty-state branching

Search Params

Use search params for state that should survive reloads and linking:

  • range

Recommended values:

  • 7d
  • 30d
  • 12m
  • all

This is the one clear page-state value worth encoding in the URL. Everything else can remain derived from server data.

Query Model

Recommended split:

  • primary query:
    • statsCachedQueryOptions(profileId, range)
  • secondary queries:
    • statsListeningStatusQueryOptions(profileId)
    • statsDbStorageQueryOptions(profileId)
    • statsLibraryDiskUsageQueryOptions(profileId)

Why this split:

  • cached is the real page backbone
  • db-storage and library-disk-usage are already separate in the backend
  • they can render as progressively enhanced cards without blocking the whole route
  • listening-stats/status updates the sync label and complements the sync mutation

Recommended route-loader behavior:

  • always ensure:
    • cached stats
    • listening status
  • optional:
    • db storage
    • disk usage

If we want a snappier first migration, we should keep the last two as client-side useQuery calls rather than route-loader requirements.

Component Sketch

StatsPage

  • calls useReactPageShell('stats')
  • reads range from route search
  • renders:
    • StatsHeader
    • StatsOverviewCards
    • StatsEmptyState or main sections

StatsHeader

  • range segmented control
  • last synced text
  • sync button mutation

StatsOverviewCards

  • five summary cards

StatsActivityChart

  • Recharts BarChart
  • responsive container
  • route-local tooltip
  • accepts already-shaped rows

StatsGenreChart

  • Recharts PieChart
  • legend rendered in React markup beside the chart
  • top-10 clipping stays in helpers

StatsDbStorageChart

  • Recharts PieChart
  • custom center label rendered in React
  • legend list rendered beside chart

StatsRankedList

  • shared component for artists / albums / tracks
  • variant props for:
    • artwork
    • subtitle/meta
    • count label
    • optional play action
    • optional artist-detail deep link

StatsRecentPlays

  • simple list component
  • play action

StatsLibraryHealth

  • overview metrics
  • format breakdown bar
  • enrichment coverage rows

StatsDiskUsage

  • total bytes row
  • pending/deep-scan message
  • per-format horizontal bars

Recharts Mapping

Legacy Chart.js to React mapping:

  • listening activity
    • from imperative new Chart(... type: 'bar')
    • to ResponsiveContainer + BarChart + Bar + XAxis + YAxis + Tooltip
  • genre breakdown
    • from doughnut chart
    • to PieChart + Pie + custom legend
  • database storage
    • from doughnut chart with center total overlay
    • to PieChart + Pie + React-rendered center label

Suggested chart convention:

  • keep all chart data shaping outside the chart components
  • chart components should receive already-normalized rows and colors
  • never read directly from raw server payloads inside Recharts markup

CSS Strategy

Recommended first pass:

  • create stats-page.module.css
  • port stats-specific selectors from webui/static/style.css
  • keep class names semantically similar to reduce migration risk

Suggested approach:

  • move only the selectors needed by the React route
  • leave legacy stats selectors in place until the route flip is complete
  • after the React route owns stats, remove unused legacy selectors in a cleanup pass

Do not try to redesign the page during the migration.

Shell And Routing Changes

When the route is ready:

  1. Add webui/src/routes/stats/route.tsx
  2. Regenerate the TanStack route tree if needed
  3. Change stats from legacy to react in webui/src/platform/shell/route-manifest.ts
  4. Keep the legacy stats-page DOM in webui/index.html during the initial cutover if that reduces risk
  5. Remove legacy activation from webui/static/init.js once React ownership is confirmed
  6. Remove the global Chart.js script from webui/index.html

Incremental Migration Order

Recommended order:

  1. Add types, API layer, and helpers
  2. Build the React route with plain markup and no charts yet
  3. Port overview, ranked lists, recent plays, and empty state
  4. Port library health and disk usage
  5. Port Recharts activity, genre, and db storage charts
  6. Flip route ownership from legacy to React
  7. Remove global Chart.js import
  8. Delete or shrink legacy stats logic from stats-automations.js

This order gives us a working React page before charting becomes the critical path.

Testing Sketch

Unit tests:

  • -stats.helpers.test.ts
    • range formatting
    • duration formatting
    • db storage grouping into Other
    • genre top-10 shaping
    • disk usage empty-state shaping

API tests:

  • -stats.api.test.ts
    • cached stats success / error
    • listening status success / error
    • db storage success / error
    • disk usage success / error
    • sync mutation success / error

Route / component tests:

  • initial render for default range=7d
  • changing range updates the URL and query key
  • empty state renders when overview.total_plays === 0
  • ranked artist click deep-links to library / artist detail
  • track play action triggers the expected handoff
  • sync action shows pending state and invalidates relevant queries

Playwright is optional for the first pass.

Decisions To Keep Simple

  • Keep the existing page structure.
  • Keep the current backend endpoint split.
  • Keep the current time-range set.
  • Reuse the existing shell deep-link behavior for library and playback.
  • Use Recharts only inside stats first.

Follow-Up Opportunities

  • Extract shared chart colors into route-local constants or a small shared viz helper.
  • Consider a tiny components/charts/ layer only after a second React page needs charts.
  • Revisit whether stats/cached should remain the primary page payload or whether the route should fan out to narrower endpoints later.
  • Keep watching for overlap between route-local controls and shared UI primitives. The stats range selector is a good example of a pattern that should stay local for now, but should be reconsidered if another migrated route needs the same segmented-control behavior.

Recommendation

The first implementation should optimize for:

  • parity
  • clear route-local boundaries
  • removal of global Chart.js
  • reusable data/query conventions

It should not optimize for:

  • visual redesign
  • a cross-app chart abstraction
  • backend reshaping

Outcome

  • The route now serves as the reference for data-heavy read-only React pages.
  • The migration proved out route-local charts, route-search state, explicit shell-bridge interop, and post-cutover legacy cleanup.
  • The work also reinforced a migration guideline for future routes:
    • prefer local implementation on first use
    • actively note overlap with shared primitives
    • extract only once the second clear consumer appears