Commit Graph

8 Commits

Author SHA1 Message Date
gramps ecb329e578 fix(ui): drawers close on any click outside the rail — TEST was the missing third
Creator: 'when the test slide-out is active, then any click off the div should
close the div — this behaviour applies to all tabs, not just TEST.'

MainWindow.Window_PreviewMouseLeftButtonDown already closed the Stream Settings
and YPP drawers on any click outside TextPullOutHost, but its close list skipped
TestSession — so the TEST drawer never dismissed on an outside click.

Fix: run TestSession.CloseDrawerCommand in the same outside-click branch.
All three drawers share the rail host, so the containment check is unchanged.

Good Dog: ONE integration test — the existing TestStream window section now
raises a window-root PreviewMouseLeftButtonDown (source = window => outside the
rail; deterministic, no OS mouse) and asserts the TEST and Stream Settings
drawers both close. Gate: clean build 0 warnings, full vstest 318/318.
2026-09-25 08:18:25 -07:00
gramps cb750660c3 fix(ypp): refresh 403'd on every real call — drop the auditDetails part (needs a partner scope)
Creator: 'when I attempt to refresh my YPP page, I get an error about not being
able to reach YouTube. Seriously?' Real log: channels.list failed (403) x3.

Root cause #1 (the 403): channels.list?mine=true&part=statistics,auditDetails,
contentDetails returns 403 insufficientPermissions when the token lacks the
youtubepartner-channel-audit scope — which the auditDetails part ALONE requires,
per the docs ('A request that retrieves the auditDetails part ... must provide an
authorization token that contains the youtubepartner-channel-audit scope'). That
scope is MCN partner tooling with a 2-week token-revocation rule; the app must not
hold it. TASK-39's 'current scopes suffice, no re-consent' slice-1 claim was wrong
for this part; mock-fake tests never touched the real API, so it shipped green and
403'd every refresh since 2026-09-22.
https://developers.google.com/youtube/v3/docs/channels/list

Fix: part=statistics,contentDetails only; standing flags removed from
ChannelStatsService -> YppStatSnapshot surface -> YppTrackerViewModel -> drawer,
replaced by an honest deep-link row ('Channel standing isn't exposed to YouTube
apps — check the Earn page'). YppSnapshot standing columns stay (schema-stable,
always false). channels.list failures now log the response BODY — the bare code
could not name insufficientPermissions, which is what made this undiagnosable.

Root cause #2 (found by the new Good Dog, masked by the 403): statistics come back
as JSON STRINGS ('350'); raw GetInt64() throws. Tolerant ReadInt64 (ValueKind-first;
JsonElement.TryGetInt64 THROWS on strings — type-in, not try-type).

Good Dog: ChannelStatsServiceTests.CaptureCurrent_RequestsNoAuditDetails_AndStillParsesTheSnapshot
(URL asserts no auditDetails + snapshot parses); YppPullOutTests fixture updated.
Recipes for both 403/scope and statistics-strings entered in MyMistakes.md.

Also shipped in the same commit (shared PreviewPane.xaml + ai.md): the audio-sync
status dot removal from the #77 feedback round (creator: 'what is the point of the
status light? Lose it') — IntToSyncBrushConverter deleted with it.

verify.sh gate: 0 warnings, 316/316 pass, scope-check clean.
2026-09-23 16:45:56 -07:00
gramps bb5dcb4ba2 feat(stream): TASK 41 Test Stream mode — private test broadcast + TEST drawer
Test button next to Start runs the real private-only go-live pipeline as a
session variant (IsTestStream): BeginTestStream -> extracted
StartStreamingSession(alsoRecord:false) shared with the dialog path — no Go
Live dialog, no 'last live' stamp, no recording. Gold top bar + TEST badge +
'End Test' button face; TEST drawer (third right-rail pull-out, three-way
exclusivity) auto-opens once the liveChatId resolves.

Chat tooling: Mock Chat Input sends a REAL liveChat/messages.insert
(YouTubeStreamService.InsertChatMessageAsync) that round-trips the real ~2s
poll and renders through the live overlay; simulated Subscriber/New
Member/Super Chat events inject through the poller's MessageReceived seam
(YouTubeChatService.InjectSimulatedMessage, ChatMessage.IsSimulated) — the
insert API only creates textMessageEvent, so non-text events are local-only
by design (ref: developers.google.com/youtube/v3/live/docs/liveChatMessages/insert).
Scopes youtube + youtube.force-ssl already cover insert; no re-consent.

Good Dog: ytLive.Tests/TestStreamTests.cs — coordinator protocol with fake
HTTP + real-window drawer exclusivity/gate. 0 warnings; 314/314 pass (clean
run; intermittent host abort is the pre-existing WinRT-webcam flake).

Docs ride in the same change: TASKS.md row 41, TASKS/task-41-test-stream-mode.md,
ai.md (YouTube Live API constraints), HANDOFF rewrite.
2026-09-22 09:31:00 -07:00
gramps a9d5f29084 feat(ypp): YPP journey tracker slice 1 — the YPP pull-out below Stream Settings (TASK 39, Good Dog ONE test)
Current-OAuth-scope data, no re-consent: subscriber bars for both tiers (500 fan-funding /
1,000 ad-revenue; the 2027-02-01 doubling is versioned date-aware data in YppThresholds),
3-uploads/90d, live standing from channels.list auditDetails, self-reported 2FA/AdSense
checkboxes + deep links (Google 2-Step / youtube.com/earn), and the "what counts" education.
Every refresh appends a YppSnapshot (SQLite v10) so the analytics slice has history from day one.

- ChannelStatsService (channels.list statistics,auditDetails,contentDetails + playlistItems.list,
  virtual CaptureCurrentAsync = test seam); YppTrackerViewModel mirrors LiveBroadcastFormViewModel.
- MainViewModel.Ypp.cs: Ypp property, ChannelStatsFactoryOverride seam, one-open-at-a-time drawer
  exclusivity (both directions); account sign-in/out/restore hooks in Account.cs.
- PreviewPane.xaml: TextPullOutHost is now a drawer bank [broadcast][ypp][stacked white tabs];
  outside-click collapse covers both drawers. MainWindow.xaml.cs updated.
- Wait - watchers: the whole feature already reviewed against YouTube's own docs (YPP Earn tab +
  API availability) before building — see TASKS/task-39-ypp-journey-tracker.md.
