Files
lunchtime-web/docs/statistics.md
T
simandClaude Sonnet 5.5 a2e4321ac7
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m44s
Rework My Orders stats and add responsive UI checks
My Orders: year calendar and lunch profile first, chips and "How you
compare" removed, open and paid payments merged into one table (open
first, then newest). Activity is now a line chart with a cumulative
switch and finer buckets (daily up to ~3 months, weekly up to ~13).
Cards get subtle elevation and tighter spacing.

Responsive: phone tab strip instead of the side menu, heatmap fits
narrow screens, grouped column-chart labels are truncated, the manager
weekday table scrolls instead of overflowing.

Tooling: viewports.mjs (presets, overflow detector), shoot.mjs --sizes
and --select, tests/responsive.ui.spec.ts, docs/ui-testing.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 23:15:25 +00:00

5.2 KiB

Statistics

Statistics pages live under /stats/* (src/frontend/src/views/StatisticsView.tsx). Status: ✅ done, 🔲 TODO (needs backend changes, listed under each item).

Architecture

  • Backend (services/stats_service.py) returns compact fact rows, not aggregates, for the per-user pages. Aggregation (months, weekdays, hours) happens in the frontend (lib/stats.ts) so buckets use the viewer's timezone.
  • The site overview is aggregated in the backend (GET /api/admin/stats) because it spans all users and needs files on disk.
  • Item names/prices come from the per-order config files (data/configs/<admin_token>.json), resolved in the backend.
  • Estimated totals only exist for orders whose menu items have prices.
Endpoint Auth Used by
GET /api/stats/me any user My Orders
GET /api/stats/managed any user (own orders only) Order Management
GET /api/admin/stats site admin Site Overview

Timeframe and insights

  • One page-wide timeframe filter (range.tsx: 1W, 1M, 3M, 1Y, YTD, All, custom) drives every number and chart, except the all-time blocks labelled as such (lunch profile, calendar, payments, funnel).
  • Stat tiles show the change vs. the previous period of equal length (Delta); "All" has no comparison.
  • The "Activity" panel (TimeSeriesPanel.tsx) draws line charts of per-period totals (days up to ~3 months, weeks up to ~13 months, months beyond; periodBuckets in lib/stats.ts) with a "Cumulative" switch (off by default) for running totals. Site-wide daily counts come from daily in /api/admin/stats.
  • Personal insights (lunch personality, records, adventurousness, calendar and weekday x hour heatmaps) are computed in lib/stats.ts from the fact rows. community in /api/stats/me (participant count, average, percentile) is the only extra aggregate, from user_order_tokens.
  • Site "active users" now means users who created an order or submission in the window (was: last profile update). The funnel and weekly retention use the same data.
  • Privacy model: My Orders only ever contains the viewer's own data plus anonymous aggregates (percentile, orders-per-participant histogram, organizer counts/shares). /api/stats/me returns no emails; organizers are numbered per response. Order Management is scoped to the viewer's own orders (their participants are visible, as in the order admin view). Only Site Overview (admin) sees site-wide data and names.
  • Not possible without new tracking: fill rate per order (no invitee list), display names (only emails exist; the local part is shown).

My Orders (orderees)

Layout: year calendar, lunch profile (all-time facts: orders, streak, average, busiest month, milestone, organizers, adventurousness, rank), activity, favourite items + payments, weekday x hour.

  • ✅ Participation over time (orders joined per month) and current streak (consecutive weeks)
  • ✅ Favourite items and categories, items repeated most (drill-down: all items, by category, or one category)
  • ✅ Total and average spend per order (priced menus only)
  • ✅ Usual order day / hour
  • ✅ Most frequent organizers
  • ✅ Payments: one table of open (accepted, unpaid) and paid orders, open first, then newest first; status filter
  • 🔲 How early you submit before the deadline — orders have no deadline. Backend: add group_orders.deadline (nullable ISO text) + create/update API + UI field; expose it in /api/stats/me.

Order Management

  • ✅ Orders created over time; open vs. closed (closed = ordering done)
  • ✅ Participants per order (average, trend); repeat vs. new participants
  • ✅ Submission timeline: time from order creation to submission
  • ✅ Popular items and categories across own orders (same category drill-down)
  • ✅ Order volume: items and value per order
  • ✅ Payment status: paid / accepted-unpaid / pending
  • 🔲 Average time to payment — only the current paid flag is stored. Backend: add submissions.paid_at (and accepted_at), set on status updates, backfill NULL.
  • 🔲 Participants who are invited but rarely submit — there is no invite/participant list. Backend: order_invites table (order, email) or a per-owner contact list.
  • 🔲 Submission timeline relative to the deadline — needs the deadline (see above).

Site Overview (admins)

  • ✅ Active contributors per week (users who created an order or submission), new registrations per week
  • ✅ Users with recent activity (7 / 30 days, from last known activity — approximation)
  • ✅ Unconfirmed accounts, pending email / user-id changes, open registration links
  • ✅ Orders per week and share of orders with ≥1 submission
  • ✅ Top order managers, most active participants
  • ✅ Most ordered items site-wide (same category drill-down; items in /api/admin/stats is capped at 300)
  • ✅ Storage usage per data folder and orphaned files
  • 🔲 Real daily/weekly/monthly active users — only creation timestamps are recorded. Backend: user_profiles.last_seen_at updated (throttled) in get_existing_user_id, or an activity_log table.
  • 🔲 Email volume and failures — email_service does not log sends. Backend: email_log table (kind, recipient hash, status, error, created_at) written by email_service.