The support bundle gives each printer's connection diagnostic 15 s and
discarded the whole result on overrun, recording only "timed_out". A
14-printer farm's bundle carried that marker for every printer and
nothing else, so it could not say which check was slow.
run_connection_diagnostic now keeps an optional progress dict current
(finished checks + the step in flight). On timeout the snapshot records
stalled_in, elapsed_s and the checks that completed.
The three diagnostic surfaces shipped earlier this month
(6bc6a1d6 VP setup diagnostic, e222a0ef log-health scanner,
ed31b8f4 connection diagnostic in the bug-report bubble) were
only ever shown to the *user*. A bug report arriving in the
maintainer's inbox carried raw logs but no diagnostic results —
the user-visible "your X1C can't reach MQTT" finding never made
it into the issue body, so the maintainer had to ask the user
to re-run and paste.
New `services/diagnostic_snapshot.collect_diagnostic_snapshot`
runs all three concurrently with a per-probe 15 s wall-clock cap
(so total ≈ max(per-cap), not sum — fleet size doesn't matter)
and is fail-soft per probe: a crash inside one printer's check
emits `{"printer_id": N, "error": "..."}` for that entry rather
than nuking the whole snapshot. The snapshot is then added as a
`diagnostics` top-level key by `_collect_support_info()`, so both
flows (POST /support/bundle and POST /bug-report/submit via
`support_info=...`) pick it up without their own changes.
Private-data sanitization
-------------------------
The diagnostic schemas embed raw IPv4 in five field shapes that
must not land in a submitted GitHub issue or a shared support ZIP:
- PrinterDiagnosticResult.ip_address (top-level)
- DiagnosticCheck.params.printer_ip (network-mode check)
- DiagnosticCheck.params.host_ip (network-mode check)
- VPDiagnosticResult per-check params.bind_ip (VP setup)
- IPs embedded in log-health sample lines
The first two carry the printer's own IP (already in the
existing `collect_sensitive_strings` table via the Printer rows);
host_ip and bind_ip are NOT in the DB so a sensitive_strings-only
pass missed them.
Fix: `_sanitize_recursive` walks the full snapshot tree, masks
DB-known values with the same `[PRINTER]/[IP]/[SERIAL]/[ACCESS_CODE]`
labels the log sanitizer applies (via the shared
`collect_sensitive_strings`), then an IPv4-regex pass catches any
IP the DB didn't cover — most importantly the Bambuddy host IP
returned by `_get_host_ip()` and the VP `bind_ip` the user picked
at setup. Recursive walk so arbitrary nested dicts/lists don't
slip through future schema additions.
Live-DB smoke test against the dev fleet: zero raw IPv4 instances
in the serialized snapshot output; all five field shapes plus the
embedded log samples render as `[IP]`.
Progress indicators
-------------------
The bubble's "submitting" view and the System page's Download
button now render a static four-line checklist showing what's
running (printer connectivity → VP setup → log scan →
submit / build ZIP). Static, not faked phase progress — we can't
actually track server-side phases without SSE and the honest
"here's what's happening" list communicates the longer wait
without lying about percentage complete.
9 new i18n keys, real translations in all 9 locales (no English
fallback). parity script clean at 4993 leaves per locale.
Tests: 6 new in test_diagnostic_snapshot.py
- empty-input shape stable (the three top-level keys always present)
- per-printer / per-VP result coverage (lists match input lengths)
- fail-soft on a single-probe crash (other entries + log-health
still complete)
- timed_out marker when a probe exceeds the per-probe cap
(test patches the cap to 0.05 s)
- end-to-end IP sanitization across all five field shapes plus
log-sample IPs, with a final JSON-serialize-and-regex sweep
asserting zero raw IPv4 escapes anywhere in the result
- concurrent execution proof (4 × 0.2 s probes complete in
< 0.5 s; sequential would be 0.8 s)