- ONE integration test: YppPullOutTests (snapshot capture/persist + checklist round-trip +
  drawer exclusivity). 313/313 pass, 0 warnings.
- Memory in same commit: TASKS.md row, ai.md Journey-tracker bullet (roadmap→reality), HANDOFF.md.

Slice 2 (Analytics yt-analytics.readonly re-consent → watch-hours/Shorts + velocity/ETA +
sparkline) is queued behind this, not merged.
2026-09-22 08:07:54 -07:00
gramps 4e0915a36e feat(backdrop): Capture Window… in-app window pin (TASK 38, Good Dog ONE test)
Click-to-pin an open window as the full-bleed Live backdrop, via the existing
window:<hwnd> capture path (same render layer as game/desktop). Session-scoped:
window:/picker: keys heal to auto on reload (HWND-recycle hazard); pin wins
while the window enumerates, then auto-fallbacks (game → desktop → static); a
dead window's capture session heals to auto, no dead ends.

Fix: ScreenCaptureSourceFactory.Resolve hex-parsed "window:0x<Hwnd>" WITHOUT
stripping the 0x prefix — NumberStyles.HexNumber rejects it, so every pin
resolved null ("No capture target for this key") and the heal silently rolled
back. Same flaw in IsAliveWindowPin (pin-wins guard was dead). Both fixed.

Commit also carries an out-of-scope prerequisite: PillRadioTests.cs:105 had a
committed stray token ("...PrimaryStartButtonLabel soil;", CS1003) blocking the
whole test-project compile — token removed.

