Commit graph

17 commits

Author SHA1 Message Date
edb7975039 rebrand: rename all TeamsISO source paths to Dragon-ISO
Some checks failed
CI / build-and-test (push) Failing after 29s
Release / build-msi (push) Failing after 21s
- Rename solution files: TeamsISO.sln/slnf -> Dragon-ISO.sln/slnf
- Rename all src/TeamsISO.* directories and project files
  to src/Dragon-ISO.* equivalents
- Update .gitignore to exclude build/test output logs
- Update ci.yml, CHANGELOG.md, build-and-test.ps1, docs references

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:18:27 -04:00
1cdd4ebd04 fix(installer+wpf): REVERT runas /trustlevel demotion (it was the bug, not the fix)
Some checks failed
CI / build-and-test (push) Failing after 26s
Massive misdiagnosis correction. The 2025-05-16 effort to 'fix elevation' has been actively breaking every Start Menu / Desktop shortcut launch since rc7. Empirical retrace:

  - Elevated PowerShell -> Process.Start(exe) -> elevated TeamsISO -> WORKS
  - Elevated PowerShell with --keep-elevation -> elevated TeamsISO -> WORKS (vm.Participants.Count=2)
  - Non-elevated PS Process.Start(exe) -> medium TeamsISO -> WORKS
  - ANY launch through runas /trustlevel:0x20000 -> SAFER-restricted TeamsISO -> BROKEN (window appears, zero managed code runs past BAML parse, no logs, no port binds)

The SAFER-restricted token that runas /trustlevel produces breaks .NET 8 WPF apphost in a way that leaves the process apparently alive (with the MainWindow.xaml rendering the empty state from default property values) but executing zero managed code. So my StartupTrace, Serilog file sink, and ControlSurface bind all silently failed for every shortcut launch. Looked exactly like 'cold-start NDI Find stuck at zero' from the outside but had nothing to do with NDI.

Revert:
  - installer/Package.wxs: shortcuts target the .exe directly, no runas wrapper
  - App.xaml.cs: removed ShouldDeElevate, TryDeElevateAndExit, RelaunchEnvVar, --keep-elevation/--relaunched handling. The check is gone, not just disabled, so future-me can't bring it back without re-discovering the same bug.

Kept:
  - StartupTrace (still useful for any future startup mystery)
  - Self-healing NDI Find rebuild (c30a616) - still valuable for legitimate stuck-finder cases
  - System.Management PackageReference - TryGetParentProcessName still used in StartupTrace

Verified post-revert: Start Menu shortcut click -> PID 43060 -> full trace -> REST 2 participants. 252/252 tests still passing.
2026-05-16 16:27:23 -04:00
54ee578fe9 fix(wpf): de-elevate via runas env-var marker (CLI arg breaks runas /trustlevel)
Some checks failed
CI / build-and-test (push) Failing after 26s
The earlier de-elevation attempts failed because runas /trustlevel:0x20000 rejects any args after the program path (returns exit code 1 silently). Switch the relaunch loop-guard from --relaunched CLI arg to TEAMSISO_RELAUNCHED env var, which runas inherits and propagates cleanly. Also: always demote when elevated regardless of parent (the parent==explorer heuristic was too narrow; the runas demotion is cheap enough to do unconditionally), and add a StartupTrace fallback log at %LOCALAPPDATA%\\TeamsISO\\startup-trace.log that captures every checkpoint in OnStartup so future launch failures can be diagnosed without Serilog being up.

Verified end-to-end: elevated parent (PID 47536, isAdmin=True) -> spawns runas -> medium-integrity child (PID 51228, isAdmin=False) -> NDI discovery succeeds (vm.Participants.Count=2 at +5s). The TryDeElevateAndExit now returns bool so spawn failures fall through to normal startup instead of leaving the process in a zombie state.

