elements: match IconButton style for revert (red X Path) and check (green Path)
This commit is contained in:
+41
-289
@@ -1,297 +1,49 @@
|
||||
# HANDOFF — session state
|
||||
# Handoff — 2026-08-17
|
||||
|
||||
> Current operational state, read right after `TASKS.md`. Trust this file as the
|
||||
> truth of what is in flight — do not re-derive from git/fs unless it points at
|
||||
> a problem. Conventions: [`schema.md`](schema.md). Rewrite this file at session
|
||||
> end, compaction, or any interruption.
|
||||
## State
|
||||
- **Branch:** `main` — clean, everything pushed
|
||||
- **Latest commit:** `b9173f5` (layout: cap side panel max widths)
|
||||
- **Tests:** 207 passing, 0 warnings
|
||||
- **App version:** 0.1.0
|
||||
|
||||
## Session state (last updated: 2026-08-17, creator feedback batch complete)
|
||||
## What shipped today
|
||||
|
||||
- **Branch:** `main`, tracking `origin/main`. Working tree: **TASK 14 complete** — all creator feedback items shipped. Build **0 warnings**, **207 tests passing**. Ready to commit.
|
||||
- **TASK 14 — CREATOR FEEDBACK BATCH — SHIPPED.** Both branches complete: TRAX volume/tooltip, mic meter boost, click-to-position sliders, backdrop visibility + rename, Elements panel (webcam border config, Countdown source, Web source).
|
||||
- **TASK 9 — REUSABLE STREAM + HEALTH BANNER — SHIPPED 2026-08-16 (items 1–3).**
|
||||
- **Items 1–2 (reusable stream):** `_rtmpUrlProvider` now yields a real RTMP URL.
|
||||
`YouTubeStreamService` gains `GetOrCreateReusableStreamAsync` (lists `liveStreams?mine=true`,
|
||||
reuses the existing `cdn.isReusable` stream, inserts once on first use with
|
||||
`resolution=variable`/`frameRate=variable`); `CreateBroadcast(..., streamId)` binds at insert via
|
||||
`boundStreamId` + the one-click v3 flags (`enableMonitorStream=false`, `latencyPreference=low`).
|
||||
Cached via `LayoutStore` Settings (`SaveReusableStream`/`LoadReusableStream`). **Go-live order
|
||||
changed** (the pump reads the URL once at start — `FramePump.StartAsync`): `BeginGoLive` →
|
||||
`PrepareAndStartLiveAsync` = ensure stream → create+bind broadcast → THEN start the pump.
|
||||
- **Item 3 (report-by-exception health):** `GetStreamHealthAsync(streamId)` polls
|
||||
`liveStreams?part=status`; the pure `Services/StreamHealthReporter.BannerFor` decides (null text on
|
||||
good/ok/noData/info-only; warning/error issue → its type text, error beats warning). The VM polls
|
||||
every **30s while live** (`_healthPollTimer`, first poll right after go-live, stopped on End/Error
|
||||
via `UpdateLiveVisuals`; failures log-only). UI = full-width banner strip under the top bar,
|
||||
`HealthIssueBanner` + `HealthIssueBackground` (amber `#b8860b` warning / dark red `#8f1f1f` error),
|
||||
hidden by `NotNullToVis`; cleared in `ResetHealth`.
|
||||
- **Tests (8 new this branch, 207 total, 0 warnings):** 3 service units (parse, good→no issues,
|
||||
no-session→null) + 4 `StreamHealthReporterTests` + the ONE integration test
|
||||
`GetStreamHealthAsync_Report_By_Exception_Banner_Only_On_Warning_Or_Error` (real service JSON parse
|
||||
→ real reporter: good → no banner, error issue → banner text + error color).
|
||||
- **CONFIDENTIAL files created (gitignored):** `MONETIZATION.md` (pricing, billing research, unlock
|
||||
mechanics) and `MARCOM.md` (launch marketing strategy, positioning, platform strategy). These are
|
||||
NOT committed to the public repo. `Helpers/OAuthCredentials.cs` was already gitignored.
|
||||
- **TASK 9 items 6-7 scoped (not built):** visibility unlock (remove temporary "always Private" enforcement)
|
||||
and full broadcast form (Core + Advanced tabs with all YouTube API-supported fields). These are the
|
||||
next technical tasks after live chat (item 4) and error handling (item 5).
|
||||
- **TASK 10 trimmed** to technical scope only (billing decision, unlock mechanism, bug report, alerts gating).
|
||||
Business details moved to `MONETIZATION.md`.
|
||||
- **TASK 13 added** — marcom/launch kit. Business details moved to `MARCOM.md`. Queued after all v1 features.
|
||||
- **Backlog updated** — removed v0.3 (stream scheduling, not needed for casual streamers). Multi-destination
|
||||
restreaming noted as "congrats, you're ready for OBS" moment.
|
||||
- **Resume point (next branch):** **TASK 9 item 4 — live chat** (`liveChat/messages` poll, right-panel
|
||||
render, Super Chat + membership badges). Then item 5 (error handling), item 6 (visibility unlock),
|
||||
item 7 (full broadcast form), then TASK 10 (billing + unlock + support).
|
||||
- **TASK 12 — MASTER LIMITER — COMMITTED + PUSHED 2026-08-15.** Queued from the TRAX discussion: the
|
||||
live mix summed mic + loopback with no ceiling, so hot gains could pass 0 dBFS and clip the AAC
|
||||
encode. New pure `Services/Audio/MasterLimiter.cs` (−1 dBFS ceiling, instant attack per frame,
|
||||
smoothed release) applied at the end of `AudioMixer.FillAndMix`. ONE integration test
|
||||
(`MasterLimiter_CapsTheLiveMix_OnThePipe`). The review also confirmed file size needs no guard
|
||||
(`MediaFoundationReader` streams) and the music **0.20 cap is already relative by construction**
|
||||
(music rides the same loopback gain as the game → always exactly 20% of the desktop volume). Build
|
||||
**0 warnings**, full suite green.
|
||||
- **GAME AUDIO BAR ALWAYS VISIBLE — COMMITTED + PUSHED 2026-08-15.** The TASK 4 show/hide gating was a
|
||||
UX bug: the desktop/game meter kept vanishing whenever no full-screen game with sound was up (or no
|
||||
TRAX music played). The whole `IGameAudioDetector`/`GameAudioDetector`/`GameAudioHysteresis` stack +
|
||||
the VM's 250ms poll timer + its two test files were **deleted**; the bar is now permanently overlaid
|
||||
at the bottom of the preview. Build **0 warnings**, full suite green.
|
||||
- **TASK 11 — POST-PAUSE POLISH BATCH — SHIPPED + COMMITTED + PUSHED 2026-08-15.** All 8 creator
|
||||
review issues fixed in one branch (details below). Build **0 warnings**, full suite **197 passing**
|
||||
(ONE integration test for that branch: `AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback`).
|
||||
- **AUDIO MILESTONE (TASK 8) — SHIPPED + COMMITTED + PUSHED.** Real stream audio + voice filters +
|
||||
auto-duck + free TRAX background music. Commits `6d71ace` (milestone) + `f2f6401` (secrets cleanup),
|
||||
force-pushed (`725f5a7...f2f6401`).
|
||||
- **MONETIZATION LOCKED (2026-08-14, creator) — committed `86fcd86`.** One paid line = **annual
|
||||
subscription**: early access **$49.99/yr** → **$99/yr list at GA**, **grandfather-while-subscribed**,
|
||||
lapse → list on renewal. Free tier unchanged (branding flash = the billboard + no Alerts; Alerts is
|
||||
the paid feature). **Billing NOT itch-locked**; candidates Gumroad (native affiliates =
|
||||
tiebreaker) / Lemon Squeezy. Support = in-app bug-report → git issues. Full policy in `ai.md` →
|
||||
Monetization; scoped plan in `TASKS.md` **TASK 10**.
|
||||
- **Secrets scrubbed from git history.** The DO token + passwords were purged via `git filter-branch`
|
||||
+ `git gc --prune=now --aggressive` (all-refs scan = 0 hits); values still exist in chat — keep
|
||||
treating as **compromised**; rotate the DO API token.
|
||||
- **Shipped, all pushed:** TASK 9 items 1–3 (reusable stream + health banner, 2026-08-16), TASK 12
|
||||
master limiter, TASK 11 polish batch, creator-hub About (`3d92bd0`), TASK 7 (meter +10 dB),
|
||||
TASK 4 ship step 7 (one-click go-live + private-only), TASK 8 audio milestone (`6d71ace`), secrets
|
||||
cleanup (`f2f6401`), monetization docs (`86fcd86`).
|
||||
### Branding (`2d5d2e9`)
|
||||
- Replaced all llama logo assets with new ytLlive branded icons
|
||||
- `tyllive-icon.png` → window icons + About overlay
|
||||
- `tyllive-icon.ico` → Windows app icon (multi-size)
|
||||
- `ytLlive-logo.png` → full branded logo with tagline
|
||||
|
||||
## TASK 9 — reusable stream + health banner 2026-08-16 (what changed, items 1–3)
|
||||
### Creator feedback batch (`28765a0`) — 9 items
|
||||
1. About box bg → `#0A0C1A` (matches icon bg)
|
||||
2. Elements panel collapses when nothing selected / scene changes
|
||||
3. OpacityChip moved from preview overlay into Elements panel
|
||||
4. Color picker fix: `is not SceneElement` (was `Source`, silently blocked `WebcamSceneConfig`)
|
||||
5. Red left accent bar on selected items in Scene/Source lists
|
||||
6. ✓ character → Path checkmark icon
|
||||
7. Removed ToolTip from green check buttons
|
||||
8. Created `bugs.md` with first entry (iTunes audio on mic meter)
|
||||
9. Full logo in README + About box uses `ytLlive-logo.png`
|
||||
|
||||
The recorded resume point: the "last blocker" was that go-live ran the visual flow but never pushed —
|
||||
`_rtmpUrlProvider` returned null, so `FramePump.StartAsync` skipped the encoder entirely.
|
||||
### Layout (`32f8923` → `b9173f5`)
|
||||
- Proportional column sizing: left/right at `1*`, preview at `2*` (50% growth rate)
|
||||
- Side panels capped: left 360px max, right 400px max
|
||||
|
||||
- **`Services/YouTubeStreamService.cs`:** new `GetOrCreateReusableStreamAsync()` lists
|
||||
`liveStreams?mine=true` and reuses the existing `cdn.isReusable` stream, inserting once per channel
|
||||
only on first use (`cdn.resolution=variable`, `cdn.frameRate=variable`, `isReusable=true`) and
|
||||
returning a `ReusableStream(Id, IngestionAddress, StreamName)` record (`RtmpUrl` =
|
||||
`ingestionAddress/streamName`). `CreateBroadcast` takes an optional `streamId` and binds at insert
|
||||
via `contentDetails.boundStreamId` (no second bind round-trip) and now always sends the full
|
||||
one-click v3 flag set (`enableAutoStart/Stop`, `enableMonitorStream=false`, `latencyPreference=low`).
|
||||
The old per-broadcast `BindStream` (throwaway 1080p/60fps stream + `contentDetails.streamId`) is
|
||||
**gone**.
|
||||
- **`Services/LayoutStore.cs`:** `SaveReusableStream`/`LoadReusableStream` — the Settings key/value
|
||||
table caches the stream id/address/name so the pump has its RTMP URL at startup, no round-trip.
|
||||
- **`ViewModels/MainViewModel.cs`:** `_rtmpUrlProvider` returns `_reusableStreamUrl` (loaded from the
|
||||
cache in the ctor, set fresh on go-live). `BeginGoLive` → `PrepareAndStartLiveAsync`: ensure the
|
||||
stream → cache it → create the broadcast bound to it → **then** `_framePump.StartAsync()`. The
|
||||
ordering matters because the pump reads `_encoderOptions()` once at startup.
|
||||
- **Tests (6 new, 199 total, 0 warnings):** `GetOrCreateReusableStreamAsync_Reuses_Existing_Reusable_Stream`
|
||||
(list path, no POST), `..._Creates_When_None_Exists` (insert path with variable/isReusable),
|
||||
`CreateBroadcast_With_StreamId_Binds_Reusable_Stream_At_Insert` (boundStreamId + new v3 flags),
|
||||
`..._Without_Session_Returns_Null`, `ReusableStream_Cache_RoundTrips`, and the ONE integration test
|
||||
`FramePumpTests.ReusableStream_Url_From_Service_Feeds_Encoder_Startup` (real service + real pump +
|
||||
hermetic HTTP: the reusable stream's URL lands in `EncoderOptions.RtmpUrl` and the encoder starts).
|
||||
## New files
|
||||
- `Helpers/NullToVisibilityConverter.cs` — shows element when null
|
||||
- `bugs.md` — bug tracker
|
||||
- `Assets/tyllive-icon.png`, `Assets/tyllive-icon.ico`, `Assets/ytLlive-logo.png`
|
||||
|
||||
**Out of scope this branch (next branches):** TASK 9 item 4 live chat, item 5 the YouTube error-code
|
||||
mappings, and the design's bottom-strip YouTube logo/green-red dot (clickable → dialog).
|
||||
## Modified files
|
||||
- `Models/SceneElement.cs` — added `IsDraggable` virtual property
|
||||
- `Models/Source.cs` — `IsDraggable => Type == SourceType.Image`
|
||||
- `Models/WebcamSceneConfig.cs` — `IsDraggable => true`
|
||||
- `ViewModels/MainViewModel.cs` — `IsElementsPanelEmpty` property
|
||||
- `Themes/Controls.xaml` — accent bar on selected ListBoxItem
|
||||
- `MainWindow.xaml` — elements restructure, layout columns, About bg
|
||||
- `MainWindow.xaml.cs` — OpacityChip removal, color picker fix
|
||||
- `README.md` — centered logo
|
||||
|
||||
### Item 3 — report-by-exception health banner (same session, second branch-worth of scope)
|
||||
|
||||
- **`Services/YouTubeStreamService.cs`:** `GetStreamHealthAsync(streamId)` polls
|
||||
`liveStreams?part=status&id={id}` → `StreamHealth` with parsed `healthStatus` (`good|ok|bad|noData`)
|
||||
+ `configurationIssues[]` (severity `info|warning|error` + type). The old `GetStreamHealth(broadcastId)`
|
||||
(wrong endpoint — `liveBroadcasts.lifeCycleStatus` — zero callers) is **gone**.
|
||||
- **`Services/StreamHealthReporter.cs` (new, pure):** `BannerFor(issues)` → `HealthIssueReport(Text?, IsError)` —
|
||||
null text on good/ok/noData/info-only; the first warning/error issue produces its type text
|
||||
(comma-joined, blank types dropped); error beats warning for color.
|
||||
- **`ViewModels/MainViewModel.cs`:** `_reusableStream` (the record, not just the URL) is stashed by
|
||||
`PrepareAndStartLiveAsync` + loaded from the cache in the ctor; a 30s `DispatcherTimer`
|
||||
(`_healthPollTimer`) polls while live — first poll fires right after the pump starts, the tick
|
||||
handler fire-and-forgets `PollHealthAsync()` (fully try/caught, failures log-only), and
|
||||
`UpdateLiveVisuals`' offline/error branch stops the timer. `ApplyHealthIssue` sets
|
||||
`HealthIssueBanner` + `HealthIssueBackground` from the reporter; `ResetHealth` clears both.
|
||||
- **`MainWindow.xaml`:** a full-width banner strip in its own window-grid row (below the top bar,
|
||||
above the content) bound via `NotNullToVis` to `HealthIssueBanner`, background to
|
||||
`HealthIssueBackground` (amber `#b8860b` warning / dark red `#8f1f1f` error); rows shifted
|
||||
(content → row 2, footer → row 3).
|
||||
- **Tests (8 new this branch, 207 total, 0 warnings):** 3 service units + 4 `StreamHealthReporterTests`
|
||||
+ the ONE integration test `GetStreamHealthAsync_Report_By_Exception_Banner_Only_On_Warning_Or_Error`
|
||||
(real service JSON parse → real reporter: good → no banner, error issue → banner text + error color).
|
||||
|
||||
## TASK 11 — the 8-issue polish batch — SHIPPED 2026-08-15 (what changed)
|
||||
|
||||
The creator's review of the TASK 8 build, all fixed in one branch (full record in `TASKS.md` TASK 11).
|
||||
|
||||
1. ✅ **Desktop/game audio volume slider had no effect** — the `AudioMixer` gain seams defaulted to
|
||||
unity (the VM never passed them): the slider/mute were decorative, stream AND local. Fixed with a
|
||||
new **`AudioGainProvider`** (`Services/Audio/`) wired into the mixer at construction, read live
|
||||
each mix tick.
|
||||
2. ✅ **Desktop/game mute had no effect** — same wiring; `GameMuted` now zeroes the loopback on the
|
||||
stream AND silences the music locally via new `MusicPlayer.LocalGain` (scaled by the game bar on
|
||||
every volume/mute change + on TRAX load) — the creator hears the control work in the headphones.
|
||||
3. ✅ **TRAX played once then stopped** — `OnPlaybackStopped` only looped when `Position >= Length`
|
||||
(unreliable for `MediaFoundationReader`); now any clean stop rewinds + replays. Dropped the unused
|
||||
`using System.Runtime.InteropServices;` too.
|
||||
4. ✅ **TRAX/MIC buttons swapped** — footer mic cluster is now MIC + meter + mute + volume (TRAX is out
|
||||
of the mic cluster entirely).
|
||||
5. ✅ **Mic source persists across restarts** — `LayoutStore` gained a `Settings` key/value table
|
||||
(`SaveMicSourceName`/`LoadMicSourceName`); the VM saves on pick and restores before the mixer's
|
||||
first `Start` → same already-vetted device reconnects green on restart, missing → yellow.
|
||||
6. ✅ **TRAX moved right of Socials** — both centered under the scenes/sources listboxes (footer line
|
||||
1, left cluster).
|
||||
7. ✅ **Backdrop icons shifted right** — new `HiddenBoolToVisibilityConverter` keeps the trash column
|
||||
reserved (`Hidden`, not `Collapsed`) so edit/eye stay in fixed columns for every source row.
|
||||
8. ✅ **No separation between scenes and sources** — a 1px hairline with top/bottom padding now sits
|
||||
between the two left-panel listboxes.
|
||||
|
||||
**THE ONE integration test:** `Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback` — real mixer +
|
||||
`AudioGainProvider` gains through the pipe harness: scaled loopback audible at 0.5, silence after mute.
|
||||
|
||||
## Game audio bar — always visible 2026-08-15 (what changed)
|
||||
|
||||
The creator reported the desktop/game sound meter "lost" — it was the TASK 4 show/hide gating
|
||||
(`_gameAudioBarActive || IsMusicPlaying`): the bar only rendered while a full-screen game produced
|
||||
sound or TRAX played, so it sat hidden during normal use. Fix = the bar is now **always visible**:
|
||||
|
||||
- **Deleted** `Services/IGameAudioDetector.cs`, `Services/GameAudioDetector.cs`,
|
||||
`Services/GameAudioHysteresis.cs`, `ytLive.Tests/GameAudioDetectorTests.cs`,
|
||||
`ytLive.Tests/GameAudioHysteresisTests.cs` — the stack's only output (`_gameAudioBarActive`) fed
|
||||
`IsGameAudioBarVisible`, which no longer exists.
|
||||
- **MainViewModel.cs:** removed `_gameAudioTimer` (250ms `DispatcherTimer`), `_gameAudioDetector`,
|
||||
`_gameAudioBarActive`, the constructor wiring, `OnGameAudioActiveChanged`, `OnGameAudioPollTick`,
|
||||
`IsGameAudioBarVisible`, and the `IsMusicPlaying` → visibility notification. Desktop audio is just
|
||||
automatic WASAPI loopback now.
|
||||
- **MainWindow.xaml:** dropped the `Visibility` binding on the preview-overlay game bar; it renders
|
||||
unconditionally (TRAX button + "Desktop Audio" label + meter + mute + volume unchanged).
|
||||
|
||||
## TASK 12 — master limiter 2026-08-15 (what changed)
|
||||
|
||||
Came out of the TRAX discussion (file-size guard? 20% cap? sound-event balance?). Verdict: two of the
|
||||
three instincts were already satisfied, one real gap existed:
|
||||
|
||||
- **No file-size guard needed** — `MediaFoundationReader` streams from disk; memory is flat (~a few MB)
|
||||
whatever the file size.
|
||||
- **The 0.20 music cap is relative by construction** — music rides the same loopback gain as the game,
|
||||
so music:game is always exactly 0.20:1 at any slider position; it cannot rise above 20% of the
|
||||
current desktop volume. (Per-channel hierarchy: voice on top via the ducker −12 dB, then game, then
|
||||
music at 20%.)
|
||||
- **The gap:** `FillAndMix` summed mic + loopback with no ceiling — hot gains could pass 0 dBFS and
|
||||
clip the AAC encode.
|
||||
- **Fix:** new pure `Services/Audio/MasterLimiter.cs` — **−1 dBFS ceiling** (`Ceiling = 0.891`),
|
||||
**instant attack per frame** (hot frames scaled exactly to the ceiling, no overshoot), **smoothed
|
||||
release** toward unity (no pumping); gain never exceeds 1. Applied at the end of `AudioMixer.FillAndMix`.
|
||||
- **ONE integration test:** `MasterLimiter_CapsTheLiveMix_OnThePipe` — real mixer + pipe harness, a
|
||||
0.95 loopback bed capped to exactly 0.891 on the wire while staying audible. Plus 3 unit tests for
|
||||
the pure math.
|
||||
|
||||
## TASK 8 audio milestone — SHIPPED 2026-08-14 (what changed)
|
||||
|
||||
The creator's feature review settled this as the single next branch ("all the audio issues done and
|
||||
tested — a huge milestone"). Final spec and full shipped-state records live in `TASKS.md` (TASK 8)
|
||||
and `ai.md` ("Live audio capture"). Highlights:
|
||||
|
||||
- **Real audio into the encoder.** `FfmpegArgs` now builds `-f f32le -ar 48000 -ac 2 -i \\.\pipe\ytllive_audio`
|
||||
+ explicit `-map 0:v -map 1:a` (replaces `anullsrc` silence). `MainViewModel.BeginGoLive` →
|
||||
`_audioMixer.StartLive(EncoderOptions.DefaultAudioPipeName)`; `StopStream` → `_audioMixer.StopLive()`
|
||||
before `_framePump.StopAsync()`.
|
||||
- **2-input mix (mic + loopback)**, honest gains: `micGain = MicVolume` (mute = 0),
|
||||
`loopbackGain = GameMuted ? 0 : GameAudioVolume` × duck. No third music channel.
|
||||
- **Voice chain on the mic** (TASK 8), before meter AND mix: bass 120 Hz +4 dB → treble 8 kHz +3 dB →
|
||||
gate (0.005 / hysteresis 0.5) → compressor (0.5, 4:1). Pure TDF2 DSP, per-sample, always on.
|
||||
- **Auto-duck:** mic RMS > 0.02 → loopback ×0.25 (−12 dB), attack 0.05 / release 0.005.
|
||||
- **TRAX (free BGM):** `MusicPlayer` = MediaFoundationReader → `VolumeWaveProvider16` at fixed **0.20** →
|
||||
`WaveOutEvent`. Plays to the default device → rides the loopback into the stream (ducked with game).
|
||||
Footer TRAX button (dot red/yellow/green + "TRAX"), left-click toggles/picks, right-click opens the
|
||||
picker (`OpenFileDialog`), tooltip shows the track name. Track persists via **schema v9** single-row
|
||||
`Music`. Sound-bar label "Game Audio Capture" → **"Desktop Audio"**;
|
||||
`IsGameAudioBarVisible = gameDetectorProducingSound || IsMusicPlaying`.
|
||||
- **Tests:** new `AudioPipelineTests.cs` (DSP/ring-buffer/ducker/resampler units + the ONE integration
|
||||
test reading real pipe bytes via `NamedPipeClientStream`); `FfmpegEncoderTests` + layout persistence
|
||||
updated for pipe args / Music roundtrip. Ring-buffer overwrite bug found by the unit test and fixed
|
||||
(head must NOT advance on eviction — the write itself advances it). Integration test pre-fills the
|
||||
ring buffers before `StartLive` so the first pipe tick already carries audio (deterministic — a
|
||||
start-of-stream silence race was seen and eliminated).
|
||||
|
||||
**Out of scope this branch:** IP webcam, chat box, alt-key crop, credits, bg removal, music-off-VOD
|
||||
track (YouTube mutes VODs with copyrighted music — future feature), `PremiumUrl` (TASK 10 seam).
|
||||
|
||||
- **Landmines:**
|
||||
- Never add another test that constructs `new App()` — use `RealAppHost.Run(...)` (shared STA host
|
||||
for the one WPF App per AppDomain; round-clip + source-naming tests).
|
||||
- Never set a local `Canvas.SetTop` on the social bar — a local value permanently
|
||||
overrides `{Binding SocialBarTop}` (the `ClearValue` lesson from 5.5).
|
||||
- `AudioMixer` meter `Push` is unconditional **by design now**: `OnMicSample`/
|
||||
`OnLoopbackSample` compute the level first, then raise the event — a
|
||||
`?.Invoke(meter.Push(...))` short-circuit skipped the meter update when
|
||||
nothing was subscribed (found by `RestartMic_ResetsLevel`, fixed).
|
||||
- `MicConnected` comes from the source `Started` event, raised right after
|
||||
`StartRecording()` succeeds — tests must `MarkStarted()` the fake source
|
||||
before asserting connection state.
|
||||
- The mic dot is red until a resource connects (see contract below) — green
|
||||
only after `Started`, yellow on `Failed`.
|
||||
- Zero mic devices at startup = red dot AND the mixer is never started, so
|
||||
loopback + the game bar can't run either (no capture at all) — acceptable.
|
||||
- The pump reads the active scene on a background thread while the UI can still
|
||||
edit it — a concurrent-mutation exception is contained (logged + `Failed` +
|
||||
the pump stops), not a crash.
|
||||
- `StopAsync` must stop the encoder (closes stdin) **before** awaiting the pump
|
||||
loop — closing stdin unblocks a write stuck on pipe backpressure; the reverse
|
||||
order deadlocks. Same rule for audio: `StopLive()` (pipe EOF) before the pump
|
||||
stop, so ffmpeg's two inputs end in order.
|
||||
- Tests never instantiate `MainViewModel` directly except via a real `MainWindow`
|
||||
on the `RealAppHost` STA thread (round-clip + naming), which never go live.
|
||||
- Sandbox can't reach outbound HTTPS — `HttpSocialValidator` stub-handler tests
|
||||
only, never the real instance.
|
||||
- **Mic status contract (creator's rule, verified — do NOT "fix"):** the status dot is
|
||||
**red until a mic resource is actually connected**. `MicStatus` starts `NotConnected`
|
||||
(red); it goes green ONLY when the mixer's `MicConnected` fires, which comes strictly
|
||||
from the mic source's `Started` event raised after `StartRecording()` succeeds. Zero
|
||||
devices at startup → stays red and the mixer is never started; capture failure → yellow.
|
||||
|
||||
- **Secret/DB/port facts live:** OAuth client id/secret in `Helpers/OAuthCredentials.cs`;
|
||||
OAuth session token in `Helpers/TokenStore.cs` (DPAPI → `%APPDATA%\ytLlive\ytLlive.auth`);
|
||||
layout DB `%APPDATA%\ytLlive\ytLlive.db` (**schema v9** — single-row `Music`; the
|
||||
`SocialEntry.Software` column is a column-presence migration like the others); OAuth callback
|
||||
`http://localhost:8765/oauth2/callback`; crash log `%APPDATA%\ytLlive\startup.log`.
|
||||
|
||||
## Server recovery (llamachile.tube / YunoHost / DO) — 2026-08-14
|
||||
|
||||
**Facts (hunted this session — do not re-hunt):**
|
||||
- Droplet **llamachile.tube**: DO droplet ID `473190301`, IP `143.244.176.131`, sfo3, 4GB/2vCPU/50GB.
|
||||
**SSH port = 2214** (matches git remote `ssh://llgit.llamachile.tube:2214/gramps/ytLlive.git`);
|
||||
22/2222/2200/etc all closed. SSH as `gramps` works with `~/.ssh/id_ed25519` (key auth, no password).
|
||||
- **Root is YunoHost-locked**: `PermitRootLogin no` in `/etc/ssh/sshd_config` (there's a conflicting
|
||||
cloud-init override section below it, but DO's "Reset root password" email does NOT work on this
|
||||
box — cloud-init `set-passwords` only ran once at install). Root is reachable via `sudo -i` or the
|
||||
DO web Recovery Console (Settings → Recovery console — VNC-based; the droplet page's Console button
|
||||
is SSH-based and fails with "all configured authentication methods failed" when no account has a
|
||||
working password).
|
||||
- **gramps is a YunoHost LDAP account** (local `/etc/passwd` entry has `*`, auth via pam_ldap) — its
|
||||
password is changed with `sudo yunohost user update gramps -p '<pw>'`, NOT `passwd`.
|
||||
- **fail2ban bans the whole IP for 10-12 min** after repeated failed SSH/root logins (all ports
|
||||
refuse; `ping` still works; droplet API still shows `active`). Wait it out — do NOT power-cycle.
|
||||
- Password reset this session: `sudo yunohost tools rootpw -n '<pw>'` sets root (rootpw takes `-n`).
|
||||
Verification: `getent shadow root` shows a `$y$` yescrypt hash + `passwd -S root` = `P`.
|
||||
- Mastodon services run as systemd units `mastodon-web/sidekiq/streaming` (all were `active` after
|
||||
the reboot); gitea/nginx/mariadb/postgres/redis/yunohost-api also systemd units. Disk was **93% full**
|
||||
(3.6G free) — keep an eye on it before any big upgrade.
|
||||
- The original outage was an interrupted Mastodon upgrade + a DO `password_reset` reboot; everything
|
||||
came back on its own after boot. No code/schema damage observed.
|
||||
|
||||
**Secrets (removed 2026-08-14 — see git history if a value is ever needed again):** the DO API token,
|
||||
the gramps/root passwords and the DO password-reset email password were recorded in this file and in
|
||||
chat. They are treated as **compromised** — the DO API token and every listed password have been or
|
||||
should be rotated/revoked, and **no new secrets belong in this repo or in chat**. Secret paths that
|
||||
stay here are the file locations only (OAuth creds → `Helpers/OAuthCredentials.cs`, session token →
|
||||
`Helpers/TokenStore.cs`).
|
||||
## Next up
|
||||
- **TASK 9 item 4 — live chat** (`liveChat/messages` poll, right-panel render, Super Chat + membership badges)
|
||||
- Item 8 (iTunes on mic meter) — logged in `bugs.md`, likely acoustic coupling, not a code bug
|
||||
|
||||
+24
-21
@@ -413,14 +413,15 @@
|
||||
VerticalAlignment="Center" Margin="8,0,0,0" Width="20"/>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0,4,0,0">
|
||||
<Button DockPanel.Dock="Right" Width="22" Height="22"
|
||||
Background="#22c55e" IsEnabled="False" Margin="4,0,0,0">
|
||||
<Path Data="M4,9 L8,14 L16,4" Stroke="White" StrokeThickness="2"
|
||||
Stretch="Uniform" Width="12" Height="12"/>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
||||
ToolTip="Revert" Click="WebcamBorderRevert_Click" Margin="4,0,0,0">
|
||||
<Path Data="M5,5 L15,15 M15,5 L5,15" Stroke="#e94560" StrokeThickness="2"
|
||||
Width="14" Height="14" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Button DockPanel.Dock="Right" IsEnabled="False" Margin="4,0,0,0">
|
||||
<Path Data="M4,9 L8,14 L16,4" Stroke="#22c55e" StrokeThickness="2"
|
||||
Width="14" Height="14" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Button DockPanel.Dock="Right" Content="✕" Width="22" Height="22"
|
||||
Background="#ef4444" Foreground="White" FontSize="11" FontWeight="Bold"
|
||||
ToolTip="Revert" Click="WebcamBorderRevert_Click" Margin="4,0,0,0"/>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
|
||||
@@ -435,14 +436,15 @@
|
||||
VerticalAlignment="Center" Margin="8,0,0,0" Width="20"/>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0,4,0,0">
|
||||
<Button DockPanel.Dock="Right" Width="22" Height="22"
|
||||
Background="#22c55e" IsEnabled="False" Margin="4,0,0,0">
|
||||
<Path Data="M4,9 L8,14 L16,4" Stroke="White" StrokeThickness="2"
|
||||
Stretch="Uniform" Width="12" Height="12"/>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
||||
ToolTip="Revert" Click="CountdownRevert_Click" Margin="4,0,0,0">
|
||||
<Path Data="M5,5 L15,15 M15,5 L5,15" Stroke="#e94560" StrokeThickness="2"
|
||||
Width="14" Height="14" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Button DockPanel.Dock="Right" IsEnabled="False" Margin="4,0,0,0">
|
||||
<Path Data="M4,9 L8,14 L16,4" Stroke="#22c55e" StrokeThickness="2"
|
||||
Width="14" Height="14" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Button DockPanel.Dock="Right" Content="✕" Width="22" Height="22"
|
||||
Background="#ef4444" Foreground="White" FontSize="11" FontWeight="Bold"
|
||||
ToolTip="Revert" Click="CountdownRevert_Click" Margin="4,0,0,0"/>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
|
||||
@@ -454,14 +456,15 @@
|
||||
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"
|
||||
ToolTip="https://example.com"/>
|
||||
<DockPanel Margin="0,4,0,0">
|
||||
<Button DockPanel.Dock="Right" Width="22" Height="22"
|
||||
Background="#22c55e" IsEnabled="False" Margin="4,0,0,0">
|
||||
<Path Data="M4,9 L8,14 L16,4" Stroke="White" StrokeThickness="2"
|
||||
Stretch="Uniform" Width="12" Height="12"/>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
||||
ToolTip="Revert" Click="WebRevert_Click" Margin="4,0,0,0">
|
||||
<Path Data="M5,5 L15,15 M15,5 L5,15" Stroke="#e94560" StrokeThickness="2"
|
||||
Width="14" Height="14" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Button DockPanel.Dock="Right" IsEnabled="False" Margin="4,0,0,0">
|
||||
<Path Data="M4,9 L8,14 L16,4" Stroke="#22c55e" StrokeThickness="2"
|
||||
Width="14" Height="14" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Button DockPanel.Dock="Right" Content="✕" Width="22" Height="22"
|
||||
Background="#ef4444" Foreground="White" FontSize="11" FontWeight="Bold"
|
||||
ToolTip="Revert" Click="WebRevert_Click" Margin="4,0,0,0"/>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user