Plus the previous session's uncommitted pricing docs (one-time $29/$49) that
share TASKS.md/ai.md/HANDOFF.md.
2026-09-22 07:00:47 -07:00
gramps b22d08eca6 feat: signed A/V sync offset (−500..+500), negative advances by eating live stream head (OBS eat-head semantics)
Positive offsets still delay the whole mix via the delay line (lip-sync fix);
negative offsets now ARM once at StartLive and drop |N| ms off the pipe's write
head so audio events land earlier when audio runs BEHIND video. Slider relabeled
AUDIO SYNC, Min −500, locked while live/recording (IsEditMode). LayoutStore and
VM clamp to −500..500.

OBS reference for eat-the-head negative sync: https://obsproject.com/kb/obs-studio/buffering-time (negative sync values pull audio earlier by discarding buffered player audio).

Test: StartLive_NegativeOffset_AdvancesAudio_ByDroppingTheStreamHead (6x0.9 head
must be eaten before 0.2 bed reaches the wire).
2026-09-14 12:30:44 -07:00
gramps e8ff4df05f TASK 22: global audio sync offset (positive delay at the mixer out)
Adds AudioSyncOffsetMs (0..500ms, default 0) that delays the whole
interleaved-stereo mix so audio lands on the video when it runs ahead —
OBS's documented lip-sync fix. Positive-only: advancing audio needs a
video-side delay and is out of the audio layer's scope.

- Services/Audio/AudioSyncDelay.cs: pure delay line, flushed on Configure
- AudioMixer: Func<int> syncOffsetMs seam + delay applied post-limiter
- LayoutStore.Settings + MainViewModel.Audio/VM: load/save + binding
- PreviewPane mic bar: SYNC slider + status dot (IntToSyncBrushConverter)
- AudioSyncDelayTests: identity, negative/beyond-500 clamps, 10ms→960 samples

Reference (external scan): https://obs-versions.com/blog/how-to-fix-audio-delay-on-obs
(audio ahead => positive delay). Verified: build 0 warnings; 3/3 delay tests pass.
2026-08-31 18:30:03 -07:00
gramps c5e3466df0 refactor: extract PreviewPane out of MainWindow.xaml — Phase 2, Commit 15
PreviewPane.xaml(.cs) takes the center preview (Row 1 Col 1): the 1920x1080
canvas (OverlayCanvas/SelectionOverlay/BrandFlashLayer/SocialBarElement),
the text pull-out drawer, and the live-controls row (TRAX / game / mic meters,
volume sliders, speaker toggles, socials button) inside the preserved
glow/background Border.

Code-behind moved into the control (drive by DataContext==MainViewModel):
preview drag/select/resize/hit-test (Preview_Mouse{Down,Move,Up},
EndPreviewDrag, HitHandle, HitElement), webcam context-menu handlers
(WebcamMenu_ChangeWebcam/SetBorderAnimation/StopBorderAnimation/
FindBorderShape/CreateBorderAnimation/AddDoubleAnimation/SetBorderColor/
HideInScene/Remove), social-bar glow (SocialBar_MouseLeftButtonDown,
SocialsButton_Mouse{Enter,Leave}, SocialGlowTick), audio controls
(VolumeSlider_*, GameVolumeSlider_*, MicSpeaker_*, GameSpeaker_*,
SetSliderValueFromClick, FindParent), and TraxButton_*.

Window keeps the left source/property/chat panel + thumbnail strip + overlay
host, and routes the selection overlay and preview hit-testing through the
control's public surface (_previewPane.UpdateSelectionOverlay(bool),
IsClickInsidePreview/Drawer, PreviewPane.IsDraggableElement). BackgroundMenu_*
stayed in the window because the left-panel source context menu uses them
(the preview's own desktop/refresh menu binds RefreshCaptureCommand instead).

Preserved the outer preview Border (background/glow/margin) that was initially
dropped in the cut — caught and restored before commit. Pruned now-unused usings
(System.Net.Http, Media.Effects, Media.Animation, Media.Imaging, Shapes,
Threading) from MainWindow.xaml.cs.

Zero behavior change; build 0 warnings; 246 pass, only the 2 known failures.
Docs: ViewModels/index.md Phase 2 tracker updated in the same commit.
2026-08-30 19:38:22 -07:00