Opt-out: --keep-elevation CLI arg bypasses the demotion.
2026-05-16 12:16:55 -04:00
191b2c5f52 fix(wpf): de-elevate when spawned by elevated explorer (NDI mDNS isolation)
Some checks failed
CI / build-and-test (push) Failing after 27s
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.
2026-05-16 11:36:52 -04:00
09e5b59dfd fix: cold-start discovery + installer shortcuts + single-instance hardening
Some checks failed
CI / build-and-test (push) Failing after 26s
Three independent fixes bundled because all were chasing the same operator
report: 'I just installed, launched from the shortcut, no participants.'

1) NdiDiscoveryService: poll immediately, then ramp from 200ms to the
   configured interval over the first 3 seconds. PeriodicTimer.WaitForNext-
   TickAsync waits the full interval before its first tick, so for a 500ms
   discovery interval the operator stared at 'no ndi sources yet' for half
   a second on every cold start. Force-poll up front (catches the runtime
   cache), then run a fast inner loop for ~3s while mDNS replies trickle
   in. Both loops share a try/finally so the NDI finder is always disposed.

2) MainViewModel.IsDiscovering: new boolean, true for 8s after engine start
   AS LONG AS no participants have arrived. MainWindow.xaml swaps the
   empty-state copy on this binding:
     IsDiscovering=true  -> 'scanning for ndi sources...' (cyan dot)
     IsDiscovering=false -> 'no ndi sources visible -- is teams in a
                            meeting?' + Refresh CTA
   The old copy ('no ndi sources yet -- open teams and start a meeting')
   was being shown immediately at launch even when discovery just hadn't
   run yet, making the app look broken.

3) App.xaml.cs: single-instance mutex moved from Local\ to Global\. On
   admin-user boxes with UAC disabled, launches from different parents
   (elevated File Explorer, non-elevated shell, etc.) can land in slightly
   different security contexts and a Local\ name can be invisible to the
   sibling. Global\ namespace closes that hole — both processes see the
   same mutex regardless of integrity. Belt-and-braces against future
   dual-instance file/port contention.

4) installer/Package.wxs: add a Desktop shortcut component (per-machine
   feature, HKCU keypath per ICE38/ICE43). Operators who can't find the
   Start Menu entry get the Desktop icon. Both shortcuts target the
   installed exe, NOT a stale path under publish/.
2026-05-16 11:23:19 -04:00
e67c02c2ff refactor(app): split App.xaml.cs into themed partial files
App.xaml.cs was 461 lines / 21KB and conflated four concerns: process-
level lifecycle (mutex / message pump filter / shutdown), engine bootstrap
(NDI runtime / IsoController / view model construction), crash handling
(three exception channels + log directory + dialog), and the background
update-checker kickoff.

Splits via partial-class into themed sibling files:

* App.xaml.cs (was 461L → now 219L) — class skeleton, fields, internal
  property accessors, Win32 P/Invoke surface, OnStartup as a wiring
  pipeline that calls the bootstrap steps in order, OnExit, CLI parser.
* App.Bootstrap.cs (250L, new) — linear startup steps:
  TryAcquireSingleInstance, TryBootstrapNdiInterop, BootstrapEngine,
  ConstructAndShowMainWindow, BootstrapControlSurfaceServices,
  BootstrapTrayIcon, TryShowOnboarding, TryAutoLaunchTeams. Each
  returns a signal (bool / window ref) when OnStartup needs it to
  decide whether to continue.
* App.CrashHandlers.cs (93L, new) — OnAppDomainUnhandled,
  OnDispatcherUnhandled, OnUnobservedTaskException, TryLogFatal,
  TryShowCrashDialog, LogDirectory.
* App.UpdateCheckBootstrap.cs (42L, new) — StartBackgroundUpdateCheck
  (24h-throttled, fire-and-forget).

OnStartup's body is now a 30-ish-line procedure that names each step,
which is what the original was trying to be. Comments inline the
"happened before, kept here for reason X" notes (theme.Apply before
window show; CLI args parsed before InitializeAsync). Behavior is
unchanged — Shutdown codes, error paths, and the side-effect order are
all preserved.

