Skip to content

feat: schedule ingestion engine#31

Open
chiptus wants to merge 23 commits into
mainfrom
feat/schedule-ingestion
Open

feat: schedule ingestion engine#31
chiptus wants to merge 23 commits into
mainfrom
feat/schedule-ingestion

Conversation

@chiptus
Copy link
Copy Markdown
Owner

@chiptus chiptus commented May 9, 2026

Replaces the old client-side CSV import with a server-side ingestion system.

Two Supabase Edge Functions (diff-schedule, commit-schedule) handle the diff and atomic commit via a Postgres RPC. The frontend wizard walks admins through upload → conflict resolution → commit.

Key design decisions: sets matched by artist roster + stage (preserving votes), orphaned sets surfaced as explicit archive/keep conflicts, stage name mismatches resolved via map-to-existing or create-new, all writes wrapped in a single transaction with full rollback on failure.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
upline Ready Ready Preview, Comment May 11, 2026 4:52am

chiptus and others added 2 commits May 9, 2026 18:27
Implements the dry-run diff engine for the new schedule ingestion system.
Compares a CSV payload against current DB state for a festival edition and
returns clean operations + conflicts requiring user resolution (orphaned sets,
stage name mismatches). Core business logic extracted into diff.ts with 22
unit tests covering slugging, timezone conversion, B2B matching, and midnight crossing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the atomic write path for the schedule ingestion system:
- Migration: adds UNIQUE constraints on artists.slug and stages(festival_edition_id, name),
  creates the commit_schedule PL/pgSQL RPC that wraps all writes (artist upserts,
  stage upserts, set inserts/updates, set_artists sync, orphan archiving) in a
  single transaction with full rollback on failure.
- Edge Function: thin admin-gated HTTP handler that calls the RPC via service role key.
- Integration tests for the RPC covering create, update, archive, and time storage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the legacy client-side CSV schedule import with a server-driven ingestion workflow backed by Supabase Edge Functions and a transactional Postgres RPC, plus a new admin import wizard UI (upload → diff/conflicts → commit).

Changes:

  • Added diff-schedule and commit-schedule Edge Functions, plus a commit_schedule Postgres RPC to perform atomic schedule writes.
  • Implemented a new admin schedule import wizard UI with stage-mismatch and orphan-set resolution flows.
  • Added unit tests for diffing logic and integration tests for the commit RPC.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
vite.config.ts Adds test runner config exclusions.
supabase/migrations/20260509142022_commit_schedule_rpc.sql Adds constraints and the transactional commit_schedule RPC used by ingestion.
supabase/functions/_shared/auth.ts Shared admin auth + CORS helpers for Edge Functions.
supabase/functions/diff-schedule/index.ts Edge Function endpoint to compute a diff from CSV rows vs DB.
supabase/functions/diff-schedule/diff.ts Core diff/matching logic (artists, stages, sets, orphan detection).
supabase/functions/diff-schedule/diff.test.ts Unit tests covering slugging, time conversion, matching rules, and conflicts.
supabase/functions/commit-schedule/index.ts Edge Function endpoint that calls the commit_schedule RPC.
supabase/functions/commit-schedule/commit-schedule.test.ts Integration tests targeting the RPC behavior against local Supabase.
src/services/scheduleImportService.ts Frontend service layer for parsing CSV + invoking diff/commit + building commit payloads.
src/pages/admin/FestivalScheduleImport.tsx New admin page wrapper for the import wizard route.
src/pages/admin/FestivalEdition.tsx Adds an “Import” tab and routing to the new import page.
src/components/router/GlobalRoutes.tsx Wires the /import sub-route under festival edition admin routes.
src/components/Admin/ScheduleImport/ScheduleImportWizard.tsx Wizard state machine: upload → review → commit result, plus cache invalidation.
src/components/Admin/ScheduleImport/CsvUploadStep.tsx CSV upload + timezone selection + invokes diff.
src/components/Admin/ScheduleImport/DiffReviewStep.tsx Review UI container including conflicts and commit action.
src/components/Admin/ScheduleImport/DiffSummaryBanner.tsx Summary banner for diff results.
src/components/Admin/ScheduleImport/StageMismatchResolver.tsx UI to map mismatched stage names or create new stages.
src/components/Admin/ScheduleImport/OrphanedSetsPanel.tsx UI to archive/keep orphaned sets not present in CSV.
src/components/Admin/ScheduleImport/CommitResultCard.tsx Success UI and “import another file” reset action.

