From 809c0661841cf19d4bdaa186aa2fa260081e4cdf Mon Sep 17 00:00:00 2001 From: gramps Date: Sun, 23 Aug 2026 07:25:54 -0700 Subject: [PATCH] Fix: derive IsBackground from Type on layout load (legacy rows healed) --- HANDOFF.md | 56 ++++++++++++++------- Services/LayoutStore.cs | 8 ++- TASKS.md | 3 +- ai.md | 2 +- ytLive.Tests/LayoutStorePersistenceTests.cs | 42 ++++++++++++++++ 5 files changed, 88 insertions(+), 23 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 73a3f07..f3a1fe4 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,31 +1,49 @@ # HANDOFF — Session State ## Branch -`main` — clean, HEAD `d2114c7`, 0 warnings. Not pushed. +`main` — **dirty (2 files, uncommitted)**, HEAD `46da995`. Not pushed. -## What's in commit d2114c7 (this session) -1. **All-five-scenes backgrounds** (prior session work, was uncommitted): `Scene.UseDefaultBackground` + `CustomBackgroundPath` on `SceneElement` superclass, LayoutStore persistence (`BackgroundPath_{sourceId}` / `BackgroundUseDefault_{sourceId}` in Settings), `SceneCatalog.HasBackground` true for all five scenes, `EnforceBackgroundPolicy` no longer strips non-Live backgrounds, tests updated. -2. **UI polish (this session):** - - "Use default background:" label + pill toggle on ONE horizontal line (MainWindow.xaml ~612) - - PillToggle resized 48×24 → 36×18 (thumb 18→12, travel 24→18) to match 11px label (Themes/Controls.xaml) - - New `SubHeader` style (11px SemiBold #a0a0b0, indented 4px); "BACKGROUND" → "Background" sub-title; Background section contents indented under it — PROPERTIES stays the only title-styled text +## Uncommitted work (this session): IsBackground load-time derivation fix + +The user-reported bug "Background properties show for only Starting and Live" is FIXED: + +1. **Root cause confirmed:** `Source.Type`'s setter derives `IsBackground` (`Type == Background`), + but `LayoutStore.Load()`'s object initializer assigned the DB **column** afterward — rows saved + before b74f15f carry `IsBackground=0` for `Type='Background'` and stayed false after load, so the + properties panel (`SelectedElement.IsBackground`) stayed hidden in BRB/Chat/Ending. +2. **Fix** (`Services/LayoutStore.cs` ~601): hoist the `Type` parse, then + `IsBackground = type == SourceType.Background || `. The OR keeps the persisted bit for + legacy DisplayCapture backdrops (column=1 must survive so the live-capture layer stays flagged). + First save after load self-heals the columns. +3. **Test** (this branch's ONE integration test): + `LayoutStorePersistenceTests.Legacy_IsBackground_Zero_Rows_Derive_From_Type_On_Load` — saves a BRB + scene holding DisplayCapture + Background + Image, zeroes the legacy columns via raw SQL, reloads, + asserts Background→true / DisplayCapture→true / Image→false. +4. Build **0 warnings**; **212 total, 209 pass** (3 pre-existing failures, below). ## ⚠️ Landmines -- **User verdict (2026-08-23):** "The AI wrote fucked up code after repeatedly and intentionally misunderstanding instructions." This extends the ai.md 2026-08-22 audit flag — trust nothing from those sessions without manual verification. User switched models. -- **1 pre-existing test failure:** AudioPipeline gain-math (210/211 pass). + +- **User verdict (2026-08-23):** "The AI wrote fucked up code after repeatedly and intentionally + misunderstanding instructions." This extends the ai.md 2026-08-22 audit flag — trust nothing from + those sessions without manual verification. User switched models. +- **Pre-existing test failures are THREE, not one (handoff correction — verified by stashing and + running clean HEAD):** + 1. `AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback` (gain-math) + 2. `SourceNamingTests.Duplicate_Sources_Get_Next_Free_Numbered_Name` — asserts StagedScene names == + ["Text","Text2","Text3"], but StagedScene now carries a seeded "Game Capture" element (d2114c7 + made all five scenes background-enabled); the test must filter the background out. + 3. `SceneCatalogTests.EnforceBackgroundPolicy_Sets_Flag_For_All_Scenes` — calls `EnsureBackground` + on scenes with `HasBackground=false`; that returns null **by design**, so it asserts against + elements never inserted. + All three are fallout of the audit-flagged sessions' background-policy churn. NOT fixed here + (Good Dog Rule — this branch carries its one test only). - **Real-MainWindow tests MUST set `LayoutPathOverride`** (see ai.md) — DB pollution wiped real data once. -## 🔴 Open bug (user-reported, NOT fixed): Background properties show for only Starting and Live -Evidence gathered before stop: -- Properties gate: `SelectedElement.IsBackground` (MainWindow.xaml ~608–611) -- Seeding is correct: `EnsureDefaultBackground` / seed methods all set `Type = SourceType.Background` -- **Root cause suspect:** `LayoutStore.cs:616` loads `IsBackground` from the DB **column**, not derived from `Type`. Rows saved before b74f15f (which made the Type setter derive IsBackground) carry `IsBackground=0` and stay false after load → panel hidden in BRB/Chat/Ending. -- **Likely fix:** derive `IsBackground = Type == SourceType.Background` on load (or backfill migration). Not attempted. - ## Next steps -1. Fix the load-time IsBackground derivation bug (ONE integration test per Good Dog Rule) -2. Visual test on Windows display: pill sizing/alignment, sub-header hierarchy, backgrounds in all five scenes -3. Then: thumbnail drag-reorder → real compositor snapshots → transition dropdown UI (per previous plan) +1. Commit this fix (code + docs, one commit). +2. Fix the two stale background tests (tiny, separate commit each or together — they are unit-level repairs, not features). +3. Visual test on Windows display: pill sizing/alignment, sub-header hierarchy, Background properties panel in all five scenes (BRB/Chat/Ending should show it again). +4. Then: thumbnail drag-reorder → real compositor snapshots → transition dropdown UI (per previous plan). ## Build ```bash diff --git a/Services/LayoutStore.cs b/Services/LayoutStore.cs index aafaf79..d33a4d1 100644 --- a/Services/LayoutStore.cs +++ b/Services/LayoutStore.cs @@ -598,11 +598,12 @@ public class LayoutStore : IDisposable while (reader.Read()) { var sceneId = reader.GetString(1); + var type = Enum.Parse(reader.GetString(3)); var source = new Source { Id = reader.GetString(0), AssetId = reader.IsDBNull(2) ? null : reader.GetString(2), - Type = Enum.Parse(reader.GetString(3)), + Type = type, Name = reader.GetString(4), IsEnabled = reader.GetInt32(5) != 0, X = reader.GetDouble(6), @@ -613,7 +614,10 @@ public class LayoutStore : IDisposable MonitorIndex = reader.IsDBNull(11) ? null : reader.GetInt32(11), ClipShape = Enum.TryParse(reader.GetString(12), out var clip) ? clip : ClipShape.Traditional, IsMirrored = reader.GetInt32(13) != 0, - IsBackground = reader.GetInt32(14) != 0, + // Rows saved before the Type setter derived the flag carry + // IsBackground=0; derive from Type, keeping the persisted bit + // for legacy DisplayCapture backdrops. + IsBackground = type == SourceType.Background || reader.GetInt32(14) != 0, CaptureKey = reader.IsDBNull(15) ? null : reader.GetString(15), ShowDesktop = reader.GetInt32(16) != 0, ChatFontSize = reader.IsDBNull(17) ? 16 : reader.GetDouble(17), diff --git a/TASKS.md b/TASKS.md index e3b051d..123c0f7 100644 --- a/TASKS.md +++ b/TASKS.md @@ -552,7 +552,8 @@ the validator → persisted), compositor bar overlay (top/bottom + above-flash), 3. ✅ File-model save/open — the active layout file is tracked (default is the AppData DB); **Save Layout As… / Open Layout…** switch the active file; auto-save writes to whatever is active 4. ✅ Auto-save (invisible) — ~1.5s debounce on scene/source add/remove/reorder/rename/hide + any source transform change; flush on window close 5. ✅ Startup — load the active file; seed the five canonical scenes only when the DB is empty; (+) re-adds a missing canonical scene and is hidden once all five are present; adding beyond the five is rejected -6. ✅ Schema v1 → v8 — webcam columns (v2), singleton `Webcam` + per-scene `WebcamSceneConfig` (v3), `RectWidth`/`RectHeight` round-to-rect restore (v4), `Source.IsBackdrop` + `Source.CaptureKey` (v5), `Scene.HasBackdrop` — backdrop **Live-only by policy** (v6, one-time backfill + `EnforceBackdropPolicy` on every load), `Scene.HasSocialBar` (v7, dropped per-scene toggle — column back-compat, unread), `Socials.BarEnabled` (v8); the `SocialEntry.Software` fediverse-software column is a **column-presence migration** (commented v8→v9, no version bump — `user_version` stays 8); `WindowHandle` stays in-memory (per-session); save = transactional rewrite; orphaned assets pruned. **v9 lands in the TASK 8 audio milestone** (single-row `Music` — `TrackPath`/`IsEnabled`) +6. ✅ Schema v1 → v9 — webcam columns (v2), singleton `Webcam` + per-scene `WebcamSceneConfig` (v3), `RectWidth`/`RectHeight` round-to-rect restore (v4), `Source.IsBackdrop` + `Source.CaptureKey` (v5), `Scene.HasBackdrop` — backdrop **Live-only by policy** (v6, one-time backfill + `EnforceBackdropPolicy` on every load), `Scene.HasSocialBar` (v7, dropped per-scene toggle — column back-compat, unread), `Socials.BarEnabled` (v8); the `SocialEntry.Software` fediverse-software column is a **column-presence migration** (commented v8→v9, no version bump — `user_version` stays 8); v9 = single-row `Music`. **Load-time rule:** sources load with `IsBackground` derived from `Type` (OR'd with the persisted column so legacy DisplayCapture backdrops keep their flag) — rows saved before the Type setter derived the flag heal on load +7. ✅ WindowHandle stays in-memory (per-session); save = transactional rewrite; orphaned assets pruned. **v9 lands in the TASK 8 audio milestone** (single-row `Music` — `TrackPath`/`IsEnabled`) ### Design decisions diff --git a/ai.md b/ai.md index 988939f..9d87a91 100644 --- a/ai.md +++ b/ai.md @@ -137,7 +137,7 @@ C# / WPF (.NET 8) following MVVM: - **Audio is KISS by rule** — the whole of audio is *one knob*: **desktop/game audio is automatic** (WASAPI loopback from the default output at unity, zero UI — "it just is"); the **mic is the creator's only audio control** — sound meter + mute button + volume slider (`MicVolume`, defaults to 0.8) all sit together on the footer's top line, CENTERED beneath the preview panel. Meter: 288px, muted slate track (`#3a3b52`) with ruler graduations and muted yellow/red zone tints at 60%/80%; fill = green → yellow → red via `MeterFillWidth`/`MeterBrush`; the meter is a **READ-ONLY realtime level display** — it shows the live input level scaled by the volume (raising the volume moves ambient noise up the bar), NOT the volume setting: the fill is `Math.Min(1, AudioLevelMeter.ToDisplay(AudioLevel) * MicVolume)` — `ToDisplay` maps the raw linear RMS onto a −60..0 dBFS display scale, because real speech sits around −40..−20 dBFS (0.01..0.1 linear) which would leave a flat scale dead (`AudioLevel` is fed by the audio mixer once capture lands, 0 with no input) and 0 while muted. While the volume slider is being dragged the bar previews the slider position (`SetVolumeAdjusting`, from `PreviewMouseLeftButtonDown/Up` + `LostMouseCapture` handlers) so the creator sees where they're setting it; on release it returns to the live level — with no input it bounces back to 0, exactly as it does today. Clicking the meter does nothing; **clicking the MIC label opens the mic picker** (`OpenMicPickerCommand`), and the picked voice source name (`MicSourceName`) is shown left-justified INSIDE the meter bar (FontSize 10, ellipsized to the bar) — the fill runs at 75% opacity so the text and the ruler markings stay visible through it. Mute (`ToggleMicMuteCommand`/`MicMuted`) is a plain clickable speaker icon (`MicSpeaker_MouseLeftButtonUp` code-behind handler — not a Button, `Stretch="Uniform"` so the glyph is never clipped) that swaps to a red do-not-symbol (slashed speaker) when muted. **The slider and the speaker can never disagree:** `MicMuted` is read-only, derived from `MicVolume == 0` — sliding the volume off flips the speaker to muted (storing the prior level in `_volumeBeforeMute`), sliding it up from 0 clears the mute indicator (and the stored level); the speaker button just runs the volume to 0 or restores it (default 0.8 if unknown). Muting zeroes the meter; **unmuting flashes the meter to the restored position for ~300ms** (`BeginVolumeFlash`/`EndVolumeFlash` on a DispatcherTimer, cancelled if the slider is grabbed) before it returns to the live level. Line 2 of the footer holds everything else: stream stats (bitrate/fps/dropped/duration/health) on the left, quality dropdown + gear on the right. The slider is a slim dimensional style in `Themes/Controls.xaml` (gradient track, beveled green fill on a 5px pill, gloss-sphere thumb with drop shadow — deliberately NOT flat). No device pickers (never show device names — no "install a device you didn't know existed"), no filter stacks, no monitoring, no routing — OBS's confusion (dynamic mixer, unintuitive names, four required filters) is deliberately absent. A production-ready mic chain (high-pass → noise gate → compressor) will be applied invisibly in the mixer, unconfigurable. Capture runs only while live (privacy indicator stays off otherwise). Capture pipeline = `IAudioSource` seam + NAudio `WasapiCapture`/`WasapiLoopbackCapture` + `AudioMixer` (pending — the UI is in place now). **Mic mute icon (2026-08-13):** a second 16px clickable glyph — a microphone, red + slash when muted — sits **between the meter and the speaker** (both mutes adjacent, spacing between the icons) and reuses the same `MicSpeaker_MouseLeftButtonUp` → `ToggleMicMuteCommand` handler. **REC sign (2026-08-13):** the top-center indicator is an **always-visible REC chip** (`RecDotBrush`/`RecTextBrush`/`RecDotOpacity`/`IsLivePrivate`) — dark gray dot + dim "REC" offline, bright red (#e94560) while live, darker red (#8f1f1f) when live with a **private** stream (driven by the dialog's chosen `StreamVisibility`; the stream service still forces public — see TASK 4 ship step 7); the dot pulses while live, and the elapsed timer shows only when live. The connected account's avatar/name shows in the top bar next to Start Stream (`AccountAvatarUrl`/`AccountDisplayName` via `SyncConnectedAccount`), so the creator always sees WHICH account will go live. - ViewModels are constructed in XAML (`` as DataContext) - Services are currently instantiated in MainViewModel's constructor — no DI container yet -- Layout persists to SQLite (`Microsoft.Data.Sqlite`); scenes/sources/asset bytes stored in the DB, asset identity is a SHA-256 content hash (1:M reuse, no file paths — assets are always available) +- Layout persists to SQLite (`Microsoft.Data.Sqlite`); scenes/sources/asset bytes stored in the DB, asset identity is a SHA-256 content hash (1:M reuse, no file paths — assets are always available). Loaded sources always derive `IsBackground` from `Type` (OR'd with the persisted column, so legacy DisplayCapture backdrops keep their flag) — pre-derivation rows with `IsBackground=0` heal on load - **Five-scene catalog (`Models/SceneCatalog.cs`):** the product is exactly Starting/Live/BRB/Chat/Ending — work with less, never more (the escape hatch for "more" is OBS). Scenes are matched **by name** (`SceneCatalog.Is`, case-insensitive trim). Empty DBs seed all five; the scenes-header "+" (`ShowAddScene`/`MissingScenes` on `MainViewModel`) only appears while ≥1 canonical scene is missing and its menu lists only the missing ones, re-adding them by name (`AddSceneCommand`). Renaming a canonical scene makes it missing again; `AddScene` rejects non-canonical names. - Theming: all custom styles live in `Themes/Controls.xaml`, merged in `App.xaml` — never duplicate styles per-window (dialog duplicates were consolidated into this dictionary) - Resolution tiers (bottom bar): 1080p60@8 (default) → 1080p30@8 → 720p60@6 → 720p30@6 → **Vertical 1080p60@8 (9:16, 1080×1920)**. The composition master frame is **always 1920×1080** — a tier is an output rect + target resolution over that master, so source geometry is never rewritten (no rounding drift). 16:9 tiers use the full frame; the vertical tier uses a centered **607×1080** window and the preview dims the cropped side strips at 55% black with an accent outline (semi-crop — the cut area stays visible). A resolution badge in the preview corner shows the active tier; the bottom bar shows bitrate/FPS. A **tooltip** explains finding upload bandwidth — an in-app speed test was deliberately dropped (unreliable). The future encoder crops the master to the rect and scales to the tier's Width×Height diff --git a/ytLive.Tests/LayoutStorePersistenceTests.cs b/ytLive.Tests/LayoutStorePersistenceTests.cs index 02676e2..3e3190f 100644 --- a/ytLive.Tests/LayoutStorePersistenceTests.cs +++ b/ytLive.Tests/LayoutStorePersistenceTests.cs @@ -120,6 +120,48 @@ public class LayoutStorePersistenceTests } } + // Rows saved before the Type setter derived IsBackground carry IsBackground=0 + // for Type='Background', which used to load false — hiding the Background + // properties panel in BRB/Chat/Ending. Load must derive from Type while + // keeping the persisted bit for legacy DisplayCapture backdrops. + [Fact] + public void Legacy_IsBackground_Zero_Rows_Derive_From_Type_On_Load() + { + var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db"); + try + { + using (var store = new LayoutStore(path)) + { + var scene = new Scene { Name = "BRB", HasBackground = true }; + scene.Elements.Add(new Source { Name = "Game Capture", Type = SourceType.DisplayCapture, IsBackground = true }); + scene.Elements.Add(new Source { Name = "Background", Type = SourceType.Background }); + scene.Elements.Add(new Source { Name = "Logo", Type = SourceType.Image }); + store.Save(new[] { scene }, null, null); + + using var conn = new SqliteConnection($"Data Source={path}"); + conn.Open(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "UPDATE Source SET IsBackground = 0 WHERE Name IN ('Background', 'Logo');"; + cmd.ExecuteNonQuery(); + } + + using var reopened = new LayoutStore(path); + var reloaded = reopened.Load(); + var sources = Assert.Single(reloaded).Elements.OfType().ToList(); + + Assert.True(sources.Single(s => s.Type == SourceType.Background).IsBackground, + "A legacy IsBackground=0 row with Type=Background must derive the flag on load"); + Assert.True(sources.Single(s => s.Type == SourceType.DisplayCapture).IsBackground, + "The persisted bit must survive for legacy DisplayCapture backdrops"); + Assert.False(sources.Single(s => s.Type == SourceType.Image).IsBackground); + } + finally + { + SqliteConnection.ClearAllPools(); + try { File.Delete(path); } catch { /* best-effort cleanup */ } + } + } + // Per-scene background switch (schema v6): a scene whose background was turned // off must stay off across a save + reload. [Fact]