Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Home page restyled as a modular tile grid.** Each category card now sizes to its own content — single-link cards stay compact (one tile-unit) while data-heavy ones (Imaging Tools, Settings) span two units and line up with the single cards stacked beside them, instead of every card in a row stretching to match the tallest. The hero is slimmed to a horizontal band, card headers are left-aligned, and the grid is responsive (three roomy columns on a wide screen, dropping to two then one as the window narrows). Card spans are measured and snapped client-side (`wwwroot/js/home-tiles.js`) over a server-seeded estimate, so data-heavy cards size correctly with no first-paint flash. Colours come from the existing theme variables (light and dark).
- **Setup wizard "Scan Network" buttons unified** — the Devices and Cameras steps now use the same button (secondary style + `bi-broadcast` icon). Previously the Devices button used a different style and icon, so the two steps looked inconsistent.
- **The DI container is now validated by a test (internal, no behavior change).** The app's service registrations are extracted from `Program.cs` into an `AddControlMenuServices` extension so a unit test can build the real container with `ValidateScopes` + `ValidateOnBuild`. This catches the captive-dependency class of bug — a Singleton consuming a scoped service, exactly like the imaging-services regression below — at test time instead of at the first `dotnet run`. `Program.cs` simply calls the extension; startup is unchanged.
- **The Phone/Tablet/Watch dashboards are consolidated into one shared `DeviceDashboard` component (internal, no behavior change beyond the Watch fix below).** The three pages were ~95% identical; they are now thin route wrappers that render `DeviceDashboard` with their title/icon/device-type/device-kind, removing ~850 lines of duplication. The richer Google TV dashboard is a genuine superset and stays standalone.
- **Duplicated transient-message and imaging CSS are de-duplicated (internal).** A shared `TransientNotice` helper replaces the per-page status-message copies, and eight component-scoped `.spin`/`@keyframes spin` rules (the Imaging pages plus DatabaseUpdate and FileUnblocker) are removed in favour of the identical global rule in `app.css`. Camera-scan and subnet parsing now share one `SubnetMath` for IPv4 arithmetic.

### Fixed

Expand All @@ -25,6 +27,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **scrcpy device probes read the full response.** The probe deserialized only the first ≤8 KiB WebSocket frame, so a device reporting a long video/audio-encoder list returned truncated JSON and no probe data; the response is now accumulated across frames until end-of-message.
- **File logging survives disposal of a host's logger factory.** The Serilog provider was registered to dispose the process-global `Log.Logger`, so tearing a logger factory down (host shutdown, or a second host built in the same process) tore down live logging for every other holder. The configurator now owns the logger lifetime (the provider no longer disposes it) and flushes it on process exit.
- **A hung bundled-binary command can no longer block the UI indefinitely.** adb / magick / sqlite3 invocations carried no deadline, so a stuck child left the awaiting Blazor circuit hanging forever. They now run under a default 5-minute backstop (overridable per call) that kills the child process tree and returns a timeout result.
- **Settings status messages no longer disappear early.** The five Settings pages auto-dismissed their status message with an uncancelled timer, so showing a second message within the dismiss window let the earlier message's timer wipe the newer one. A shared `TransientNotice` helper now cancels the pending timer on each new message.
- **Camera scans can't start on top of each other.** `CameraScanService` used a non-atomic "if not already scanning, start scanning" check, so two near-simultaneous starts could both begin and clear each other's in-progress results; the check-and-set is now guarded by a lock.
- **Camera-scan subnet handling is correct at the edges and much faster.** Subnet membership was tested by enumerating every address in the subnet (O(subnet-size) per check), and the address enumerator's host-count math wrapped for a `/0` and silently produced nothing for `/31` and `/32`. A shared `SubnetMath` now does an O(1) mask/range compare and enumerates `/31`/`/32` correctly.
- **The Watch dashboard mirror uses the right device hint.** It passed `DeviceKind="phone"` to ws-scrcpy-web; it now correctly passes `"watch"`.

### Security

Expand Down
4 changes: 2 additions & 2 deletions docs/TECHNICAL_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ The module declares two dependencies — `adb` (auto-managed in `dependencies/`)

- **DeviceSelector** (`/android/devices`) -- CRUD for device records, network discovery for IP resolution
- **GoogleTvDashboard** (`/android/googletv`) -- Power toggle, screensaver selector, screen timeout, launcher enable/disable, Projectivy backup list with restore, Shizuku start, screen mirror (passes `DeviceKind="tv"` to `ScrcpyMirror` so the ws-scrcpy-web toolbar defaults to D-pad mode)
- **PixelDashboard** (`/android/phone`) -- ADB connect, PIN unlock, portrait-mode screen mirror with aspect ratio from ADB screen dimensions (passes `DeviceKind="phone"` to `ScrcpyMirror` so the ws-scrcpy-web toolbar defaults to Touch mode)
- **DeviceDashboard** (Phone `/android/phone` + `/android/pixel`, Tablet `/android/tablet`, Watch `/android/watch`) -- the shared dashboard for the simple single-mirror device pages; the three route pages are thin wrappers that render it with their title/icon/device-type/device-kind. ADB connect, PIN unlock, portrait-mode screen mirror with aspect ratio from ADB screen dimensions. The device kind (`phone`/`tablet`/`watch`) is passed to `ScrcpyMirror` so the ws-scrcpy-web toolbar seeds the right input mode.

### WsScrcpyService

Expand Down Expand Up @@ -579,7 +579,7 @@ go2rtc is auto-installable via the dependency manager. During updates, `Dependen

- `CameraServiceTests` (9 tests) -- CRUD operations, credential storage, camera count management
- `CamerasModuleTests` (5 tests) -- Module metadata, dynamic nav entry generation
- Scanner / network coverage: `CameraScanServiceTests`, `OnvifClientTests`, `RtspProbeClientTests`, `HikvisionIsapiClientTests`, `CameraLivenessHostedServiceTests`, `PurgeLegacyCameraSettingsMigrationTests`
- Scanner / network coverage: `CameraScanServiceTests`, `SubnetMathTests`, `OnvifClientTests`, `RtspProbeClientTests`, `HikvisionIsapiClientTests`, `CameraLivenessHostedServiceTests`, `PurgeLegacyCameraSettingsMigrationTests`

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,14 @@ public async Task StartScanAsync_ConcurrentStarts_StartExactlyOneScan()

var subnets = new[] { Subnet("192.168.1.0/24") };
var tasks = Enumerable.Range(0, 32).Select(_ => Task.Run(() => sut.StartScanAsync(subnets))).ToArray();
await Task.Delay(150); // let every start attempt the guard; losers return, the winner blocks on the gate

// Wait until a scan has actually begun — a fixed delay flaked on loaded CI runners where the
// Task.Run bodies hadn't been scheduled yet (started == 0). Then settle briefly: a non-atomic
// guard would have let several of the 32 slip past and increment by now, while the atomic guard
// holds the winner on the gate so the count stays at exactly one.
for (var i = 0; i < 400 && Volatile.Read(ref started) == 0; i++)
await Task.Delay(25);
await Task.Delay(100);

Assert.Equal(1, Volatile.Read(ref started));

Expand Down
Loading