Comment thread supabase/migrations/20260509142022_commit_schedule_rpc.sql
Comment thread supabase/migrations/20260509142022_commit_schedule_rpc.sql Outdated
Comment thread supabase/migrations/20260509142022_commit_schedule_rpc.sql Outdated
Comment thread supabase/functions/commit-schedule/commit-schedule.test.ts Outdated
Comment thread supabase/functions/commit-schedule/commit-schedule.test.ts Outdated
Comment thread src/components/Admin/ScheduleImport/ScheduleImportWizard.tsx Outdated
Comment thread src/components/Admin/ScheduleImport/CsvUploadStep.tsx Outdated
Comment thread src/components/Admin/ScheduleImport/StageMismatchResolver.tsx Outdated
Comment thread src/components/Admin/ScheduleImport/StageMismatchResolver.tsx Outdated
chiptus and others added 4 commits May 9, 2026 18:35
Adds the full schedule ingestion frontend:
- ScheduleImportWizard: 3-step flow (upload → review → result)
- CsvUploadStep: file drop zone, timezone picker, CSV parse + diff call
- DiffSummaryBanner: counts for new artists/stages/sets/conflicts
- StageMismatchResolver: map-to-existing (dropdown) or create-new per mismatch
- OrphanedSetsPanel: per-set archive/keep toggle, bulk action, default keep
- scheduleImportService: CSV parser, Edge Function callers, commit payload builder
- Import tab added to FestivalEdition page, route wired in GlobalRoutes

Refactors diff.ts SetPayload to use stageName instead of stage_id so
the payload aligns directly with what the commit RPC expects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts DiffReviewStep and CommitResultCard from ScheduleImportWizard to
keep all components under 150 lines per codebase conventions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents Vitest from picking up supabase/functions Deno tests and
tests/e2e Playwright specs, which caused import errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deletes CSVImportDialog and csvImportService (326 lines of client-side
import logic). Import CSV buttons removed from StageManagement and
SetManagement — replaced by the dedicated Import tab on the edition page.
parseCSV inlined into scheduleImportService to remove the dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chiptus chiptus force-pushed the feat/schedule-ingestion branch from 4f1b288 to c8110dd Compare May 9, 2026 15:41
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

DB Migrate succeeded for stagingworkflow run.

Comment thread src/components/Admin/ScheduleImport/CsvUploadStep.tsx Outdated
Comment thread src/components/Admin/ScheduleImport/CsvUploadStep.tsx
Comment thread src/pages/admin/festivals/FestivalScheduleImport.tsx Outdated
Comment thread src/services/scheduleImportService.ts
Comment thread supabase/functions/diff-schedule/diff.ts Outdated
Comment thread supabase/migrations/20260509142022_commit_schedule_rpc.sql
Comment thread src/components/Admin/ScheduleImport/StageMismatchResolver.tsx Outdated
Comment thread src/components/Admin/ScheduleImport/ScheduleImportWizard.tsx Outdated
Comment thread src/components/Admin/ScheduleImport/OrphanedSetsPanel.tsx Outdated
claude added 12 commits May 9, 2026 16:10
Previously v_sets_updated incremented unconditionally regardless of whether
the UPDATE matched a row, and the subsequent DELETE FROM set_artists ran
without any edition filter. Combined with the service-role context, a
forged or wrong-edition set id could wipe artist links across editions
while still being reported as a successful update.

