Skip to content

Allow song-specific mix names - #45

Merged
johannesschiessl merged 2 commits into
mainfrom
t3code/rename-mixes-per-song
Aug 4, 2026
Merged

Allow song-specific mix names#45
johannesschiessl merged 2 commits into
mainfrom
t3code/rename-mixes-per-song

Conversation

@johannesschiessl

Copy link
Copy Markdown
Member

Summary

  • Add editable song-specific mix name overrides in the setlist song view.
  • Persist and validate mix name overrides through contracts, RPC, services, and SQLite storage.
  • Use song-specific mix names in live song views, falling back to the show-level mix name.
  • Add coverage for validation, persistence, inheritance, and live-view projection.

Testing

  • Added SongService tests for storing, loading, and removing song-specific mix name overrides.
  • Added contract tests rejecting duplicate mix name overrides.
  • Added live song view tests verifying song-specific mix name resolution.

- Persist mix name overrides per song
- Show overridden names in song editing and live views

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 14 files

Confidence score: 3/5

  • In packages/backend/src/songs/SongService.ts, editing fails when a song has a mix-name override tied to a soft-deleted mix because findMixNames in ShowRepository pulls stale song_mix_names rows without filtering; this creates a concrete user-facing regression where affected songs become uneditable — filter out soft-deleted mixes (or ignore orphaned overrides) when loading mix names.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/backend/src/songs/SongService.ts">

