-
Notifications
You must be signed in to change notification settings - Fork 0
feat(schedule): publishable schedules via reveal-level enum #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
c28afc0
docs: seed CONTEXT.md and ADR-0001 for schedule reveal level
claude f5cd61c
docs(adr-0001): record client-side masking trade-off
claude 99fc176
feat(schedule): add schedule_reveal_level enum + column (#46)
claude c36ae12
feat(schedule): add reveal-level masking util (#46)
claude c352c7e
feat(schedule): apply reveal level to set/stage rendering (#46)
claude 3749030
feat(schedule): admin controls for reveal level (#46)
claude 6755a4b
feat(schedule): level-aware placeholder, tab indicator, import warnin…
claude a9d511c
refactor(schedule): extract ScheduleRevealLevelField, drop obvious co…
claude 5018e2c
refactor(schedule): move reveal control to admin app, single advance …
claude b82486d
fix(schedule): invalidate edition-by-slug cache; extract LiveCommitWa…
claude 18ffc18
refactor(schedule): drop unused isAdmin param from mask helpers (#46)
claude e0a66bf
refactor(tabs): move tab indicator into TabConfig (#46)
claude d326db9
fix(schedule): apply reveal mask at the Artists-tab read boundary (#46)
claude b01f9ee
Revert "fix(schedule): apply reveal mask at the Artists-tab read boun…
claude 1dc07fe
fix(schedule): hide stage filter when stages aren't revealed (#46)
claude bc9e218
fix(schedule): make reveal status pill readable on admin background (…
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # UpLine | ||
|
|
||
| Collaborative festival voting platform. Core Team curates a lineup; festival-goers vote on artists and (later) coordinate around set times. | ||
|
|
||
| ## Language | ||
|
|
||
| **Festival**: | ||
| A recurring music festival (e.g. Boom). Has many editions over time. | ||
| _Avoid_: Event | ||
|
|
||
| **Edition**: | ||
| A single instance of a festival in a given year (e.g. Boom 2025). Owns its own lineup, schedule, stages, and voting. | ||
| _Avoid_: Year, instance, event | ||
|
|
||
| **Lineup**: | ||
| The set of artists associated with an edition. Visible to voters once the edition is published. | ||
| _Avoid_: Roster, bill | ||
|
|
||
| **Schedule**: | ||
| The arrangement of an edition's **sets** across **stages** and time. Presented to users via the Timeline and List views on the Schedule tab. Not a stored entity — derived from sets + stages of an edition. | ||
| _Avoid_: Lineup (lineup = who; schedule = when/where), program, timetable | ||
|
|
||
| **Set**: | ||
| A single scheduled performance within an edition, with one or more artists, a stage, and a start/end time. | ||
| _Avoid_: Show, gig, slot, performance | ||
|
|
||
| **Stage**: | ||
| A named venue/space within an edition where sets take place. | ||
| _Avoid_: Venue, room | ||
|
|
||
| **Vote**: | ||
| A user's reaction to an artist within a group context. Three values: "Must Go" (+2), "Interested" (+1), "Won't Go" (-1). | ||
| _Avoid_: Rating, like | ||
|
|
||
| **Group**: | ||
| A collection of users who share votes and notes for collaborative decision-making within an edition. | ||
| _Avoid_: Team, party | ||
|
|
||
| **Core Team**: | ||
| Admin users who curate editions, manage the lineup, and import the schedule. | ||
| _Avoid_: Staff, organizers, moderators | ||
|
|
||
| ### Publish states | ||
|
|
||
| An edition has two independent publish states. | ||
|
|
||
| **Edition published**: | ||
| The edition's lineup is publicly visible and voting is open. Users can read artists and vote. A boolean. | ||
| _Avoid_: Live, released | ||
|
|
||
| **Schedule reveal level**: | ||
| How much of the **schedule** (set times + stage assignments) is exposed to the public for this edition. Independent of edition published — the lineup can be live while the schedule is still hidden. An ordered enum, not a boolean. Levels (low → high): | ||
|
|
||
| 1. **`draft`** — no schedule info revealed beyond the artist/set existing. | ||
| 2. **`days`** — the day each set takes place is visible. Stage and time-of-day are hidden. | ||
| 3. **`stages`** — day + stage are visible ("Artist X plays Main Stage on Friday"). Exact start/end time hidden. Implies `days`. | ||
| 4. **`full`** — exact start/end times are visible. Implies `stages`. | ||
|
|
||
| Each level reveals strictly more than the previous. There is no "stages without days" state. | ||
| _Avoid_: Schedule released, schedule published (as a boolean), timetable live | ||
|
|
||
| ## Example dialogue | ||
|
|
||
| > **Dev**: For Boom 2025, when do users first see the artists? | ||
| > **Domain**: As soon as the **edition** is **published**. They can read the **lineup** and start voting. The schedule reveal level is still `draft` at that point. | ||
| > **Dev**: When do set times show up? | ||
| > **Domain**: We move the **schedule reveal level** in steps. First we bump to `days` so people see which day each artist plays. Closer to the festival we bump to `stages` — now they know which stage too. The day before, we go to `full` and exact start/end times are public. | ||
| > **Dev**: So `stages` doesn't reveal times? | ||
| > **Domain**: Right. You see "Artist X, Friday, Main Stage" but not "18:00". Times only appear at `full`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Schedule visibility uses an ordered reveal level, not a boolean | ||
|
|
||
| Festivals progressively reveal their schedule in phases (lineup → days → stages → exact times), so a single `schedule_published` boolean was insufficient. We model schedule visibility as an ordered enum `schedule_reveal_level` on `festival_editions` with values `draft < days < stages < full`. Each level reveals strictly more set fields (`time_start` date → `stage_id` → time-of-day + `time_end`) to non-admins. The level is independent of `edition.published` — that flag governs the lineup and voting; this enum governs the "when/where" layer of the lineup. | ||
|
|
||
| ## Considered Options | ||
|
|
||
| - **Boolean `schedule_published`.** Rejected: didn't fit phases like "stages known, times not yet" that the domain expert called out. | ||
| - **Independent booleans (`days_published`, `stages_published`, `times_published`).** Rejected: gives 8 combinations, half nonsensical (e.g. times without days). Ordered enum captures the natural reveal progression and prevents bad states. | ||
| - **Separate `schedules` entity with versioned drafts.** Rejected as out of scope: heavy data-model change for a workflow festivals rarely use (re-publishing a different version), when live-writes with a commit warning cover the same need. | ||
| - **Per-day or per-stage granularity (publish Friday before Saturday).** Considered but not adopted: orthogonal axis to the reveal level, can be added later without breaking the enum. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Reading set timing/stage fields for non-admins is masked **on the client** based on the edition's reveal level. We deliberately chose this over a server-side view for simplicity. The trade-off: the embargoed fields (`time_start` time-of-day, `time_end`, `stage_id`) are still sent on the wire — anyone with devtools or direct Supabase access can read them. Acceptable because the schedule is embargoed, not secret. If we ever need true leak-proofness, the upgrade path is a `public_sets` view + revoke of public SELECT on `sets`. | ||
| - "Stages always implies days" is enforced by the ordering. If we ever need "stages without days," the enum must be replaced with independent flags — that's a breaking change. | ||
| - The Schedule tab's UI for `days` and `stages` is deliberately deferred: it shows the existing "coming soon" placeholder until `full`, while the progressive reveal manifests on Artist-tab SetCards. A multi-mode Schedule tab is tracked as a separate task. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { AlertTriangle } from "lucide-react"; | ||
| import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; | ||
| import type { RevealLevel } from "@/lib/scheduleReveal"; | ||
|
|
||
| const LEVEL_DESCRIPTION: Record<RevealLevel, string> = { | ||
| draft: "draft (not visible to the public)", | ||
| days: "days revealed", | ||
| stages: "stages revealed", | ||
| full: "full schedule revealed", | ||
| }; | ||
|
|
||
| type Props = { | ||
| level: RevealLevel; | ||
| setsToCreate: number; | ||
| setsToUpdate: number; | ||
| setsToArchive: number; | ||
| }; | ||
|
|
||
| export function LiveCommitWarning({ | ||
| level, | ||
| setsToCreate, | ||
| setsToUpdate, | ||
| setsToArchive, | ||
| }: Props) { | ||
| if (level === "draft") return null; | ||
| return ( | ||
| <Alert> | ||
| <AlertTriangle className="h-4 w-4" /> | ||
| <AlertTitle>Schedule is {LEVEL_DESCRIPTION[level]}.</AlertTitle> | ||
| <AlertDescription> | ||
| Committing will update what the public sees immediately: {setsToCreate}{" "} | ||
| new · {setsToUpdate} updated · {setsToArchive} archived. | ||
| </AlertDescription> | ||
| </Alert> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; | ||
| import { | ||
| type MaskableSet, | ||
| type RevealLevel, | ||
| canShowDay, | ||
| canShowStage, | ||
| canShowTime, | ||
| maskSetForReveal, | ||
| } from "@/lib/scheduleReveal"; | ||
|
|
||
| export function useScheduleReveal() { | ||
| const { edition } = useFestivalEdition(); | ||
| const level: RevealLevel = edition?.schedule_reveal_level ?? "draft"; | ||
|
chiptus marked this conversation as resolved.
|
||
|
|
||
| return { | ||
| level, | ||
| canShowDay: canShowDay(level), | ||
| canShowStage: canShowStage(level), | ||
| canShowTime: canShowTime(level), | ||
| maskSet<T extends MaskableSet>(set: T): T { | ||
| return maskSetForReveal(set, level); | ||
| }, | ||
|
chiptus marked this conversation as resolved.
|
||
| }; | ||
| } | ||
|
chiptus marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.