Build clean (0 warnings, 0 errors); 56 + 104 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:36:07 -04:00
647deec304 feat(web): topology + thumbnail endpoints, redesigned /ui control panel
Some checks failed
CI / build-and-test (push) Failing after 30s
REST additions: GET /topology returns mode (hidden/public/unknown) + sender/receiver group lists. POST /topology/apply confines local senders to teamsiso-input + receivers to public+teamsiso-input. POST /topology/restore returns both to public defaults.

GET /participants/{id}/thumbnail.jpg encodes the latest engine ProcessedFrame as a 192-wide JPEG. 404 when no pipeline is running. Used by the /ui control panel for live preview tiles.

Settings: ControlSurfaceEnabled now persists across sessions via UIPreferences and auto-starts the server on app launch when previously enabled.

/ui control panel rebuilt: live thumbnail per row, topology toggle card with Hide/Restore buttons, removed dead recording marker button, larger layout (920px), participant rows in single card with hover affordances.
2026-05-15 15:06:11 -04:00
d282e1b0f8 feat(wpf): v2 task 39+40 - studio table redesign + Ctrl+K command palette
Some checks failed
CI / build-and-test (push) Failing after 30s
Task 39: 5-column participants table - state LED, name+codec caption, 5-bar audio meter, mono output name, ISO pill. Row height 52, full-row active-speaker tint (no left stripe). New converter LevelThresholdConverter, OutputName property on ParticipantViewModel.

Task 40: Ctrl+K / Ctrl+P command palette - chromeless centered floating window, fuzzy Contains match across Label/Category/Keywords, arrow nav, Enter invoke, Esc close. Quick/Teams/Network/App categories cover top operator verbs and theme switching.

Also: log startup exceptions to Serilog before the modal MessageBox fires - much better triage signal than user-pasted dialog text.
2026-05-15 11:15:00 -04:00
c27130302f feat(wpf): v2 'Studio Terminal' shell - theme system, header, transport strip, drawer
Some checks failed
CI / build-and-test (push) Failing after 31s
- Theme split: Theme.Dark.xaml + Theme.Light.xaml + ThemeManager

- New shell: 32px header (mark + wordmark + 3 icons), 40px transport strip, conditional meeting bar, slide-over settings drawer

- Removed: 72px rail, 380px permanent settings panel, 6-column footer, custom chromeless title bar buttons

- Ctrl+T toggles theme; follows Windows app-mode by default

- Shape doc at docs/shapes/2026-05-13-teamsiso-v2-studio-terminal.md
2026-05-14 12:46:24 -04:00
1d1ce6a2a0 feat(wpf): rollback to WPF host, axe recording, fix settings pane
Some checks failed
CI / build-and-test (push) Failing after 29s
2026-05-14 06:02:40 -04:00
d8186c5eb8 Auto-launch + auto-hide Teams: 'I only see TeamsISO' experience
Some checks failed
CI / build-and-test (push) Failing after 28s
Two new persisted preferences in DISPLAY settings, paired to give operators the 'launch TeamsISO, never see Teams' experience the user asked for:

- LaunchTeamsOnStartup: TeamsISO auto-starts Teams in the background each launch (fire-and-forget background task in App.OnStartup, after the main window has materialized so a slow Teams launch doesn't delay the UI).

- AutoHideTeamsWindows: as soon as Teams' windows materialize after launch, hide them. New TeamsLauncher.AutoHideAfterLaunchAsync runs a polling loop (250ms / up to 15s) that catches the splash, main window, and any follow-up panels Teams opens. Teams takes 2-5s to render its main window and the splash arrives separately, so a one-shot hide right after launch wouldn't be enough.

When TeamsISO starts and Teams is already running (from a prior session), the auto-hide path still fires so the 'I only see TeamsISO' rule applies even when Teams was launched externally.

Operator drives everything through the IN-CALL bar (mute / camera / share / leave / marker) + participants DataGrid (ISO routing). Eye-toggle in the rail still restores Teams windows on demand.

Both toggles default to off — opt-in. Persisted via UIPreferences so they survive process restart.
2026-05-10 20:35:00 -04:00
b5fcc98d40 feat(ui): crash diagnostics, first-launch welcome, reset-to-defaults 2026-05-10 09:41:29 -04:00
16e0a483e2 fix: address review findings on tonight's commits
All checks were successful
CI / build-and-test (push) Successful in 35s
Code review on d14a33a..bab29b0 turned up three real issues, fixed here.

1. EngineLogging.CreateDefault no longer mutates Serilog.Log.Logger. The static set was a belt-and-suspenders attempt to catch any code path that reaches for the singleton, but it doesn't matter (engine code uses ILogger<T>, never Serilog.Log.*) and it raced under xUnit's parallel test execution.

2. IsoPipeline stops holding a RawFrame reference for stats. The receiver-side TappedChannelWriter callback now snapshots only Width/Height into volatile ints — frame's pixel buffer is allowed to GC on its normal schedule and a late stats poll can never resurrect a dropped frame. (Today the buffer is fully managed so a use-after-free wasn't actually possible, but the snapshot pattern is the right ownership shape.)

