diff --git a/docs/superpowers/plans/_NEXT.md b/docs/superpowers/plans/_NEXT.md index 2b4483f..4a800a6 100644 --- a/docs/superpowers/plans/_NEXT.md +++ b/docs/superpowers/plans/_NEXT.md @@ -6,38 +6,68 @@ - **Phase B-1 — Pipeline Orchestration** (tag: `phase-b-1-complete`) — NdiReceiver, NdiSender, ExponentialBackoff, NdiRuntimeProbe, IsoPipeline supervisor, IsoController. - **Phase B-2 — Real NDI Interop** (tag: `phase-b-2-complete`) — `NdiInteropPInvoke` against NDI 6 SDK, managed BGRA scaler, `TeamsISO.Console` headless smoke runner, `NdiVersion` constants. - **Phase C — WPF UI** (tag: `phase-c-complete`) — MVVM helpers, ParticipantViewModel, GlobalSettingsViewModel, AlertBannerViewModel, MainViewModel, MainWindow XAML with participants DataGrid + settings sidebar + alert banner, App.xaml DI bootstrap. -- **Hardening — May 2026** (no tag) — see "Done in May 2026" below; covers the bug-fix triad that turned this from "won't even start" into "discovers real Teams participants in a real meeting", plus integration coverage and the brand/UX rebuild. -- **Phase D — WiX Installer** — WiX v5 MSI scaffold, Forgejo CI fix (artifact v3 pin), Forgejo release workflow on tag push. +- **Hardening + brand pass — May 2026** — see "Done since the May 2026 hand-off" below. +- **Phase D — WiX Installer & Forgejo release** — WiX v5 MSI scaffold, ARP icon wired, tag-push release workflow that builds + uploads MSI as a release asset. -## Done in May 2026 +## Done since the May 2026 hand-off -- Fixed `.sln` path-separator mismatch that broke `.slnf` filters on Windows. -- `NdiNativeLibraryResolver` resolves `Processing.NDI.Lib.x64.dll` via `NDI_RUNTIME_DIR_V6` so the engine starts on installs where the NDI dir isn't on PATH. -- `NdiVersion.ExpectedRuntimeVersionPrefix` updated to match the shipping NDI 6 banner format (`NDI SDK WIN64 ...`). -- `NdiSourceParser` accepts current Teams desktop's `MS Teams - ` brand format (plus legacy `Teams` and defensive `Microsoft Teams`). -- `--list-sources` diagnostic on `TeamsISO.Console`. -- NDI groups end-to-end (discovery + output) so the operator can confine Teams' raw broadcasts to a private group. -- Hide-(Local) toggle so the user's own self-preview doesn't pollute the participants list. -- Single-instance enforcement via per-user named Mutex with broadcast bring-to-front. -- WPF rebuilt around Wild Dragon brand × Microsoft Teams flush layout (left rail + chromeless title bar + caption controls + cyan accent + JetBrains Mono). -- `IsoHealthStats` wired end-to-end: live receiver/sender refs published from the inner pipeline, frame counters and source resolution displayed in a `Live` column on the participants DataGrid (1 Hz polled). +### Engine +- Forward-slash project paths in `TeamsISO.sln` so `.slnf` filters work on Windows MSBuild. +- `NdiNativeLibraryResolver` resolves `Processing.NDI.Lib.x64.dll` via `NDI_RUNTIME_DIR_V6` (with V5 / V4 fallbacks), so the engine starts on installs where the NDI dir isn't on PATH. +- `NdiVersion.ExpectedRuntimeVersionPrefix` updated to match the shipping NDI 6 banner format (`NDI SDK WIN64 …`). +- `NdiSourceParser` accepts current Teams desktop's `MS Teams - ` brand format (plus legacy `Teams` and defensive `Microsoft Teams`); reserved suffixes (`Active Speaker`, `Audio`, `Audio Mix`, `Screen Share`) are recognized in both legacy and dash-prefixed forms. +- NDI **groups** end-to-end (discovery + output): `INdiInterop.CreateFinder(string?)` and `CreateSender(string, string?)` populate `p_groups`; `IsoController` threads them through from `EngineConfig.NdiGroups`. +- `ParticipantTracker` surfaces `NdiSourceKind.ActiveSpeaker` as a synthetic routable row named "Active Speaker" with a deterministic v5-GUID Id derived from `auto-mix:`. +- `IsoHealthStats` wired end-to-end: live receiver/sender/processor refs published from the inner pipeline, frame counters / source resolution / running FPS (30-frame moving window) / drops + duplicates / pipeline state surfaced via `IsoController.GetStats`. - Rolling daily file logging at `%LOCALAPPDATA%\TeamsISO\Logs\` via Serilog.Sinks.File. -- Real-NDI integration test tier (`requires=ndi`): runtime probe, finder/sender lifecycle on default + custom groups, loopback discovery, and a full pipeline frame round-trip that asserts 1920×1080 normalization. -- Forgejo CI is green (`actions/upload-artifact` pinned to v3 since Forgejo doesn't support v4). -- WiX v5 MSI scaffold + Forgejo release workflow on tag push (windows-latest runner; uploads MSI as both workflow artifact and release asset). -- Code-review pass + fixes (no static `Serilog.Log.Logger` mutation, frame-dimension snapshot instead of holding a `RawFrame` ref, ComponentDispatcher unsubscribe, Mutex-ownership flag). -- "Launch Teams" rail button (Phase E.1 starter, see `docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md`). + +### UI +- WPF rebuilt around Wild Dragon brand × Microsoft Teams flush layout — left rail with real dragon-mark logo (clickable → About dialog), chromeless title bar with custom min/max/close caption controls, cyan accent. +- Inter Variable + JetBrains Mono Variable bundled as `` so typography matches wilddragon.net regardless of system fonts. +- App icon `teamsiso.ico` (7 sizes) on taskbar / window / About / WiX MSI ARP. +- Single-instance enforcement via per-user named Mutex with broadcast bring-to-front. +- Empty-state placeholder when no Teams sources are visible (faded dragon + checklist). +- Live frame counters in the Source / Live columns (in/out/drops, source resolution, running FPS). +- Per-pipeline state surfaced in the ISO toggle: `● LIVE` (cyan), `● ERROR` (coral), `● NO SIGNAL` (amber), `…` (processing). +- "Stop all ISOs" emergency button at the participants header. +- Hide-(Local) toggle so the user's own self-preview is filtered from the participants list. +- Window position / size / state persisted to `%LOCALAPPDATA%\TeamsISO\window.json`, multi-monitor safe. +- Tooltips on every interactive control in the settings panel + per-row textbox + ISO toggle. + +### Networking automation +- One-click **transcoder topology** button in Settings: writes `%APPDATA%\NDI\ndi-config.v1.json` so all local senders broadcast on `teamsiso-input` and local receivers see both `public` + `teamsiso-input`. Engine settings auto-flip to receive-from `teamsiso-input` and emit-on `public`. Atomic write with timestamped backup of the prior config. + +### Phase E.1 starter (embedded Teams) +- Spec at `docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md` (three-phase rollout: launcher → window orchestration → in-app meeting controls). +- Rail "Launch / Stop Teams" toggle: launches via `ms-teams:` URI / `ms-teams.exe` / classic `Update.exe --processStart`, asks to confirm `WM_CLOSE` of all running Teams windows when toggled while Teams is up. + +### Diagnostics +- `TeamsISO.Console --list-sources` enumerates raw NDI source names visible to the local finder for ~5 seconds; debugging tool for setup issues. +- `TeamsISO.Console --version` prints engine version + build SHA + .NET + OS + NDI runtime banner + exit-code legend, for support tickets. +- About dialog inside the WPF host with the same info. + +### CI / Release / Docs +- Forgejo CI is green: `actions/upload-artifact@v3` (Forgejo doesn't support v4 yet). +- `.forgejo/workflows/release.yml`: tag-push (`v*.*.*`) builds + tests + publishes + builds the MSI on a Windows runner and attaches it to the auto-created Forgejo release via the REST API. +- `docs/RELEASING.md` walks through cutting a release and flags the code-signing TODO. + +### Tests +- 78 unit tests passing; 9 NDI integration tests gated behind `--filter requires=ndi` (runtime probe, finder + sender lifecycle on default and custom groups, loopback discovery, full pipeline frame round-trip asserting 1080p normalization). ## Next -1. **Phase E — Embedded Teams orchestration** — see the spec at `docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md`. Three-phase rollout: launcher → window orchestration → in-app meeting controls. Phase E.1 partially shipped (launcher). +1. **Phase E — Embedded Teams orchestration (continued).** E.2 (window orchestration: hide Teams' main window once launched, forward keyboard shortcuts via SendInput) and E.3 (Microsoft Graph or UIAutomation in-call controls) per the spec at `docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md`. -2. **Code-signing the MSI** — `installer/TeamsISO.Installer.wixproj` has a `SignOutput` hook but no cert. For a v1.0 release, wire `signtool` invocation from a release-only CI secret. Until then SmartScreen will warn on first launch. +2. **Code-signing the MSI.** `installer/TeamsISO.Installer.wixproj` has a `SignOutput` property hook but no cert; for v1.0 wire a `signtool` invocation from a CI secret. SmartScreen will warn on first launch until that lands. -3. **Bundle Inter / JetBrains Mono fonts** so the typography doesn't depend on Windows fallbacks. Today the WPF UI uses Segoe UI Variable Display + Cascadia Mono as fallbacks. +3. **Toast / inline feedback** for "Apply Changes" and other settings actions (currently silent unless an error occurs). -4. **Wild Dragon dragon-mark** — the rail logo is a stylized "W" placeholder; swap in the real dragon SVG when available. +4. **Rail "Refresh sources"** affordance — force a discovery rescan, useful right after applying a new transcoder topology when the operator wants Teams to reconnect. -5. **Optional polish before v1.0** — running incoming-fps display (today the field on `IsoHealthStats` is 0), per-pipeline output thumbnail previews, MaterialDesignThemes-equivalent transitions, system health (CPU/network) meters in the rail. +5. **Output thumbnail previews** in the participant DataGrid — small live-frame previews of each ISO output. Complex (needs WPF WriteableBitmap from the Pipeline's last ProcessedFrame); deferred until v1.5. -6. **Drops counter on `IsoHealthStats`** — `FrameProcessor` doesn't currently surface drops or duplicates; wire those through so the Live column can show "↓ N (dropped M)". +6. **Settings panel UX tightening.** It's getting long; consider an Accordion or tabs for OUTPUT FORMAT / NDI NETWORK / DISPLAY rather than one scrolling stack. + +7. **Auto-disable on participant departure (configurable).** Today an ISO stays "enabled" if its participant leaves; the source goes null. Optional toggle: tear down the pipeline automatically when a participant has been gone past the rename window. + +8. **Operator presets.** "Save current ISO assignments to a named preset" + "Load preset on next launch" so an operator with a recurring show doesn't have to re-toggle every meeting. diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index 4a1fae8..c8b8d59 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -545,6 +545,29 @@ + + + + + + + + diff --git a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs index 41333e8..9b78e56 100644 --- a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs +++ b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs @@ -12,6 +12,7 @@ namespace TeamsISO.App.ViewModels; public sealed class GlobalSettingsViewModel : ObservableObject { private readonly IIsoController _controller; + private readonly ToastViewModel? _toast; private TargetFramerate _framerate; private TargetResolution _resolution; private AspectMode _aspect; @@ -20,9 +21,10 @@ public sealed class GlobalSettingsViewModel : ObservableObject private string _outputGroups; private bool _hideLocalSelf = true; - public GlobalSettingsViewModel(IIsoController controller) + public GlobalSettingsViewModel(IIsoController controller, ToastViewModel? toast = null) { _controller = controller; + _toast = toast; var current = controller.GlobalSettings; _framerate = current.Framerate; _resolution = current.Resolution; @@ -80,6 +82,8 @@ public sealed class GlobalSettingsViewModel : ObservableObject DiscoveryGroups: string.IsNullOrWhiteSpace(_discoveryGroups) ? null : _discoveryGroups.Trim(), OutputGroups: string.IsNullOrWhiteSpace(_outputGroups) ? null : _outputGroups.Trim()); await _controller.SetGroupSettingsAsync(groups, CancellationToken.None); + + _toast?.Show("Settings saved"); } private async Task ApplyTranscoderTopologyAsync() @@ -121,5 +125,7 @@ public sealed class GlobalSettingsViewModel : ObservableObject "TeamsISO — Apply transcoder topology", MessageBoxButton.OK, MessageBoxImage.Information); + + _toast?.Show("Transcoder topology applied — restart Teams to take effect"); } } diff --git a/src/TeamsISO.App/ViewModels/MainViewModel.cs b/src/TeamsISO.App/ViewModels/MainViewModel.cs index ea77653..e920414 100644 --- a/src/TeamsISO.App/ViewModels/MainViewModel.cs +++ b/src/TeamsISO.App/ViewModels/MainViewModel.cs @@ -25,6 +25,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable public ObservableCollection Participants { get; } = new(); public GlobalSettingsViewModel Settings { get; } public AlertBannerViewModel AlertBanner { get; } = new(); + public ToastViewModel Toast { get; } /// /// Emergency-stop: disable every running ISO. Bound to a small "Stop all" affordance @@ -43,7 +44,8 @@ public sealed class MainViewModel : ObservableObject, IDisposable { _controller = controller; _dispatcher = dispatcher; - Settings = new GlobalSettingsViewModel(controller); + Toast = new ToastViewModel(dispatcher); + Settings = new GlobalSettingsViewModel(controller, Toast); _participantsSub = controller.Participants .ObserveOn(new SynchronizationContextScheduler( diff --git a/src/TeamsISO.App/ViewModels/ToastViewModel.cs b/src/TeamsISO.App/ViewModels/ToastViewModel.cs new file mode 100644 index 0000000..e2e874a --- /dev/null +++ b/src/TeamsISO.App/ViewModels/ToastViewModel.cs @@ -0,0 +1,68 @@ +using System.Windows.Threading; + +namespace TeamsISO.App.ViewModels; + +/// +/// Lightweight transient-notification view-model. The main view holds a single +/// instance bound to a small overlay at the bottom of the content area. +/// displays a message for a fixed duration before auto-hiding; +/// successive Show calls reset the timer instead of stacking, so the user always +/// sees the most recent action's feedback. +/// +public sealed class ToastViewModel : ObservableObject +{ + private readonly DispatcherTimer _hideTimer; + private string _message = string.Empty; + private bool _isVisible; + private string _accent = "Wd.Accent.Cyan"; + + public ToastViewModel(Dispatcher dispatcher) + { + _hideTimer = new DispatcherTimer(DispatcherPriority.Background, dispatcher) + { + Interval = TimeSpan.FromSeconds(3), + }; + _hideTimer.Tick += (_, _) => + { + _hideTimer.Stop(); + IsVisible = false; + }; + } + + public string Message + { + get => _message; + private set => SetField(ref _message, value); + } + + public bool IsVisible + { + get => _isVisible; + private set => SetField(ref _isVisible, value); + } + + /// + /// Brush resource key for the accent dot. "Wd.Accent.Cyan" for success-style + /// (default), "Wd.Accent.Coral" for warnings. + /// + public string Accent + { + get => _accent; + private set => SetField(ref _accent, value); + } + + /// Show a success-style toast for ~3 seconds. + public void Show(string message) => ShowImpl(message, "Wd.Accent.Cyan"); + + /// Show a warning-style toast (coral accent) for ~3 seconds. + public void Warn(string message) => ShowImpl(message, "Wd.Accent.Coral"); + + private void ShowImpl(string message, string accentKey) + { + Message = message; + Accent = accentKey; + IsVisible = true; + _hideTimer.Stop(); + _hideTimer.Start(); + } +}