Observed behavior: on admin-user boxes with UAC effectively disabled, double-clicking the Start Menu / Desktop shortcut spawns TeamsISO with elevated File Explorer as parent. NDI Find then returns zero sources even when Teams is broadcasting — same exe spawned from any other parent (PowerShell, cmd, runas, etc.) discovers sources fine. Suspected window-station / desktop-handle inheritance quirk in NDI's mDNS layer; can't fix from inside the runtime.
Workaround: in OnStartup, if parent IS explorer.exe AND we're elevated AND we haven't already re-launched (--relaunched guard), re-spawn ourselves via 'runas /trustlevel:0x20000' to drop to medium integrity. Original process Shutdowns; only the medium child remains. Verified by reproducing the failure case in an elevated PowerShell, then watching the same runas command produce a working child (REST returns participants, log writes work).
Add PackageReference for System.Management (Win32_Process via ManagementObjectSearcher) so the parent-PID lookup compiles.
Walks the v2 polish punch list against MainWindow.
- Theme button tooltip is now "Theme (System / Dark / Light)" per the
v2 shape brief, replacing the previous "Toggle theme (Ctrl+T)".
- Participants table column widths match spec: Output 130px (was 150),
ISO pill 100px (was 110). The 24px state LED, 110px audio meter, and
52px row height already matched. The 106px Preview thumbnail column
and 32px gear-button column are intentional deviations (live thumbs
were restored at 4944de5; per-ISO override gear added at the same
time) and are now called out in the column-spec comment so a future
reader doesn't try to "fix" them.
- Empty-state placeholder finally renders when ParticipantCount == 0:
mono sentence "no ndi sources yet — open teams and start a meeting"
+ a tertiary Refresh discovery button — exactly the copy specified
by the shape brief's empty-states section. CountToVisibilityConverter
is now declared in MainWindow.Resources (it shipped as a class but
was never registered).
- OnClosing wraps WindowStateStore.Save in a try/catch so a serialization
or filesystem fault on shutdown can never block the window from
closing. Save itself already swallows its own IO errors; this is
defense-in-depth for anything that escapes.
- MessageBox copy in MainWindow.xaml.cs (Hide/show Teams, Launch Teams,
Stop Teams) moves to Properties/Strings.resx + a hand-written
Properties/Strings.Designer.cs accessor. ResourceManager reads it by
basename "TeamsISO.App.Properties.Strings"; LogicalName is set on the
EmbeddedResource so the manifest name is predictable regardless of
how MSBuild would otherwise compute it. Future-localization seam.
OnLaunchTeamsRightClick's confirmation dialog is intentional — it
guards a destructive mid-show action — and the code-behind comment
now says so; the palette also offers Stop Teams as the keyboard
surface, so the right-click affordance isn't the only one.
Build clean (0 warnings, 0 errors); 160 tests still pass (56 App +
104 Engine, Category!=ndi&requires!=ndi filter).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four polish improvements aimed at production-floor usability.
1. Empty-state placeholder for the participants card. When Participants.Count == 0, the DataGrid is hidden in favor of a friendly 'Waiting for Teams' panel: faded dragon mark, headline, explainer, and a four-item checklist (Teams running? NDI broadcast on? Discovery group correct? Firewall clear?). New CountToVisibilityConverter (with optional 'empty' parameter to invert) drives both the placeholder and the DataGrid visibility from the same Participants.Count source.
2. Per-pipeline error / no-signal surfacing. IsoHealthStats grows an init-only State property populated from IsoPipeline.State. ParticipantViewModel.UpdateStats maps that to a StateLabel ('LIVE' / 'NO SIGNAL' / 'ERROR' / 'STARTING' / '—'). The ISO toggle button gains DataTriggers on StateLabel — coral-tinted '● ERROR' when the supervisor gives up, amber-tinted '● NO SIGNAL' when the slate threshold trips. Operators can see at a glance which pipelines are broken.
3. JetBrains Mono Variable v2.304 (OFL) bundled at Assets/Fonts/JetBrainsMono.ttf. Wd.Font.Mono now points at the embedded font so machine names, timecodes, and stat counters render in JetBrains Mono regardless of system fonts. Falls back to Cascadia Mono / Consolas if the resource is missing.
4. Tooltip pass over every interactive control in the settings panel (framerate / resolution / aspect / audio / discovery group / output group / hide-local checkbox / Apply button / per-row Output Name textbox / per-row ISO toggle). Operators learn affordances on hover instead of by trial and error.
Tests: 76/76 unit + 9/9 NDI integration green.
Four polish items + a test pass.
1. Inter Variable (rsms/inter v3.19, OFL) is bundled at Assets/Fonts/Inter.ttf (~800 KB) and registered as a WPF Resource. WildDragonTheme.xaml's Wd.Font.Sans now points at pack://application:,,,/Assets/Fonts/#Inter so the typography matches wilddragon.net regardless of whether the user has Inter installed system-wide. Falls back to Segoe UI Variable Display if the resource is missing.
2. 'Stop all ISOs' button at the right of the participants header. Bound to a new MainViewModel.StopAllIsosCommand that snapshots the enabled list, awaits DisableIsoAsync sequentially, and silently swallows per-pipeline failures (best-effort emergency stop). CanExecute gates on whether any ISO is currently enabled.
3. WindowStateStore service persists the main window's Left/Top/Width/Height/State to %LOCALAPPDATA%\\TeamsISO\\window.json on close and restores it on SourceInitialized. Multi-monitor friendly: a saved position with no corner inside any virtual screen is rejected so a disconnected monitor doesn't strand the window off-screen.
4. Two new unit tests cover FrameProcessor's drops + duplicates accounting. 76/76 unit tests pass (was 74).
Six related polish items, all building on tonight's groundwork.
1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now.
2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart.
3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'.
4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets.
5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo.
6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition.
Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
Two related deliverables addressing the user's morning asks.
1. Branding: Dragon WHITE.png and Wild Dragon Logo WHITE.png from the brand kit are copied into src/TeamsISO.App/Assets/ and registered as <Resource> items in the .csproj. The rail's placeholder 'W' glyph is replaced by the real dragon mark (40x40, HighQuality bitmap scaling) with a 'Wild Dragon' caption underneath.
2. NDI Access Manager automation: NdiAccessManagerConfig service reads/writes %APPDATA%\\NDI\\ndi-config.v1.json, working in JsonNode trees so we don't clobber unrelated keys. ApplyTranscoderTopology() sets groups.send=[teamsiso-input] and groups.recv=[public, teamsiso-input] so all local senders (Teams + anything else) broadcast on the private group while local receivers can still see public sources too. Engine-side, the user's per-pipeline OutputGroups override pushes TeamsISO outputs back onto Public so downstream switchers see clean ISOs.
Atomic write: temp + replace, with timestamped backup of the prior config. ReadCurrentGroups() can be used by future UI to show what's currently configured. RestoreDefaults() reverts.
Settings panel grows an 'Apply transcoder topology' button under the NDI Network section. Click writes the system config, sets the engine's discovery=teamsiso-input / output=public, refreshes the bound text boxes, and pops a dialog with a 'restart Teams' reminder + the backup path.
- TeamsISO.App: hand-rolled net8.0-windows WPF csproj since the WPF
template isn't shipped on linux-arm64 .NET SDK; UI is a placeholder
for Phase C.
- TeamsISO.Engine.IntegrationTests: cross-platform xunit project with a
skipped scaffold fact tagged [Trait("requires", "ndi")] for Phase B.
- TeamsISO.Linux.slnf: solution filter for non-Windows CI that excludes
the WPF project (which can only build on Windows).