2026-05-31 11:18:27 -04:00
|
|
|
|
using System.Collections.Specialized;
|
refactor(control-surface): split server into endpoint partials
ControlSurfaceServer.cs was 1061 lines / 47KB — a single class hosting
the HttpListener loop, the route dispatch, and every endpoint body in
between. Splits the class via partial-class into a thin host file plus
one partial per route group, all under Services/ControlSurface/.
* Services/ControlSurfaceServer.cs (was 1061L → now 400L) — kept here:
Start / Stop / DisposeAsync (the listener lifecycle), AcceptLoopAsync,
HandleRequestAsync (the route table itself, with its CORS preflight +
WebSocket upgrade + JSON dispatch), the response helpers
(ReadBodyAsync / WriteJsonAsync / TryGetBool / TryGetString), the
NotFound switch-arm, and the JsonSerializerOptions singleton.
* Services/ControlSurface/Endpoints/HomeEndpoints.cs — GetServerInfo,
TryRead helper.
* Services/ControlSurface/Endpoints/ParticipantsEndpoints.cs (the
biggest split) — GetParticipants, SetIsoOverrideByIdAsync,
ClearIsoOverrideByIdAsync, TryParseEnum, ToggleIsoByIdAsync,
ToggleIsoByNameAsync, ToggleByIdAsync. Together: every /participants/*
handler.
* Services/ControlSurface/Endpoints/PresetsEndpoints.cs — RefreshDiscovery,
StopAllAsync, ApplyPresetAsync.
* Services/ControlSurface/Endpoints/TeamsEndpoints.cs — InvokeTeams
(the helper that maps a TeamsControlBridge result to the JSON body).
* Services/ControlSurface/Endpoints/TopologyEndpoints.cs — GetTopology,
ApplyTopologyAsync, RestoreTopologyAsync.
* Services/ControlSurface/Endpoints/NotesEndpoints.cs — AppendNote.
* Services/ControlSurface/Endpoints/ThumbnailEndpoint.cs —
TryEncodeThumbnailJpeg (which is actually the BMP path now) +
EncodeBmpDownscaled + the LE byte writers. The legacy
TryEncodeThumbnailJpeg_WpfDeadCode helper that was dead-coded "for
posterity" is gone — no call sites; we removed-comments-on-removed-
code is the anti-pattern we wanted to fix.
* Services/ControlSurface/WebSocketHub.cs — HandleWebSocketAsync,
PushSnapshotIfChangedAsync, SendAsync, GetSnapshotJsonAsync. The
push-timer wiring stays in the host's Start() so the lifetime is
obvious where the connection is opened.
No behavior change. The route table in HandleRequestAsync still
dispatches by (HttpMethod, path) — only the handler bodies moved.
Build clean; 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:48:03 -04:00
|
|
|
|
using System.Text.Json;
|
2026-05-31 11:18:27 -04:00
|
|
|
|
using DragonISO.Engine.Domain;
|
refactor(control-surface): split server into endpoint partials
ControlSurfaceServer.cs was 1061 lines / 47KB — a single class hosting
the HttpListener loop, the route dispatch, and every endpoint body in
between. Splits the class via partial-class into a thin host file plus
one partial per route group, all under Services/ControlSurface/.
* Services/ControlSurfaceServer.cs (was 1061L → now 400L) — kept here:
Start / Stop / DisposeAsync (the listener lifecycle), AcceptLoopAsync,
HandleRequestAsync (the route table itself, with its CORS preflight +
WebSocket upgrade + JSON dispatch), the response helpers
(ReadBodyAsync / WriteJsonAsync / TryGetBool / TryGetString), the
NotFound switch-arm, and the JsonSerializerOptions singleton.
* Services/ControlSurface/Endpoints/HomeEndpoints.cs — GetServerInfo,
TryRead helper.
* Services/ControlSurface/Endpoints/ParticipantsEndpoints.cs (the
biggest split) — GetParticipants, SetIsoOverrideByIdAsync,
ClearIsoOverrideByIdAsync, TryParseEnum, ToggleIsoByIdAsync,
ToggleIsoByNameAsync, ToggleByIdAsync. Together: every /participants/*
handler.
* Services/ControlSurface/Endpoints/PresetsEndpoints.cs — RefreshDiscovery,
StopAllAsync, ApplyPresetAsync.
* Services/ControlSurface/Endpoints/TeamsEndpoints.cs — InvokeTeams
(the helper that maps a TeamsControlBridge result to the JSON body).
* Services/ControlSurface/Endpoints/TopologyEndpoints.cs — GetTopology,
ApplyTopologyAsync, RestoreTopologyAsync.
* Services/ControlSurface/Endpoints/NotesEndpoints.cs — AppendNote.
* Services/ControlSurface/Endpoints/ThumbnailEndpoint.cs —
TryEncodeThumbnailJpeg (which is actually the BMP path now) +
EncodeBmpDownscaled + the LE byte writers. The legacy
TryEncodeThumbnailJpeg_WpfDeadCode helper that was dead-coded "for
posterity" is gone — no call sites; we removed-comments-on-removed-
code is the anti-pattern we wanted to fix.
* Services/ControlSurface/WebSocketHub.cs — HandleWebSocketAsync,
PushSnapshotIfChangedAsync, SendAsync, GetSnapshotJsonAsync. The
push-timer wiring stays in the host's Start() so the lifetime is
obvious where the connection is opened.
No behavior change. The route table in HandleRequestAsync still
dispatches by (HttpMethod, path) — only the handler bodies moved.
Build clean; 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:48:03 -04:00
|
|
|
|
|
2026-05-31 11:18:27 -04:00
|
|
|
|
namespace DragonISO.App.Services;
|
refactor(control-surface): split server into endpoint partials
ControlSurfaceServer.cs was 1061 lines / 47KB — a single class hosting
the HttpListener loop, the route dispatch, and every endpoint body in
between. Splits the class via partial-class into a thin host file plus
one partial per route group, all under Services/ControlSurface/.
* Services/ControlSurfaceServer.cs (was 1061L → now 400L) — kept here:
Start / Stop / DisposeAsync (the listener lifecycle), AcceptLoopAsync,
HandleRequestAsync (the route table itself, with its CORS preflight +
WebSocket upgrade + JSON dispatch), the response helpers
(ReadBodyAsync / WriteJsonAsync / TryGetBool / TryGetString), the
NotFound switch-arm, and the JsonSerializerOptions singleton.
* Services/ControlSurface/Endpoints/HomeEndpoints.cs — GetServerInfo,
TryRead helper.
* Services/ControlSurface/Endpoints/ParticipantsEndpoints.cs (the
biggest split) — GetParticipants, SetIsoOverrideByIdAsync,
ClearIsoOverrideByIdAsync, TryParseEnum, ToggleIsoByIdAsync,
ToggleIsoByNameAsync, ToggleByIdAsync. Together: every /participants/*
handler.
* Services/ControlSurface/Endpoints/PresetsEndpoints.cs — RefreshDiscovery,
StopAllAsync, ApplyPresetAsync.
* Services/ControlSurface/Endpoints/TeamsEndpoints.cs — InvokeTeams
(the helper that maps a TeamsControlBridge result to the JSON body).
* Services/ControlSurface/Endpoints/TopologyEndpoints.cs — GetTopology,
ApplyTopologyAsync, RestoreTopologyAsync.
* Services/ControlSurface/Endpoints/NotesEndpoints.cs — AppendNote.
* Services/ControlSurface/Endpoints/ThumbnailEndpoint.cs —
TryEncodeThumbnailJpeg (which is actually the BMP path now) +
EncodeBmpDownscaled + the LE byte writers. The legacy
TryEncodeThumbnailJpeg_WpfDeadCode helper that was dead-coded "for
posterity" is gone — no call sites; we removed-comments-on-removed-
code is the anti-pattern we wanted to fix.
* Services/ControlSurface/WebSocketHub.cs — HandleWebSocketAsync,
PushSnapshotIfChangedAsync, SendAsync, GetSnapshotJsonAsync. The
push-timer wiring stays in the host's Start() so the lifetime is
obvious where the connection is opened.
No behavior change. The route table in HandleRequestAsync still
dispatches by (HttpMethod, path) — only the handler bodies moved.
Build clean; 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:48:03 -04:00
|
|
|
|
|
|
|
|
|
|
// /participants/* route handlers. Anything that reads or writes
|
|
|
|
|
|
// participant + per-pipeline state lives here.
|
|
|
|
|
|
//
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// GET /participants → GetParticipants
|
|
|
|
|
|
// POST /participants/{id}/iso → ToggleIsoByIdAsync
|
|
|
|
|
|
// POST /participants/iso → ToggleIsoByNameAsync
|
|
|
|
|
|
// POST /participants/{id}/override → SetIsoOverrideByIdAsync
|
|
|
|
|
|
// DELETE /participants/{id}/override → ClearIsoOverrideByIdAsync
|
refactor(control-surface): split server into endpoint partials
ControlSurfaceServer.cs was 1061 lines / 47KB — a single class hosting
the HttpListener loop, the route dispatch, and every endpoint body in
between. Splits the class via partial-class into a thin host file plus
one partial per route group, all under Services/ControlSurface/.
* Services/ControlSurfaceServer.cs (was 1061L → now 400L) — kept here:
Start / Stop / DisposeAsync (the listener lifecycle), AcceptLoopAsync,
HandleRequestAsync (the route table itself, with its CORS preflight +
WebSocket upgrade + JSON dispatch), the response helpers
(ReadBodyAsync / WriteJsonAsync / TryGetBool / TryGetString), the
NotFound switch-arm, and the JsonSerializerOptions singleton.
* Services/ControlSurface/Endpoints/HomeEndpoints.cs — GetServerInfo,
TryRead helper.
* Services/ControlSurface/Endpoints/ParticipantsEndpoints.cs (the
biggest split) — GetParticipants, SetIsoOverrideByIdAsync,
ClearIsoOverrideByIdAsync, TryParseEnum, ToggleIsoByIdAsync,
ToggleIsoByNameAsync, ToggleByIdAsync. Together: every /participants/*
handler.
* Services/ControlSurface/Endpoints/PresetsEndpoints.cs — RefreshDiscovery,
StopAllAsync, ApplyPresetAsync.
* Services/ControlSurface/Endpoints/TeamsEndpoints.cs — InvokeTeams
(the helper that maps a TeamsControlBridge result to the JSON body).
* Services/ControlSurface/Endpoints/TopologyEndpoints.cs — GetTopology,
ApplyTopologyAsync, RestoreTopologyAsync.
* Services/ControlSurface/Endpoints/NotesEndpoints.cs — AppendNote.
* Services/ControlSurface/Endpoints/ThumbnailEndpoint.cs —
TryEncodeThumbnailJpeg (which is actually the BMP path now) +
EncodeBmpDownscaled + the LE byte writers. The legacy
TryEncodeThumbnailJpeg_WpfDeadCode helper that was dead-coded "for
posterity" is gone — no call sites; we removed-comments-on-removed-
code is the anti-pattern we wanted to fix.
* Services/ControlSurface/WebSocketHub.cs — HandleWebSocketAsync,
PushSnapshotIfChangedAsync, SendAsync, GetSnapshotJsonAsync. The
push-timer wiring stays in the host's Start() so the lifetime is
obvious where the connection is opened.
No behavior change. The route table in HandleRequestAsync still
dispatches by (HttpMethod, path) — only the handler bodies moved.
Build clean; 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:48:03 -04:00
|
|
|
|
public sealed partial class ControlSurfaceServer
|
|
|
|
|
|
{
|
|
|
|
|
|
private object GetParticipants()
|
|
|
|
|
|
{
|
|
|
|
|
|
var vm = _viewModel();
|
|
|
|
|
|
if (vm is null) return new { participants = Array.Empty<object>() };
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// Synchronously snapshot on the UI thread — ObservableCollection
|
refactor(control-surface): split server into endpoint partials
ControlSurfaceServer.cs was 1061 lines / 47KB — a single class hosting
the HttpListener loop, the route dispatch, and every endpoint body in
between. Splits the class via partial-class into a thin host file plus
one partial per route group, all under Services/ControlSurface/.
* Services/ControlSurfaceServer.cs (was 1061L → now 400L) — kept here:
Start / Stop / DisposeAsync (the listener lifecycle), AcceptLoopAsync,
HandleRequestAsync (the route table itself, with its CORS preflight +
WebSocket upgrade + JSON dispatch), the response helpers
(ReadBodyAsync / WriteJsonAsync / TryGetBool / TryGetString), the
NotFound switch-arm, and the JsonSerializerOptions singleton.
* Services/ControlSurface/Endpoints/HomeEndpoints.cs — GetServerInfo,
TryRead helper.
* Services/ControlSurface/Endpoints/ParticipantsEndpoints.cs (the
biggest split) — GetParticipants, SetIsoOverrideByIdAsync,
ClearIsoOverrideByIdAsync, TryParseEnum, ToggleIsoByIdAsync,
ToggleIsoByNameAsync, ToggleByIdAsync. Together: every /participants/*
handler.
* Services/ControlSurface/Endpoints/PresetsEndpoints.cs — RefreshDiscovery,
StopAllAsync, ApplyPresetAsync.
* Services/ControlSurface/Endpoints/TeamsEndpoints.cs — InvokeTeams
(the helper that maps a TeamsControlBridge result to the JSON body).
* Services/ControlSurface/Endpoints/TopologyEndpoints.cs — GetTopology,
ApplyTopologyAsync, RestoreTopologyAsync.
* Services/ControlSurface/Endpoints/NotesEndpoints.cs — AppendNote.
* Services/ControlSurface/Endpoints/ThumbnailEndpoint.cs —
TryEncodeThumbnailJpeg (which is actually the BMP path now) +
EncodeBmpDownscaled + the LE byte writers. The legacy
TryEncodeThumbnailJpeg_WpfDeadCode helper that was dead-coded "for
posterity" is gone — no call sites; we removed-comments-on-removed-
code is the anti-pattern we wanted to fix.
* Services/ControlSurface/WebSocketHub.cs — HandleWebSocketAsync,
PushSnapshotIfChangedAsync, SendAsync, GetSnapshotJsonAsync. The
push-timer wiring stays in the host's Start() so the lifetime is
obvious where the connection is opened.
No behavior change. The route table in HandleRequestAsync still
dispatches by (HttpMethod, path) — only the handler bodies moved.
Build clean; 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:48:03 -04:00
|
|
|
|
// isn't safe to enumerate from this request handler's thread-pool
|
|
|
|
|
|
// task, and the ParticipantViewModel property reads chase
|
|
|
|
|
|
// data-binding state.
|
|
|
|
|
|
var dispatcher = System.Windows.Application.Current?.Dispatcher;
|
|
|
|
|
|
if (dispatcher is null) return new { participants = Array.Empty<object>() };
|
|
|
|
|
|
var globals = _controller.GlobalSettings;
|
|
|
|
|
|
var list = dispatcher.Invoke(() => vm.Participants.Select(p => {
|
|
|
|
|
|
var ovr = _controller.GetIsoOverride(p.Id);
|
|
|
|
|
|
return (object)new
|
|
|
|
|
|
{
|
|
|
|
|
|
id = p.Id,
|
|
|
|
|
|
displayName = p.DisplayName,
|
|
|
|
|
|
isOnline = p.IsOnline,
|
|
|
|
|
|
isEnabled = p.IsEnabled,
|
|
|
|
|
|
customName = string.IsNullOrEmpty(p.CustomName) ? null : p.CustomName,
|
|
|
|
|
|
stateLabel = p.StateLabel,
|
|
|
|
|
|
// Effective settings = override if set, else globals. The
|
|
|
|
|
|
// web UI uses this to show the current per-row values
|
|
|
|
|
|
// without a separate round-trip to /global.
|
|
|
|
|
|
effective = new
|
|
|
|
|
|
{
|
|
|
|
|
|
framerate = (ovr ?? globals).Framerate.ToString(),
|
|
|
|
|
|
resolution = (ovr ?? globals).Resolution.ToString(),
|
|
|
|
|
|
aspect = (ovr ?? globals).Aspect.ToString(),
|
|
|
|
|
|
audio = (ovr ?? globals).Audio.ToString(),
|
|
|
|
|
|
isOverride = ovr is not null,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}).ToArray());
|
|
|
|
|
|
return new { participants = list, globals = new {
|
|
|
|
|
|
framerate = globals.Framerate.ToString(),
|
|
|
|
|
|
resolution = globals.Resolution.ToString(),
|
|
|
|
|
|
aspect = globals.Aspect.ToString(),
|
|
|
|
|
|
audio = globals.Audio.ToString(),
|
|
|
|
|
|
} };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-05-31 11:18:27 -04:00
|
|
|
|
/// POST /participants/{id}/override — set or replace the per-pipeline
|
refactor(control-surface): split server into endpoint partials
ControlSurfaceServer.cs was 1061 lines / 47KB — a single class hosting
the HttpListener loop, the route dispatch, and every endpoint body in
between. Splits the class via partial-class into a thin host file plus
one partial per route group, all under Services/ControlSurface/.
* Services/ControlSurfaceServer.cs (was 1061L → now 400L) — kept here:
Start / Stop / DisposeAsync (the listener lifecycle), AcceptLoopAsync,
HandleRequestAsync (the route table itself, with its CORS preflight +
WebSocket upgrade + JSON dispatch), the response helpers
(ReadBodyAsync / WriteJsonAsync / TryGetBool / TryGetString), the
NotFound switch-arm, and the JsonSerializerOptions singleton.
* Services/ControlSurface/Endpoints/HomeEndpoints.cs — GetServerInfo,
TryRead helper.
* Services/ControlSurface/Endpoints/ParticipantsEndpoints.cs (the
biggest split) — GetParticipants, SetIsoOverrideByIdAsync,
ClearIsoOverrideByIdAsync, TryParseEnum, ToggleIsoByIdAsync,
ToggleIsoByNameAsync, ToggleByIdAsync. Together: every /participants/*
handler.
* Services/ControlSurface/Endpoints/PresetsEndpoints.cs — RefreshDiscovery,
StopAllAsync, ApplyPresetAsync.
* Services/ControlSurface/Endpoints/TeamsEndpoints.cs — InvokeTeams
(the helper that maps a TeamsControlBridge result to the JSON body).
* Services/ControlSurface/Endpoints/TopologyEndpoints.cs — GetTopology,
ApplyTopologyAsync, RestoreTopologyAsync.
* Services/ControlSurface/Endpoints/NotesEndpoints.cs — AppendNote.
* Services/ControlSurface/Endpoints/ThumbnailEndpoint.cs —
TryEncodeThumbnailJpeg (which is actually the BMP path now) +
EncodeBmpDownscaled + the LE byte writers. The legacy
TryEncodeThumbnailJpeg_WpfDeadCode helper that was dead-coded "for
posterity" is gone — no call sites; we removed-comments-on-removed-
code is the anti-pattern we wanted to fix.
* Services/ControlSurface/WebSocketHub.cs — HandleWebSocketAsync,
PushSnapshotIfChangedAsync, SendAsync, GetSnapshotJsonAsync. The
push-timer wiring stays in the host's Start() so the lifetime is
obvious where the connection is opened.
No behavior change. The route table in HandleRequestAsync still
dispatches by (HttpMethod, path) — only the handler bodies moved.
Build clean; 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:48:03 -04:00
|
|
|
|
/// override. Body fields: framerate (enum string), resolution (enum
|
|
|
|
|
|
/// string), aspect (enum string), audio (enum string). All fields are
|
|
|
|
|
|
/// optional; missing fields fall back to the current global value.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<object> SetIsoOverrideByIdAsync(string path, JsonElement body)
|
|
|
|
|
|
{
|
|
|
|
|
|
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
if (segments.Length != 3 || segments[0] != "participants" || segments[2] != "override")
|
|
|
|
|
|
return new { ok = false, error = "expected /participants/{id}/override" };
|
|
|
|
|
|
if (!Guid.TryParse(segments[1], out var id))
|
|
|
|
|
|
return new { ok = false, error = "invalid id" };
|
|
|
|
|
|
|
|
|
|
|
|
var g = _controller.GlobalSettings;
|
|
|
|
|
|
var framerate = TryParseEnum(body, "framerate", g.Framerate);
|
|
|
|
|
|
var resolution = TryParseEnum(body, "resolution", g.Resolution);
|
|
|
|
|
|
var aspect = TryParseEnum(body, "aspect", g.Aspect);
|
|
|
|
|
|
var audio = TryParseEnum(body, "audio", g.Audio);
|
|
|
|
|
|
var ovr = new FrameProcessingSettings(framerate, resolution, aspect, audio);
|
|
|
|
|
|
await _controller.SetIsoOverrideAsync(id, ovr, CancellationToken.None);
|
|
|
|
|
|
return new { ok = true, id, effective = new
|
|
|
|
|
|
{
|
|
|
|
|
|
framerate = ovr.Framerate.ToString(),
|
|
|
|
|
|
resolution = ovr.Resolution.ToString(),
|
|
|
|
|
|
aspect = ovr.Aspect.ToString(),
|
|
|
|
|
|
audio = ovr.Audio.ToString(),
|
|
|
|
|
|
isOverride = true,
|
|
|
|
|
|
} };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 11:18:27 -04:00
|
|
|
|
/// <summary>DELETE /participants/{id}/override — pipeline reverts to global settings.</summary>
|
refactor(control-surface): split server into endpoint partials
ControlSurfaceServer.cs was 1061 lines / 47KB — a single class hosting
the HttpListener loop, the route dispatch, and every endpoint body in
between. Splits the class via partial-class into a thin host file plus
one partial per route group, all under Services/ControlSurface/.
* Services/ControlSurfaceServer.cs (was 1061L → now 400L) — kept here:
Start / Stop / DisposeAsync (the listener lifecycle), AcceptLoopAsync,
HandleRequestAsync (the route table itself, with its CORS preflight +
WebSocket upgrade + JSON dispatch), the response helpers
(ReadBodyAsync / WriteJsonAsync / TryGetBool / TryGetString), the
NotFound switch-arm, and the JsonSerializerOptions singleton.
* Services/ControlSurface/Endpoints/HomeEndpoints.cs — GetServerInfo,
TryRead helper.
* Services/ControlSurface/Endpoints/ParticipantsEndpoints.cs (the
biggest split) — GetParticipants, SetIsoOverrideByIdAsync,
ClearIsoOverrideByIdAsync, TryParseEnum, ToggleIsoByIdAsync,
ToggleIsoByNameAsync, ToggleByIdAsync. Together: every /participants/*
handler.
* Services/ControlSurface/Endpoints/PresetsEndpoints.cs — RefreshDiscovery,
StopAllAsync, ApplyPresetAsync.
* Services/ControlSurface/Endpoints/TeamsEndpoints.cs — InvokeTeams
(the helper that maps a TeamsControlBridge result to the JSON body).
* Services/ControlSurface/Endpoints/TopologyEndpoints.cs — GetTopology,
ApplyTopologyAsync, RestoreTopologyAsync.
* Services/ControlSurface/Endpoints/NotesEndpoints.cs — AppendNote.
* Services/ControlSurface/Endpoints/ThumbnailEndpoint.cs —
TryEncodeThumbnailJpeg (which is actually the BMP path now) +
EncodeBmpDownscaled + the LE byte writers. The legacy
TryEncodeThumbnailJpeg_WpfDeadCode helper that was dead-coded "for
posterity" is gone — no call sites; we removed-comments-on-removed-
code is the anti-pattern we wanted to fix.
* Services/ControlSurface/WebSocketHub.cs — HandleWebSocketAsync,
PushSnapshotIfChangedAsync, SendAsync, GetSnapshotJsonAsync. The
push-timer wiring stays in the host's Start() so the lifetime is
obvious where the connection is opened.
No behavior change. The route table in HandleRequestAsync still
dispatches by (HttpMethod, path) — only the handler bodies moved.
Build clean; 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:48:03 -04:00
|
|
|
|
private async Task<object> ClearIsoOverrideByIdAsync(string path)
|
|
|
|
|
|
{
|
|
|
|
|
|
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
if (segments.Length != 3 || segments[0] != "participants" || segments[2] != "override")
|
|
|
|
|
|
return new { ok = false, error = "expected /participants/{id}/override" };
|
|
|
|
|
|
if (!Guid.TryParse(segments[1], out var id))
|
|
|
|
|
|
return new { ok = false, error = "invalid id" };
|
|
|
|
|
|
await _controller.SetIsoOverrideAsync(id, null, CancellationToken.None);
|
|
|
|
|
|
return new { ok = true, id, cleared = true };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Parse an enum value from a JSON body, falling back to a default when
|
|
|
|
|
|
/// the field is missing or the value doesn't match any enum member.
|
|
|
|
|
|
/// Case-insensitive. Used by SetIsoOverrideByIdAsync for the four
|
|
|
|
|
|
/// FrameProcessingSettings enums.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static TEnum TryParseEnum<TEnum>(JsonElement body, string field, TEnum fallback)
|
|
|
|
|
|
where TEnum : struct, Enum
|
|
|
|
|
|
{
|
|
|
|
|
|
if (body.ValueKind != JsonValueKind.Object) return fallback;
|
|
|
|
|
|
if (!body.TryGetProperty(field, out var prop)) return fallback;
|
|
|
|
|
|
if (prop.ValueKind != JsonValueKind.String) return fallback;
|
|
|
|
|
|
var s = prop.GetString();
|
|
|
|
|
|
if (string.IsNullOrEmpty(s)) return fallback;
|
|
|
|
|
|
return Enum.TryParse<TEnum>(s, ignoreCase: true, out var result) ? result : fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<object> ToggleIsoByIdAsync(string path, JsonElement body, NameValueCollection query)
|
|
|
|
|
|
{
|
|
|
|
|
|
// path = /participants/<guid>/iso
|
|
|
|
|
|
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
if (segments.Length != 3 || segments[0] != "participants" || segments[2] != "iso")
|
|
|
|
|
|
return NotFound();
|
|
|
|
|
|
if (!Guid.TryParse(segments[1], out var id))
|
|
|
|
|
|
return new { ok = false, error = "invalid id" };
|
|
|
|
|
|
return await ToggleByIdAsync(id, body, query);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<object> ToggleIsoByNameAsync(JsonElement body, NameValueCollection query)
|
|
|
|
|
|
{
|
|
|
|
|
|
var displayName = TryGetString(body, query, "displayName");
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(displayName))
|
|
|
|
|
|
return new { ok = false, error = "displayName required" };
|
|
|
|
|
|
var vm = _viewModel();
|
|
|
|
|
|
var dispatcher = System.Windows.Application.Current?.Dispatcher;
|
|
|
|
|
|
if (vm is null || dispatcher is null)
|
|
|
|
|
|
return new { ok = false, error = "view-model not ready" };
|
|
|
|
|
|
var p = await dispatcher.InvokeAsync(() => vm.Participants.FirstOrDefault(x =>
|
|
|
|
|
|
string.Equals(x.DisplayName, displayName, StringComparison.OrdinalIgnoreCase)));
|
|
|
|
|
|
if (p is null) return new { ok = false, error = "participant not found", displayName };
|
|
|
|
|
|
return await ToggleByIdAsync(p.Id, body, query);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<object> ToggleByIdAsync(Guid id, JsonElement body, NameValueCollection query)
|
|
|
|
|
|
{
|
|
|
|
|
|
var enabled = TryGetBool(body, query, "enabled");
|
|
|
|
|
|
var customName = TryGetString(body, query, "customName");
|
|
|
|
|
|
var vm = _viewModel();
|
|
|
|
|
|
var dispatcher = System.Windows.Application.Current?.Dispatcher;
|
|
|
|
|
|
if (vm is null || dispatcher is null)
|
|
|
|
|
|
return new { ok = false, error = "view-model not ready" };
|
|
|
|
|
|
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// Look up the VM and snapshot its current state on the UI thread —
|
refactor(control-surface): split server into endpoint partials
ControlSurfaceServer.cs was 1061 lines / 47KB — a single class hosting
the HttpListener loop, the route dispatch, and every endpoint body in
between. Splits the class via partial-class into a thin host file plus
one partial per route group, all under Services/ControlSurface/.
* Services/ControlSurfaceServer.cs (was 1061L → now 400L) — kept here:
Start / Stop / DisposeAsync (the listener lifecycle), AcceptLoopAsync,
HandleRequestAsync (the route table itself, with its CORS preflight +
WebSocket upgrade + JSON dispatch), the response helpers
(ReadBodyAsync / WriteJsonAsync / TryGetBool / TryGetString), the
NotFound switch-arm, and the JsonSerializerOptions singleton.
* Services/ControlSurface/Endpoints/HomeEndpoints.cs — GetServerInfo,
TryRead helper.
* Services/ControlSurface/Endpoints/ParticipantsEndpoints.cs (the
biggest split) — GetParticipants, SetIsoOverrideByIdAsync,
ClearIsoOverrideByIdAsync, TryParseEnum, ToggleIsoByIdAsync,
ToggleIsoByNameAsync, ToggleByIdAsync. Together: every /participants/*
handler.
* Services/ControlSurface/Endpoints/PresetsEndpoints.cs — RefreshDiscovery,
StopAllAsync, ApplyPresetAsync.
* Services/ControlSurface/Endpoints/TeamsEndpoints.cs — InvokeTeams
(the helper that maps a TeamsControlBridge result to the JSON body).
* Services/ControlSurface/Endpoints/TopologyEndpoints.cs — GetTopology,
ApplyTopologyAsync, RestoreTopologyAsync.
* Services/ControlSurface/Endpoints/NotesEndpoints.cs — AppendNote.
* Services/ControlSurface/Endpoints/ThumbnailEndpoint.cs —
TryEncodeThumbnailJpeg (which is actually the BMP path now) +
EncodeBmpDownscaled + the LE byte writers. The legacy
TryEncodeThumbnailJpeg_WpfDeadCode helper that was dead-coded "for
posterity" is gone — no call sites; we removed-comments-on-removed-
code is the anti-pattern we wanted to fix.
* Services/ControlSurface/WebSocketHub.cs — HandleWebSocketAsync,
PushSnapshotIfChangedAsync, SendAsync, GetSnapshotJsonAsync. The
push-timer wiring stays in the host's Start() so the lifetime is
obvious where the connection is opened.
No behavior change. The route table in HandleRequestAsync still
dispatches by (HttpMethod, path) — only the handler bodies moved.
Build clean; 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:48:03 -04:00
|
|
|
|
// ObservableCollection enumeration and view-model property reads
|
|
|
|
|
|
// both need to happen there.
|
|
|
|
|
|
var lookup = await dispatcher.InvokeAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = vm.Participants.FirstOrDefault(x => x.Id == id);
|
|
|
|
|
|
return p is null
|
|
|
|
|
|
? null
|
|
|
|
|
|
: new { Pvm = p, p.IsEnabled, p.CustomName };
|
|
|
|
|
|
});
|
|
|
|
|
|
if (lookup is null) return new { ok = false, error = "participant not found", id };
|
|
|
|
|
|
|
|
|
|
|
|
var target = enabled ?? !lookup.IsEnabled;
|
|
|
|
|
|
var nameToUse = !string.IsNullOrEmpty(customName) ? customName : lookup.CustomName;
|
|
|
|
|
|
|
|
|
|
|
|
if (target == lookup.IsEnabled && string.IsNullOrEmpty(customName))
|
|
|
|
|
|
return new { ok = true, id, enabled = lookup.IsEnabled, action = "noop" };
|
|
|
|
|
|
|
|
|
|
|
|
// Apply CustomName change first (if any) on the UI thread so a
|
|
|
|
|
|
// subsequent EnableIsoAsync sees the new name.
|
|
|
|
|
|
if (!string.IsNullOrEmpty(customName))
|
|
|
|
|
|
await dispatcher.InvokeAsync(() => lookup.Pvm.CustomName = customName);
|
|
|
|
|
|
|
|
|
|
|
|
if (target)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _controller.EnableIsoAsync(id,
|
|
|
|
|
|
string.IsNullOrWhiteSpace(nameToUse) ? null : nameToUse,
|
|
|
|
|
|
CancellationToken.None);
|
|
|
|
|
|
await dispatcher.InvokeAsync(() => lookup.Pvm.IsEnabled = true);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
await _controller.DisableIsoAsync(id, CancellationToken.None);
|
|
|
|
|
|
await dispatcher.InvokeAsync(() => lookup.Pvm.IsEnabled = false);
|
|
|
|
|
|
}
|
|
|
|
|
|
return new { ok = true, id, enabled = target };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|