Capture ROW_COUNT after the UPDATE, raise if no row matched (so the
transaction rolls back), and join the DELETE through sets with the
edition filter as a defense-in-depth guard.
Duplicate slugs in the CSV (or duplicate rows resolving to the same artist)
would error against the UNIQUE(set_id, artist_id) constraint and roll back
the whole import. Match the update path's behaviour and skip duplicates.
Hand-rolled keys like ["sets", editionId] don't match the actual factories
(setsKeys.byEdition is ["sets","edition",editionId], stagesKeys.byEdition is
["stages", { editionId }]), so the schedule view silently failed to refresh
after a successful import. Use the factories so the invalidation actually
hits the live caches.
The earlier removal commit (c8110dd) deleted CSVImportDialog and
csvImportService but left their dependents — CSVImportPage, the old
$festivalId/$editionId/import route, the entire src/services/csv tree,
useMatchingSetsQuery, and a stale Upload-CSV button in StageManagement —
all of which still pointed at the deleted route.

Also imports CsvRow into CsvUploadStep where it was used as a state
generic but never declared (PR review comment) and drops the now-unused
CsvRow import in ScheduleImportWizard. Regenerates routeTree.gen.ts so
the new edition-scoped /import route is registered.

These are bundled because the project-wide typecheck pre-commit hook
won't pass with either fix in isolation.
Raw mismatch.csvValue went straight into id/htmlFor, so stage names with
spaces or special characters produced invalid HTML, and duplicate names
across rows would collide and break label/radio binding. Extracting a
MismatchRow child component lets us use useId() per row so each option
gets a unique, valid id without sanitisation gymnastics.

Addresses both the DOM-id PR comment and the request to break the loop
body into its own component.
Two leak fixes for the integration tests:
- The first test only deleted the artist, leaving the created set in the
  edition forever. Use a unique set name and delete by name+edition.
- The midnight-crossing test selected sets without id, then tried to
  delete by sets[0].id, which silently no-op'd. Include id in the select.
Replace the manual loading/error/result state in CsvUploadStep
(file read + analyse) and ScheduleImportWizard (commit) with
useMutation. Extract the FileReader-based handler into a plain
async readFile(file) so the mutation can simply await it instead
of wrapping the callback API. Mutation status drives the UI state
directly — no more setLoading/setError/setCommitting bookkeeping.
…Zone

CsvUploadStep was carrying a Select, a drop zone, hidden input, helper
text, and the action button all in one component. Pull the timezone Select
into TimezonePicker and the drop zone (icon, file name, row count, hidden
input, format helper) into CsvDropZone. The parent now reads as the actual
flow: pick a timezone, drop a file, hit analyse.
The per-row layout with switch and labels was inline inside the panel
mapper. Pull it into OrphanedItem so the panel reads as 'header + list'
and the row owns its own switch wiring.
Inline the page component into the import route. The wrapper was a
3-line file that only forwarded params, so keeping it as a separate
module didn't add anything.
The component-level useQuery duplicated work the parent route already does
via beforeLoad ensureQueryData. Hoist the call into a route loader so the
component receives a resolved FestivalEdition through useLoaderData and
the loading/not-found branches go away — the router blocks rendering until
the data is ready.
The original 100+ line loop body did artist resolution, stage resolution
with a four-branch tree, time computation, set matching, and dispatch
all inline. Pull each phase into a named helper:
- buildIndexes for the lookup maps
- resolveArtists for slug derivation + new-artist accumulation
- resolveStage returning a tagged kind so the caller maps it onto the
  right accumulator
- computeTimes for the date/time conversion (incl. midnight crossing)
- findMatchingSet for the candidate-narrowing logic