<violation number="1" location="packages/backend/src/songs/SongService.ts:146">
P2: A song that has a stored mix-name override for a mix that has since been soft-deleted can no longer be edited. `findMixNames` in `ShowRepository` loads every `song_mix_names` row for the show's songs without filtering by the mix's `deleted_at`, so `loadDocument` returns those overrides in `song.mixNames`. When the client echoes them back on the next edit, this validation rejects the entire request because the referenced mix is no longer active. The stale override is only ever cleaned up by a successful edit that no longer references it, so the song can be effectively stuck unless the client filters deleted mixes out of the payload. Consider filtering mix-name overrides to active mixes when loading (or dropping them from `params` validation) so a deleted mix does not block edits to unrelated fields.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant UI as SongDetail (Route)
    participant RPC as RpcClient
    participant Handler as RPC Handler (songs.edit)
    participant Sync as Sync Manager
    participant SongSvc as SongService
    participant ShowRepo as ShowRepository
    participant DB as SQLite Database

    Note over UI,DB: Song-Specific Mix Name Flow

    UI->>UI: User edits mix name in ChannelNameEditor
    UI->>UI: Trim input, compare to inherited mix name
    alt Override differs from inherited name
        UI->>UI: Build mixNames array with override
    else Override matches inherited or is empty
        UI->>UI: Remove override from mixNames array
    end

    UI->>RPC: songs.edit({ showId, id, mixNames, ... })
    RPC->>Handler: RPC call with payload

    Handler->>Sync: sync.mutation(songsSyncKey, ...)

    Sync->>SongSvc: edit({ showId, id, mixNames, ... })

    SongSvc->>ShowRepo: load show document
    ShowRepo->>DB: SELECT mixes, songs, ...
    DB-->>ShowRepo: Return mixes with inherited names

    SongSvc->>SongSvc: Validate mixIds exist in active mixes
    SongSvc->>SongSvc: For each mix, compute effective name
    Note over SongSvc: effectiveName = override if non-empty AND differs from inherited, else omit

    alt Validation fails (invalid mixId)
        SongSvc-->>Sync: RpcError("A named mix is invalid...")
        Sync-->>Handler: Error
        Handler-->>RPC: RpcError
        RPC-->>UI: Save error
    else Validation passes
        SongSvc->>ShowRepo: save song with updated mixNames
        ShowRepo->>DB: DELETE existing song_mix_names for this song_id
        ShowRepo->>DB: INSERT new song_mix_names rows
        DB-->>ShowRepo: Success
        ShowRepo-->>SongSvc: Updated Song document with mixNames

        SongSvc-->>Sync: Song (includes mixNames if non-empty)
        Sync-->>Handler: Song
        Handler-->>RPC: Song
        RPC-->>UI: Updated song data

        Note over UI: Live Song View Resolution (separate request)
        participant LiveView as LiveSongView
        participant LegacyUI as LiveSong Component

        LegacyUI->>LiveView: projectLiveSong(song, ...)
        LiveView->>LiveView: Build mixOverrides map from song.mixNames
        LiveView-->>LegacyUI: Projected mixes with song-specific names
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/backend/src/database/migrations/0001_initial.ts
Comment thread apps/web/src/routes/shows/$showId/setlist/$songId.tsx
}
if (
new Set(params.mixNames.map((item) => item.mixId)).size !== params.mixNames.length ||
params.mixNames.some((item) => !activeMixIds.has(item.mixId))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: A song that has a stored mix-name override for a mix that has since been soft-deleted can no longer be edited. findMixNames in ShowRepository loads every song_mix_names row for the show's songs without filtering by the mix's deleted_at, so loadDocument returns those overrides in song.mixNames. When the client echoes them back on the next edit, this validation rejects the entire request because the referenced mix is no longer active. The stale override is only ever cleaned up by a successful edit that no longer references it, so the song can be effectively stuck unless the client filters deleted mixes out of the payload. Consider filtering mix-name overrides to active mixes when loading (or dropping them from params validation) so a deleted mix does not block edits to unrelated fields.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/backend/src/songs/SongService.ts, line 146:

<comment>A song that has a stored mix-name override for a mix that has since been soft-deleted can no longer be edited. `findMixNames` in `ShowRepository` loads every `song_mix_names` row for the show's songs without filtering by the mix's `deleted_at`, so `loadDocument` returns those overrides in `song.mixNames`. When the client echoes them back on the next edit, this validation rejects the entire request because the referenced mix is no longer active. The stale override is only ever cleaned up by a successful edit that no longer references it, so the song can be effectively stuck unless the client filters deleted mixes out of the payload. Consider filtering mix-name overrides to active mixes when loading (or dropping them from `params` validation) so a deleted mix does not block edits to unrelated fields.</comment>

<file context>
@@ -139,6 +141,14 @@ const make = Effect.fnUntraced(function* () {
     }
+    if (
+      new Set(params.mixNames.map((item) => item.mixId)).size !== params.mixNames.length ||
+      params.mixNames.some((item) => !activeMixIds.has(item.mixId))
+    ) {
+      return yield* Effect.fail(
</file context>

Comment thread apps/web/src/routes/shows/$showId/setlist/$songId.tsx Outdated
Comment thread apps/web/src/routes/shows/$showId/setlist/$songId.tsx
Comment thread packages/backend/src/songs/SongService.ts Outdated
- Queue song edits so rapid changes preserve the latest draft
- Ignore pending or deleted mixes in song-specific names
- Report duplicate and missing mix names accurately

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 4 files (changes from recent commits).

Confidence score: 3/5

  • In apps/web/src/routes/shows/$showId/setlist/$songId.tsx, assigning a microphone to a mix during an optimistic create can be dropped by normalization, so the UI appears correct but the assignment never reaches persistence; this is a concrete data-loss/regression path for song edits — keep pending mix-linked microphone assignments in the normalized payload until create resolves.
  • In apps/web/src/routes/shows/$showId/setlist/$songId.tsx, preserveDraftRef.current can stay stuck true after a failed save, which can block expected reset behavior on later song syncs and leave users with stale draft state; add a deterministic clear path on retry/reset (not only on successful run()) to prevent cross-save state leakage.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/src/routes/shows/$showId/setlist/$songId.tsx">

<violation number="1" location="apps/web/src/routes/shows/$showId/setlist/$songId.tsx:192">
P2: After a save fails, `preserveDraftRef.current` is set to `true` and is only ever cleared to `false` inside `run()` on a subsequent *successful* save. The reset `useEffect([song])` that re-syncs the component's internal state (name/artist/notes/assignments/mixNames/draftRef) returns early whenever `preserveDraftRef.current` is `true`. Because `SongDetail` is not keyed by `song.id` and stays mounted across song changes, if the user triggers a save that fails and then navigates to a different song via the `$songId` route, the reset effect is permanently blocked: the inputs keep showing the previous song's (failed) draft while the URL/song prop show the new song. Consider clearing `preserveDraftRef` when the `song.id` changes (or keying `SongDetail` by song id) so a failure in one song cannot freeze draft sync for a different song.</violation>

<violation number="2" location="apps/web/src/routes/shows/$showId/setlist/$songId.tsx:240">
P2: Assigning a microphone to a mix while its optimistic create is still pending appears to work in the song view, but this normalization silently removes that assignment from the song payload, so it is never persisted. The pending mix’s assignment controls could be disabled or the save could be deferred until the mix has a server ID.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if (blockUi && isSavingRef.current) return false;
const activeMixIds = new Set(mixes.map((mix) => mix.id));
const draft = { ...draftRef.current, ...next };
const activeMixIds = new Set(mixes.filter((mix) => !mix.pending).map((mix) => mix.id));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Assigning a microphone to a mix while its optimistic create is still pending appears to work in the song view, but this normalization silently removes that assignment from the song payload, so it is never persisted. The pending mix’s assignment controls could be disabled or the save could be deferred until the mix has a server ID.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/routes/shows/$showId/setlist/$songId.tsx, line 240:

<comment>Assigning a microphone to a mix while its optimistic create is still pending appears to work in the song view, but this normalization silently removes that assignment from the song payload, so it is never persisted. The pending mix’s assignment controls could be disabled or the save could be deferred until the mix has a server ID.</comment>

<file context>
@@ -215,60 +235,76 @@ function SongDetail({
     if (blockUi && isSavingRef.current) return false;
-    const activeMixIds = new Set(mixes.map((mix) => mix.id));
+    const draft = { ...draftRef.current, ...next };
+    const activeMixIds = new Set(mixes.filter((mix) => !mix.pending).map((mix) => mix.id));
     const activeMicrophoneIds = new Set(microphones.map((microphone) => microphone.id));
-    const normalizedAssignments = (next?.assignments ?? assignments).flatMap((assignment) => {
</file context>

const hasUnpairedMix = orderedMixes.filter((mix) => mix.id !== mainMixId).length % 2 === 1;

React.useEffect(() => {
if (queuedSaveCountRef.current > 0 || preserveDraftRef.current) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: After a save fails, preserveDraftRef.current is set to true and is only ever cleared to false inside run() on a subsequent successful save. The reset useEffect([song]) that re-syncs the component's internal state (name/artist/notes/assignments/mixNames/draftRef) returns early whenever preserveDraftRef.current is true. Because SongDetail is not keyed by song.id and stays mounted across song changes, if the user triggers a save that fails and then navigates to a different song via the $songId route, the reset effect is permanently blocked: the inputs keep showing the previous song's (failed) draft while the URL/song prop show the new song. Consider clearing preserveDraftRef when the song.id changes (or keying SongDetail by song id) so a failure in one song cannot freeze draft sync for a different song.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/routes/shows/$showId/setlist/$songId.tsx, line 192:

<comment>After a save fails, `preserveDraftRef.current` is set to `true` and is only ever cleared to `false` inside `run()` on a subsequent *successful* save. The reset `useEffect([song])` that re-syncs the component's internal state (name/artist/notes/assignments/mixNames/draftRef) returns early whenever `preserveDraftRef.current` is `true`. Because `SongDetail` is not keyed by `song.id` and stays mounted across song changes, if the user triggers a save that fails and then navigates to a different song via the `$songId` route, the reset effect is permanently blocked: the inputs keep showing the previous song's (failed) draft while the URL/song prop show the new song. Consider clearing `preserveDraftRef` when the `song.id` changes (or keying `SongDetail` by song id) so a failure in one song cannot freeze draft sync for a different song.</comment>

<file context>
@@ -178,6 +189,15 @@ function SongDetail({
   const hasUnpairedMix = orderedMixes.filter((mix) => mix.id !== mainMixId).length % 2 === 1;
 
   React.useEffect(() => {
+    if (queuedSaveCountRef.current > 0 || preserveDraftRef.current) return;
+    draftRef.current = {
+      name: song.name,
</file context>

@johannesschiessl
johannesschiessl merged commit 5ae8c3e into main Aug 4, 2026
2 checks passed
@johannesschiessl
johannesschiessl deleted the t3code/rename-mixes-per-song branch August 4, 2026 19:11
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.

1 participant