Skip to content

feat(desktop): upload speed test and health check for Instant mode - #2322

Open
stffinfcti wants to merge 3 commits into
CapSoftware:mainfrom
stffinfcti:bounty/73-upload-health-check
Open

stffinfcti wants to merge 3 commits into
CapSoftware:mainfrom
stffinfcti:bounty/73-upload-health-check

Conversation

@stffinfcti

@stffinfcti stffinfcti commented Sep 20, 2026

Copy link
Copy Markdown

Summary

Implements the speed test and health check from #73 for the Tauri v2 desktop app.

  • Startup health check: on launch the app POSTs a 512 KiB probe to a new authenticated web route, POST /api/desktop/upload-health, which streams and counts request bytes (hard-capped at 1 MiB) and returns receivedBytes. Nothing is written to object storage, so probes leave no orphaned files. GET on the same route covers reachability/auth. A 404 is treated as "endpoint not deployed" rather than a failure.
  • Upload speed → quality gating: measured throughput is cached for 10 minutes and mapped to a max capture width (<4 Mbps → 1280, <10 → 1920, <25 → 2560, otherwise uncapped). start_recording applies min(user setting, recommended) for Instant mode only.
  • Never blocks or races recording: recording start only reads the cached result — it never launches a probe — and an in-flight probe is aborted as soon as recording becomes pending. Probes are single-flight, and failures preserve the last measured speed instead of leaving a stuck state.
  • UI: a small UploadHealthIndicator in the main window shows checking / healthy / degraded / failed with the measured Mbps and a manual re-check.

Why this approach

Existing submissions leave gaps this avoids: probes awaited inline before the recording UI appears (stalling Instant mode for the full probe timeout), payloads written through signed S3 URLs that orphan objects in storage, unauthenticated probe endpoints accepting arbitrary bodies, recording-state flags that stay stuck after a failed stop, and in-flight probes that keep running after recording starts. Here the probe only ever runs in the background against a dedicated byte-counting route — authenticated, bounded, no storage writes — and recording start is a pure cache read plus cancellation.

Testing

  • cargo test -p cap-desktop upload_health --lib — 3 passed (tier mapping, checking-state reporting, failure preserving last measured speed)
  • bunx biome check on all touched TypeScript/TSX files — clean
  • cargo fmt --all --check — clean

@algora-pbc /claim #73

Closes #73

Bounty payout: @stffinfcti

RetriggerConfidence Score: 3/5

This PR is not safe to merge until probe startup is synchronized with recording admission, retained measurements continue to gate quality after transient failures, and the explicit repository requirements are satisfied.

Findings

  1. P1 Probe can outlive cancellation
  2. P1 Failures disable cached cap
  3. P2 Component filename violates convention
  4. P2 Route bypasses required pattern
Fix with agent prompt
### Issue 1
apps/desktop/src-tauri/src/upload_health.rs:205-210
`begin_probe` releases the app read lock after checking the recording state, then installs the probe under a separate lock. If a recording becomes pending between those operations, `recording_started` finds no probe to cancel, and the later active transition does not retry cancellation. The 512 KiB probe can therefore run during an Instant recording and compete with its live upload.

### Issue 2
apps/desktop/src-tauri/src/upload_health.rs:326-331
A failed recheck retains the last measured `upload_mbps`, but changes the state to `failed` and marks the result fresh for ten minutes. This branch only accepts `healthy` or `degraded`, so a transient failure prevents that retained slow measurement from limiting subsequent Instant recordings. Those recordings can use the user's higher resolution until another probe runs or the cache expires.

### Issue 3
apps/desktop/src/components/UploadHealthIndicator.tsx:1
The repository naming directive requires TypeScript component files to use kebab-case while keeping the component symbol PascalCase. This new file is named `UploadHealthIndicator.tsx`; it must be renamed and its import updated before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 4
apps/web/app/api/desktop/[...route]/upload-health.ts:1-35
The repository directive requires new Next.js endpoints under `apps/web/app/api/*` to use the `@effect/platform` `HttpApi` builder and prohibits ad-hoc handlers. This endpoint introduces a standalone Hono handler instead, so it must be moved to the prescribed API group and layer pattern before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds an authenticated upload-health endpoint, a desktop upload-speed monitor and status indicator, and throughput-based Instant recording resolution caps.

  • Adds startup and manually triggered 512 KiB upload probes with cached health events.
  • Applies measured upload tiers to Instant recording output width.
  • Adds desktop UI for checking, degraded, and failed upload states.
  • The probe-start transition has a recording race, and failed rechecks unintentionally disable use of the retained speed measurement.
  • Two new files do not follow explicit repository conventions.

Reviews (1) · Last reviewed commit: "fix(desktop): revert bindings drift, add..."

Notdevng and others added 2 commits September 20, 2026 06:57
Adds a small status indicator in the bottom-right of the main window that
measures upload throughput and reports upload health:

- New authed endpoint POST /api/desktop/upload-health accepts a bounded
  (<=1MiB) probe body and echoes receivedBytes so the client can verify
  the upload was not truncated; GET acts as a liveness check.
- A desktop UploadHealthMonitor probes on startup and on demand,
  measures Mbps from a 512KiB upload (minus one measured RTT), and emits
  uploadHealthChanged events to the frontend.
- Probes never run during a recording: a fresh probe refuses to start
  while recording is pending/active and any in-flight probe is aborted
  the moment recording begins.
- Instant mode recordings cap their max output width by the measured
  upload speed (<4 Mbps -> 1280, <10 -> 1920, <25 -> 2560) when a fresh
  measurement exists.
