Personal recurring task manager — dashboard, calendar, recurring tasks, PWA. Frontend on Vite + React + TS, backend on Supabase (Postgres + Auth + Realtime). This phase is the web app only; thermal printer + Raspberry Pi kiosk come later.
npm install
cp .env.example .env # fill in NEXT_PUBLIC_SUPABASE_URL + NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
npm run devApply supabase/migrations/*.sql to your Supabase project (Studio SQL editor or supabase db push). RLS is enabled on every table — queries are scoped to auth.uid().
src/lib/recurrence.ts— RRULE building/parsing and occurrence materializationsrc/lib/dates.ts— Luxon + timezone helperssrc/lib/supabase.ts— typed clientsrc/hooks/— TanStack Query hooks (useTasks,useOccurrences,useLists,useRealtime,useAuth,useProfile)src/pages/— Login, Dashboard (Today), Calendar, Lists, Shopping, Settingssrc/components/calendar/— custom month grid + week viewsrc/components/tasks/— form, quick add, list item, occurrence detail, edit-scope dialogsrc/components/recurrence/— recurrence builder with livetoText()preview
A recurring task is a single tasks row with an RRULE; concrete instances live in task_occurrences. One-offs use the same shape (is_recurring=false, one occurrence row). The calendar reads occurrences directly — never expands rules at render time. The unique(task_id, occurrence_date) constraint makes generation idempotent.
Editing a recurring task asks for scope:
- This occurrence: per-occurrence override (
is_exception=true,override_*); delete =status='skipped'. - This and following: split — cap original rule with
UNTIL = boundary - 1d, create new task starting at boundary. - All: update template, delete future not-yet-completed occurrences, regenerate.
Times are anchored in profiles.timezone so "7am every day" stays 7am local across DST.
Additive feature (supabase/migrations/0003_shopping.sql): products (personal catalog that learns from manual adds for autocomplete / quick re-add), shopping_lists, and shopping_list_items. Same RLS pattern as everything else. Adding an item upserts the catalog product by (user_id, lower(name)), denormalizes the name onto the item, and bumps the quantity if an unchecked item with that name is already on the list rather than duplicating. Free-text items (null product_id) are allowed. Post-trip "clear checked" / "clear all" actions; Realtime-synced like tasks. The catalog's barcode/image_url/default_aisle columns are nullable and unused until the v2 hardware phase (scanning / aisle sorting).
supabase/migrations/0004_v2_hardware.sql makes the existing tickets + print_jobs tables functional, adds a gen_ticket_token() helper, gives profiles printer/daily-print settings (printer_width_chars, daily_print_enabled, daily_print_time, default_shopping_list_id), and adds added_via to shopping_list_items. print_jobs joins the supabase_realtime publication so a queued job pushes to the Pi printer client live.
Two edge functions live under supabase/functions/:
resolve-scan— POST{ code, list_id? }. Routes the scan:- If
codelooks like a ticket URL (.../t/<token>) or bare token and matches aticketsrow, mark its linkedtask_occurrencesrowdone(idempotent on repeat). - If
codevalidates as a UPC-A / EAN-13 / EAN-8 (mod-10 check digit in_shared/barcode.ts), resolve the destination list (list_idelseprofiles.default_shopping_list_id), look the product up in the catalog, and on a miss query Open Food Facts; on a hit cache + add (or increment) ashopping_list_itemsrow withadded_via='scan'. - Otherwise →
unknown.
- If
enqueue-print— POST{ type, … }. Builds the agreed payload (src/types/print.ts/_shared/types.ts) and inserts aprint_jobsrow. Fortype: "occurrence"it also mints aticketsrow and embeds the QR URL${APP_URL}/t/<token>so scanning the printout completes the task.
Deploy with:
supabase functions deploy resolve-scan
supabase functions deploy enqueue-print
supabase secrets set APP_URL=https://your-app.vercel.appBoth functions use the caller's JWT — no service-role path on the Pi. RLS scopes everything to auth.uid().
One account, long-lived refresh token. Sign in to the web app once on a paired laptop, copy the refresh_token from localStorage["sb-<project-ref>-auth-token"], drop it into the Pi's config. On boot the Pi calls supabase.auth.setSession({ refresh_token }) and then refreshes periodically. Each refresh rotates the token; persist the new one. The Pi never sees the Supabase JWT secret or service-role key, and you can revoke it at any time from Supabase Studio → Authentication → Users.
Off by default. When you flip profiles.daily_print_enabled = true and set daily_print_time, set up a pg_cron job (Supabase Studio → Database → Extensions → enable pg_cron + pg_net) that hits enqueue-print { "type": "daily" } near that time using the device user's JWT as the Authorization header. The function is timezone-aware via profiles.timezone.
Thermal printer logic, Pi kiosk display, push notifications, Google Calendar sync, AI, multi-user. The v2 backend (above) makes the printer/scanner integration possible; the Pi clients themselves are a separate prompt.