3. App.xaml.cs's ComponentDispatcher.ThreadFilterMessage subscription now lives in a field and is unsubscribed in OnExit. Mutex release is gated on a new _ownsSingleInstanceMutex flag so the 'lost the race; shut down silently' path doesn't accidentally try to release a handle it never owned.

Plus a load-bearing comment in NdiInteropPInvoke.CreateFinder explaining why we free the UTF-8 group buffers right after the native call returns — same lifetime contract Phase B-2's CreateReceiver / CreateSender have always relied on; if it's wrong, those would fail too. The loopback discovery integration test would catch a regression.

Tests: 74/74 unit + 9/9 NDI integration green.
2026-05-08 01:01:00 -04:00
1d85396a90 feat(logging): rolling file sink under %LOCALAPPDATA%\\TeamsISO\\Logs
Adds Serilog.Sinks.File to TeamsISO.Engine and a new EngineLogging.CreateDefault() factory that writes to BOTH the existing console sink and a rolling daily file at %LOCALAPPDATA%\\TeamsISO\\Logs\\teamsiso<date>.log. The WPF host (TeamsISO.exe is a WinExe with no console attached at runtime) now uses CreateDefault so support has something to ask for when users file an issue. The Console build keeps using CreateConsole — stdout is the right surface there and shell redirection beats a competing on-disk sink.

Files roll daily, cap at 10 MB before mid-day rollover, and only the most recent 14 are retained. Disk flush interval is 250 ms so a tail -f from another tool sees lines promptly. Path is announced via the first log line on every startup.

Two unit tests gate the wiring: AllLoggers_WriteToFile (verifies both typed and named CreateLogger() reach the file) and LogsAtBelowMinimumLevel_AreSuppressed (regression guard for level filtering). 74/74 unit tests pass (was 72).

Also adds a startup breadcrumb log line in App.OnStartup carrying the build version + PID so we can correlate a user's log file with a specific commit.
2026-05-08 00:47:25 -04:00
53c06a9af9 feat(ui): single-instance enforcement via per-user named mutex
Two simultaneous TeamsISO processes contend over the NDI runtime, the same default sender names, and %APPDATA%\\TeamsISO\\config.json — observed during testing when launchers / shortcuts produced duplicate windows. Add a Local namespace per-user-keyed mutex (Local\\WildDragon.TeamsISO.SingleInstance.<username>) at startup; if a second instance can't claim it, broadcast a registered window message ('WildDragon.TeamsISO.BringToFront') and Shutdown(0). The running instance subscribes to that message via ComponentDispatcher.ThreadFilterMessage and surfaces its main window when received.

Per-user keying lets two different Windows users on the same machine each run their own TeamsISO. Mutex is released and disposed on OnExit.

Verified: Start-Process the exe twice in a row -> only one process remains, with the original window surfaced.
2026-05-07 23:59:47 -04:00
e3321ff279 feat(ui): wire DI bootstrap in App.xaml.cs and add Windows solution filter
Some checks failed
CI / build-and-test (push) Failing after 36s
2026-05-07 15:41:58 +00:00
f21e818b28 chore: scaffold WPF app and integration test projects
- 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).
2026-05-07 15:09:56 +00:00