- On failure the indicator flags the problem and links to support.

Closes CapSoftware#73
- restore macOS SystemDiagnostics/MacOSVersionInfo in the committed
  bindings; they had drifted to the Linux shape after regeneration
- a 404 from /api/desktop/upload-health now maps to the hidden 'unknown'
  state so older self-hosted web builds don't raise a false alarm
- wire refresh_upload_health to a retry button on the failed indicator

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Comment on lines +205 to +210
if recording_in_progress(app).await {
return false;
}
match crate::auth::AuthStore::get(app) {
Ok(Some(_)) => monitor.start_probe(app),
_ => false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Probe can outlive cancellation

begin_probe releases the app read lock after checking the recording state, then installs the probe under a separate lock. If a recording becomes pending between those operations, recording_started finds no probe to cancel, and the later active transition does not retry cancellation. The 512 KiB probe can therefore run during an Instant recording and compete with its live upload.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/upload_health.rs
Line: 205-210

Comment:
**Probe can outlive cancellation**

`begin_probe` releases the app read lock after checking the recording state, then installs the probe under a separate lock. If a recording becomes pending between those operations, `recording_started` finds no probe to cancel, and the later active transition does not retry cancellation. The 512 KiB probe can therefore run during an Instant recording and compete with its live upload.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +326 to +331
match inner.status.state {
UploadHealthState::Healthy | UploadHealthState::Degraded => {
inner.status.upload_mbps.and_then(recommended_max_width)
}
_ => None,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Failures disable cached cap

A failed recheck retains the last measured upload_mbps, but changes the state to failed and marks the result fresh for ten minutes. This branch only accepts healthy or degraded, so a transient failure prevents that retained slow measurement from limiting subsequent Instant recordings. Those recordings can use the user's higher resolution until another probe runs or the cache expires.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/upload_health.rs
Line: 326-331

Comment:
**Failures disable cached cap**

A failed recheck retains the last measured `upload_mbps`, but changes the state to `failed` and marks the result fresh for ten minutes. This branch only accepts `healthy` or `degraded`, so a transient failure prevents that retained slow measurement from limiting subsequent Instant recordings. Those recordings can use the user's higher resolution until another probe runs or the cache expires.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@@ -0,0 +1,130 @@
import * as shell from "@tauri-apps/plugin-shell";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Component filename violates convention

The repository naming directive requires TypeScript component files to use kebab-case while keeping the component symbol PascalCase. This new file is named UploadHealthIndicator.tsx; it must be renamed and its import updated before merging.

Context Used: CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/components/UploadHealthIndicator.tsx
Line: 1

Comment:
**Component filename violates convention**

The repository naming directive requires TypeScript component files to use kebab-case while keeping the component symbol PascalCase. This new file is named `UploadHealthIndicator.tsx`; it must be renamed and its import updated before merging.

**Context Used:** CLAUDE.md ([source](https://github.com/capsoftware/cap/blob/main/CLAUDE.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +1 to +35
import { Hono } from "hono";
import { withAuth } from "../../utils";

export const app = new Hono().use(withAuth);

export const MAX_UPLOAD_PROBE_BYTES = 1024 * 1024;

app.get("/", (c) => c.json({ ok: true }));

app.post("/", async (c) => {
const contentLength = Number(c.req.header("content-length") ?? 0);
if (contentLength > MAX_UPLOAD_PROBE_BYTES)
return c.json({ error: "Probe payload too large" }, { status: 413 });

const body = c.req.raw.body;
if (body === null) return c.json({ receivedBytes: 0 });

let receivedBytes = 0;
const reader = body.getReader();
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
receivedBytes += value.byteLength;
if (receivedBytes > MAX_UPLOAD_PROBE_BYTES) {
await reader.cancel();
return c.json({ error: "Probe payload too large" }, { status: 413 });
}
}
} finally {
reader.releaseLock();
}

return c.json({ receivedBytes });
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Route bypasses required pattern

The repository directive requires new Next.js endpoints under apps/web/app/api/* to use the @effect/platform HttpApi builder and prohibits ad-hoc handlers. This endpoint introduces a standalone Hono handler instead, so it must be moved to the prescribed API group and layer pattern before merging.

Context Used: CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/api/desktop/[...route]/upload-health.ts
Line: 1-35

Comment:
**Route bypasses required pattern**

The repository directive requires new Next.js endpoints under `apps/web/app/api/*` to use the `@effect/platform` `HttpApi` builder and prohibits ad-hoc handlers. This endpoint introduces a standalone Hono handler instead, so it must be moved to the prescribed API group and layer pattern before merging.

**Context Used:** CLAUDE.md ([source](https://github.com/capsoftware/cap/blob/main/CLAUDE.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

- begin_probe: hold the App read lock across the recording check and probe
  install so a pending transition (which always runs recording_started under
  the write lock) can no longer slip between them and let a probe run into a
  recording
- instant_resolution_cap: a failed recheck retains the last measured speed,
  so keep enforcing it instead of letting a transient error lift the cap
- rename UploadHealthIndicator.tsx to upload-health-indicator.tsx per the
  kebab-case filename convention
- move /api/desktop/upload-health off the ad-hoc Hono handler onto the
  @effect/platform HttpApi group/layer pattern (DesktopApiContract +
  HttpApiBuilder + apiToHandler), preserving auth, the 413 byte cap, and
  early-abort streaming semantics

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement speed test and health check for Cap desktop app

1 participant