The orchestrator now reads as the actual pipeline: resolve → match →
dispatch. Behaviour is unchanged.
claude added 3 commits May 9, 2026 16:28
Replace the ad-hoc 'is festivalEditionId truthy?' check with a zod schema
covering every field the RPC consumes, including UUID format on the
edition id and archive ids. Bad input now returns 400 with the field-level
issues instead of failing later inside the RPC with an opaque error.
…, timestamp parse and artist sync

The body of commit_schedule was repeating four patterns inline:
- a stage_id resolution subquery on (edition, name)
- a CASE WHEN ... ::TIMESTAMPTZ for nullable timestamp casts
- a hand-rolled regex slug builder
- the delete-then-insert-on-conflict dance for set_artists

Pull each into a commit_schedule__-prefixed helper so the main body
reads as the actual workflow rather than a wall of subqueries. Behaviour
is unchanged — the sync helper still scopes its DELETE through sets to
preserve edition isolation.
Two unrelated test infra fixes that were both pre-existing:

- Vitest reads vitest.config.ts when present, which overrides the test
  block in vite.config.ts. The previous fix added 'supabase/**' to
  vite.config.ts only, so the Deno tests in supabase/functions/ kept
  getting picked up. Move the exclude into vitest.config.ts and drop
  the dead block in vite.config.ts.
- The Supabase client throws at module init when the env vars are
  missing. Component tests that mock the query hooks still trigger that
  init through the import graph. Stub the two vars in src/test/setup.ts
  so the client can construct (it's never actually called).
Two pre-existing oxlint failures the project-wide lint surfaced:
- scheduleImportService.parseScheduleCsv had an arrow-function const
  helper, which the func-style rule rejects.
- commit-schedule.test.ts midnight-crossing test destructured data but
  only asserted on error.
The migration was failing on staging because (a) the artists.slug dedup
suffix wasn't guaranteed unique — using just the first 6 chars of the
id can still collide — and (b) stages had duplicate (edition, name)
pairs in prod that blocked the new unique constraint outright.

Switch both dedups to append the full id, which is guaranteed unique.
Add a stages dedup mirroring the artists one. Wrap both ADD CONSTRAINT
statements in DO blocks that skip if a constraint of the same name (or
the equivalent stages_name_festival_edition_id_key from PR #28) already
exists, so the migration is safe to re-run.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 53 changed files in this pull request and generated 7 comments.

Comment thread src/routeTree.gen.ts
Comment on lines 231 to 233
'/admin/festivals/$festivalSlug': typeof AdminFestivalsFestivalSlugRouteWithChildren
'/admin/festivals/import': typeof AdminFestivalsImportRoute
'/festivals/$festivalSlug/': typeof FestivalsFestivalSlugIndexRoute
Comment on lines +201 to +206
(resolvedStageId
? (candidates.find((s) => s.stage_id === resolvedStageId) ?? null)
: null) ??
(date
? (candidates.find((s) => s.time_start?.startsWith(date)) ?? null)
: null) ??
Comment on lines +61 to +66
SELECT LOWER(
REGEXP_REPLACE(
REGEXP_REPLACE(TRIM(p_name), '[^a-zA-Z0-9\s]', '', 'g'),
'\s+', '-', 'g'
)
);
Comment on lines +193 to +196
p_festival_edition_id,
v_set_elem->>'name',
commit_schedule__slugify(v_set_elem->>'name'),
NULLIF(v_set_elem->>'description', ''),
Comment on lines +162 to +167
name = v_set_elem->>'name',
description = NULLIF(v_set_elem->>'description', ''),
stage_id = commit_schedule__resolve_stage_id(
p_festival_edition_id, v_set_elem->>'stageName'
),
time_start = commit_schedule__parse_ts(v_set_elem->>'timeStart'),
Comment on lines +63 to +66
} catch (error) {
console.error("diff-schedule error:", error);
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
Comment on lines +87 to +90
} catch (error) {
console.error("commit-schedule error:", error);
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants