* feat(setup): first-run install gate — nothing installs until the user confirms a plan New `setup` module parks first runs in BootstrapStage::AwaitingSetup instead of auto-installing. complete_setup validates the user's InstallPlan and only then starts the existing bootstrap: - install modes: installed (platform dirs) / portable (one folder next to the exe / AppImage, config.json travels with it) - user-chosen storage: env dir, data dir (OMNIVOICE_DATA_DIR), model cache (OMNIVOICE_CACHE_DIR) — None = legacy default, byte-identical behavior - minimum-space gate: per-volume free-space check (fs4 statvfs), grouped by filesystem so dirs sharing a disk sum their requirements; install refused when short (9 GiB env + 7 GiB models + 1 GiB data, measured + headroom) - custom mirrors (PyPI index, HF endpoint, python-build-standalone) take precedence over region presets in the venv/sync/backend env wiring - ROCm torch variant selectable via config (env var still wins) - existing installs migrate silently: venv present → setup_complete=true, no questions re-asked; dev trees skip the gate entirely 19 unit tests (disk probing, space grouping, mirror validation, legacy config compat). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): first-run setup screen — mode, storage with space gate, mirrors, compute FirstRunSetup renders when the Rust side reports awaiting_setup (lazy-loaded; regular launches pay nothing). One screen, defaults all work: - language picker first (rest re-renders translated), 21 locales shipped - Installed / Portable mode cards (portable disabled with reason when the exe-adjacent folder isn't writable) - storage rows with live per-path free-space probes (debounced check_install_target), 'needs ~X / Y free' readouts, folder pickers - client mirrors the Rust per-volume space gate: Start installation is disabled with an explicit reason until every volume fits - compute (CUDA-auto / ROCm), update channel, region + custom mirror URLs - complete_setup errors surface inline; on success the normal bootstrap progress UI takes over on the next status poll Verified on a wiped machine: gate parks (no spawn, no downloads), screen renders, 450 GB ≥ 17 GB requirement → Start enabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): studio-console redesign of the first-run screen The setup screen now reads as powering on studio hardware rather than a web form — true to a voice studio, and self-sufficient offline (every font and asset is bundled; a first run may be on a restricted network): - breathing waveform masthead (CSS-only, deterministic speech-cadence silhouette, staggered per-bar delays) - Source Serif 4 display headline + engraved IBM Plex Mono panel labels + Inter body — the three faces the app already ships - rack-unit panels with corner screws, engraved title rules, serial plate (OVS · vX.Y.Z) - disk space as segmented LED capacity meters: lit = what the install consumes, alarm-blink red on insufficient volumes - mode cards with indicator LEDs; 'armed' Start button — LED lights and a halo pulses only once every volume passes the space gate - atmosphere: corner accent glows + SVG film grain; staggered rise-in choreography on load - all motion transform/opacity only; prefers-reduced-motion holds every frame still; theme-token derived colors; focus-visible rings throughout No logic changes: same IPC calls, same i18n keys, same space-gate math. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): wide desktop deck, hardware-aware Compute + Update channel cards Three pieces of feedback addressed: - width: the console is now a 1240px two-column deck (storage rail left, decision rail right) that uses desktop real estate; collapses to one column under 980px and stacks fully under 620px - no outer chassis box: panels float directly on the atmospheric backdrop, each carrying its own rack-unit treatment - Compute and Update channel split into separate cards with real information: get_setup_state now detects hardware (nvidia-smi → CUDA name, /sys/class/drm vendor 0x1002 → AMD/ROCm, Apple Silicon → MPS, CPU cores + RAM via sysinfo; best-effort, never blocks) — the Compute card shows a live 'Detected: …' readout, badges the option that matches the machine, and pre-selects ROCm on AMD boxes; both cards use LED radio options with full descriptions (6 new i18n keys × 21 locales) Also pins playwright-core as an explicit devDep — bun did not materialize it through @playwright/test, breaking programmatic browser use. 20/20 Rust tests · vite build · CJK guard green. Verified live (gate engaged, responsive single-column) and at 1600×1000 via mocked-IPC browser shot (two-column deck). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): move network (region + mirrors) into the masthead with language Language and download region are the two 'where am I' choices — they now sit together top-right of the masthead, with the custom-mirrors disclosure tucked beneath the subtitle. The Network panel is gone, leaving a balanced deck: Install mode + Storage left, Compute + Update channel right. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): strip the boxes — fills and rules carry the structure One design rule now: borders only where state demands them. Panels lose their boxes entirely (engraved mono title + rule separates sections); option cards, storage rows, selects/inputs, the hw readout, the version plate and the ghost buttons are all flat fills; active options glow with an accent tint + LED; blocked rows and errors use a red tint + 2px inset edge bar instead of a border. The badge chip is fill-only too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): quiet pass — every element earns its visual weight - waveform becomes a whisper: 22px trace, 2px bars, ~half opacity — an ambient signature instead of a billboard - storage readouts collapse to one mono line ('needs ~9 GB · 449 GB free'); the LED meter now appears only when it carries information (install would consume >35% of free space, or the volume is blocked) — at 449 GB free a bar was a meaningless sliver - Change… buttons go text-quiet (transparent until hover) - custom-mirrors disclosure right-aligns under the region select it extends, instead of floating under the subtitle - version plate moves to the footer next to the disk total — the masthead keeps only title, subtitle, and the two locale/region selects Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): platform-matrix awareness — distro+arch detection, ROCm gated to Linux, no Windows console flash The install matrix is OS family × distro × arch × GPU vendor, and the setup screen now both shows it and only offers choices valid for it: - HardwareInfo gains os_name (distro PRETTY_NAME from /etc/os-release on Linux, macOS/Windows elsewhere) and arch (x86_64/aarch64) — the detected line reads 'CachyOS x86_64 · NVIDIA RTX 4070 · 32×CPU · 31 GB RAM', exactly what bug reports cite - SetupState gains os; the ROCm option renders on Linux only (wheels don't exist elsewhere) and complete_setup clamps rocm→auto on non-Linux as the server-side backstop - nvidia-smi probe gets CREATE_NO_WINDOW on Windows — no cmd flash on the first screen a user ever sees - Apple Silicon → MPS, Intel mac → CPU, ARM Linux → CPU: all matrix cells resolve through the same base constructor Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): unify the whole first-run journey under the studio-console system Setup → Installing → Model wizard now read as one continuous experience: the same atmosphere, whisper waveform masthead, serif/mono type, LED language and quiet fills across all three acts. - Installing (BootstrapSplash): rebuilt in frs-* — segmented LED journey meter (completed steps + live byte progress), LED step rail (done=green, active=pulsing accent, pending=dim), engraved ACTIVITY panel with the quiet mono log (collapse/copy as text-quiet actions), failure act with red-tint error + hints + armed Retry. All logic untouched: stage poll, event subscription + backfill, dedupe, hints, region/language selects. - Model wizard (SetupWizard): same masthead with the step rail as engraved mono LED steps top-right, welcome cards as option-card surfaces, preflight as LED check rows (pass/warn/fail), frs nav buttons with armed primaries, embedded Model Store / Engines / Dictation panels scroll inside the act. Old 556-line stylesheet replaced by ~60 lines of glue; BootstrapSplash.css reduced to a resolving stub. - FirstRunSetup.css is now the journey's shared design system (step rails, log panel, banners, hints, wizard chrome, check rows appended). - 2 new strings (Installing / Activity) translated across all 21 locales. Validated end-to-end on this machine: setup screen → Start installation → real venv bootstrap (~10 min) → backend healthy on 3900 → model wizard. 20/20 Rust tests · vite build · CJK guard green · installing act verified via mocked-IPC screenshot at stage=installing_deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): --setup re-entry flag + make the install-plan screen un-stealable The setup stage is first-run-only by design (completed installs skip it), but it must be reachable on demand and must actually win the mount when engaged. Three fixes: - 'omnivoice-studio --setup' parks the bootstrap in AwaitingSetup on any launch — checked before the attach-to-healthy-backend shortcut, so a running backend can't skip past it - App routing: awaiting_setup now outranks everything (a live backend answering /setup/status used to route straight to the model wizard); the wizard additionally requires stage === 'ready' so it can't mount during the initial stage race - useBootstrapStage: a transient IPC miss no longer permanently declares 'ready' (which killed the poll loop and silently skipped the setup / progress screens) — it retries up to 5 ticks before conceding Plus journey-wide titlebar clearance (content never sits under the GTK headerbar / macOS traffic lights / Windows controls) and drag-region mastheads on all three acts. Verified: mocked-IPC harness with stage=awaiting_setup + a LIVE backend answering /setup/status renders the setup screen, not the wizard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(setup): remove backdrop decoration — flat surface, state-only emphasis The corner accent glows and SVG film grain rendered as visible banding / noise artifacts on many panels — both gone; the journey now sits on a clean flat chrome background. Also swept the remaining decorative bloom: the active option card drops its glow shadow (flat accent tint + LED carry the state), and the armed Start button loses its pulsing halo (the lit LED already signals actionable). Remaining shadows are functional micro-detail only: 6px LED glows, meter track inset, red edge bars. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): journey rail + verbosity diet — clean, smooth, elegant The setup page is now visibly stage 1 of the install flow: a quiet breadcrumb rail (SETUP → INSTALLING → MODELS & ENGINES) sits between the waveform and the headline on both the setup and installing acts, LEDs marking done/active/pending — one continuous story across the journey. Verbosity halved without hiding information: - option descriptions unfold (260ms ease) only on the selected card; the page shows exactly one explanation per group, collapsed cards keep the text as a tooltip - storage rows drop their always-on caption (label + path + readout + Change… on one line; caption lives in the row tooltip) The whole page now fits a laptop window without scrolling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): merge Models + Engines into one wizard act Two tabs weren't necessary: models are the required gate, engines the optional extras — now two stacked panels in a single 'Models & engines' step (label reuses the journey-rail key, translated in 21 locales). Wizard shrinks to 4 steps: Welcome → System check → Models & engines → Dictation. Continue still gates on models_ready only; engines stay optional. Welcome cards updated to the 3 remaining acts; static cards keep their descriptions visible (the active-only fold is for radios). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(setup): wizard was skipped after first-run install — probe /setup/status on bootstrap ready The models-needed probe started at mount with a ~30s retry ceiling. On a first run, mount happens at the setup page — by the time the user reads it and the multi-minute install finishes, the attempts were long burned, so setupChecked landed as 'no wizard needed' and the studio rendered with zero models on disk. The probe is now keyed on bootstrapStage and runs when it hits 'ready' — the first moment a backend exists to answer. Normal launches (backend up quickly) behave exactly as before. Caught by running the full journey three times end-to-end: rounds 2–3 skipped Models & engines after install; with the fix the wizard mounts with models_ready=false (Whisper large-v3 listed missing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): drop the Welcome step — wizard opens on System check The welcome act had nothing left to say: the journey rail names the stages, the setup page already oriented the user, and the cards repeated both. The wizard is now three steps — System check (auto-runs on mount) → Models & engines → Try dictation — landing the user directly on live preflight results instead of a page about the pages to come. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): true unified library — models + engines as ONE list 'Merge them' meant one list, not two panels stacked — fair criticism. The wizard's Models & engines act is now a purpose-built WizardLibrary: every installable is a row of the same grammar (LED · name · chip · size · action): - required models lead (REQUIRED chip, Download action, live SSE progress bar + percent, green LED when installed) — they gate continue - TTS engines follow (ENGINE chip): active engine glows accent, available ones offer one-click Use (selectEngine), heavy installs defer honestly to Settings ('install later in Settings' + reason tooltip) - the optional-model tail folds behind 'Show N optional models' The full management surface (search, HF token, deletes, sorting) stays in Settings — a first run needs a checklist, not a store. 9 new strings × 21 locales. Verified against the live backend via the browser harness: required/installed/engine/active/Use/defer states all render in one list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(diagnostics): local-first self-check, error journal, and bug-report pipeline (#296) * feat(diagnostics): local self-check + scrubbed bug-report pipeline Closes the gap between 'something broke' and 'a useful GitHub issue exists' — entirely within the local-first constraint: the only outbound path remains the user's own browser opening a prefilled issues/new URL. Backend: - core/scrub.py: privacy scrubber for anything leaving the machine — env-var secret values (*TOKEN*|*KEY*|*SECRET*|*PASSWORD*), credential shapes (hf_/ghp_/github_pat_/sk-), home dirs on all three OSes - core/diagnose.py: 9-check self-check (device+GPU, ffmpeg, HF token, disk, data-dir writability, RAM, engine registry, hub reachability), pre-scrubbed, ASCII-safe output - GET /system/diagnose + 'python main.py --diagnose' (exit 0/1) - /system/info: hardware inventory (os_version, cpu_model, cpu_count, ram_total_gb, gpu_name, vram_total_gb, disk_free_gb), cached statics Frontend: - utils/bugReport.js: single source for the prefilled-URL builder — scrubText twin, hardware context capture, scrubbed error+stack embed, URL-length cap; ReportBugButton refactored onto it - ErrorBoundary 'Report this bug' action with the error attached - utils/errorToast.jsx toastErrorWithReport(); wired into export toasts - Settings > About 'Run self-check' with per-check status badges Tests: 27 pytest (scrub, diagnose) + 15 vitest (bugReport); existing suites green; verified live (--diagnose, TestClient, vite build). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(diagnostics): error journal, diagnostic bundle, crash notice, global handlers Second slice of the bug-tracking work — still zero outbound paths beyond the user's own browser/file manager. - core/error_journal.py: deduped ring of recent unhandled backend errors (fingerprint counts, error_class triage: GPU_OOM, HF_AUTH_FAILED, PYANNOTE_LICENSE_REQUIRED, DISK_FULL, FFMPEG_MISSING, NETWORK_ERROR), scrubbed, JSONL-persisted so the error that killed the last run survives restart. Wired into the global exception handler; 500 bodies now carry error_class; GET /system/errors/recent. - core/diagnostic_bundle.py + POST /system/diagnostic-bundle + Settings > About 'Save diagnostic bundle': zip of self-check report, error journal, scrubbed log tails — drag onto a GitHub issue; bypasses the ~8k prefill-URL ceiling. - crash-on-next-launch: /system/notifications flags a crash logged before this session started (size vs acked-size in prefs, mtime vs process start); POST /system/crash/ack; LogsFooter acks on action click. - utils/globalErrorHandlers.js: uncaught errors + unhandled rejections get a throttled, noise-filtered 'Report this bug' toast. - sidecar log parity fix: _tauri_log_candidates() now lists the Rust sidecar's backend.log/backend_err.log on Linux (XDG state dir) and Windows (LOCALAPPDATA) — sidecar crashes were only visible on macOS. Tests: +19 pytest (journal, bundle); suite at 102 passed. Vitest 124 passed; vite build green. Live-verified: journal recorded and classified a real HF 401 from the test run (HF_AUTH_FAILED, paths scrubbed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(diagnostics): breadcrumbs, deep self-check, report sweep, issue search Final slice of the bug-tracking work. - toastErrorWithReport adopted at the high-traffic failure sites: TTS generation, dub upload/ingest/transcribe, engine install, engines-matrix load, voice profile save/delete/test, batch enqueue/cancel/delete. Validation toasts and cancellations stay plain on purpose. - utils/breadcrumbs.js: local-only ring of the last 20 action names (closed-set names only — never content or paths), embedded as a 'Recent actions' section in the prefilled report. Instrumented: view changes, generate, dub pipeline, export, engine switch. - deep self-check: /system/diagnose?deep=true and --diagnose --deep load the active engine and synthesize a short utterance (num_step=4) — catches 'installed but broken'. 180s time-box, skips during model load, scrubbed failure detail. Verified live: cold-loaded omnivoice and produced 2.2s of audio in 43.9s on CUDA. - 'Search similar issues' action on the ErrorBoundary: scrubbed, noise-stripped GitHub issue search URL — dedupe before filing. - bug_report.md template now points at the diagnostic bundle and the --diagnose CLI so manual reports arrive with the same evidence. Tests: pytest 107 passed (4 new deep-check tests, CJK gate green); vitest 218 passed (breadcrumbs + issue-search suites); vite build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(diagnostics): self-diagnosis section in troubleshooting + README pointer Settings > About self-check / --diagnose / --deep / diagnostic bundle are now the documented first step before the per-error entries — and the support team's first ask on every issue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): flush sticky action bar, global dbl-click maximize, open maximized First-run polish on the studio-console journey: - FirstRunSetup: fixed-footer / scrollable-middle layout — mast + decision grid live in a dedicated .frs__scroll region; the install action bar is the last flex item, so it sits flush at the window's bottom edge and nothing (e.g. an expanded compute-option description) can render beneath it on small windows. - Double-click-to-maximize on the custom borderless titlebar now works on EVERY drag region (splash, first-run, wizard, main header) via one delegated listener in main.jsx, on all platforms; removed App.jsx's redundant inline handler so it doesn't double-toggle. Skips interactive controls in the bar. - Window opens maximized to the available desktop size (tauri.conf.json). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(diagnostics): quiet Bandit on the journal hash and hub probe The journal fingerprint is a dedup key, not a security boundary — usedforsecurity=False. The hub reachability probe gets an explicit https scheme guard on its constant URL so the urlopen sink is audited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(setup): address PR #295 review findings — security, lifecycle, privacy, i18n Security: - setup.rs valid_mirror: reject plaintext http:// mirror URLs (MITM supply-chain path into UV_PYTHON_INSTALL_MIRROR / UV_INDEX_URL / HF_ENDPOINT); explicit http://localhost / 127.0.0.1 / [::1] exceptions only. Tests extended incl. loopback-lookalike hosts. - setup.rs detect_hardware: AMD vendor ID alone no longer maps to kind="rocm" — a cheap ROCm userspace probe (/opt/rocm or rocminfo on PATH) gates it; bare AMD GPUs report kind="amd" so the UI offers ROCm without pre-selecting it ("matches this machine" only when verified). Functional: - lib.rs/setup.rs --setup re-entry: complete_setup now kills any backend still serving on the port before retry_bootstrap, so changed env/mirror/layout settings actually apply instead of re-attaching. - setup.rs: nvidia-smi probe runs behind a 3 s timeout thread — a wedged driver degrades to CPU instead of hanging the first-run IPC. - setup.rs: is_first_run is now a pure read; the existing-install migration write moved to migrate_existing_install_if_needed, invoked only from the bootstrap thread (get_setup_state no longer writes). - setup.rs complete_setup: config save errors now abort setup and surface in the UI instead of bootstrapping into a stale on-disk layout. - setup.rs complete_setup: logs default-vs-custom flags instead of the user's absolute env/data/models paths (privacy rule). - scrub.py + bugReport.js: also redact forward-slash Windows homes (C:/Users/<name>, file:///C:/Users/...), ordered before the macOS pattern so "C:~" residue can't form. Tests added on both sides. - bugReport.js: context fetches bounded by a 2.5 s AbortController timeout so report assembly degrades to partial context instead of hanging on a stalled backend. - system.py: crash ack is now {size, mtime} (legacy size-only ack still honored) and /system/logs/clear drops the ack — truncation can no longer permanently suppress 'crash-last-session'. - system.py: Linux Tauri-log probe honors XDG_DATA_HOME. - setup.ts/WizardLibrary.jsx: SetupProgressEvent type now documents the full phase taxonomy actually emitted (per-file start/progress/done + install_*/delete_* lifecycle); reducer verified correct against the backend stream and annotated — a file-level 'done' must not clear the repo row. - SetupWizard.jsx: step rail clamps to the highest unlocked step (preflight/models gates) — no more jumping straight to "Enter studio". Polish: - BootstrapSplash.jsx: Waveform heights wrapped in useMemo([bars]) like its siblings. - BootstrapSplash.jsx: detectHints returns i18n keys (bootstrap.hint_*) rendered through t(); translated in all 21 locales. - SetupWizard.jsx: step rail aria-label localized (setup.step_aria / setup.step_completed) in all 21 locales. - FirstRunSetup.css: deprecated word-break: break-word → overflow-wrap: anywhere; reduced-motion override also stops the frs-hw-pulse LEDs (.frs-step.is-active LED + .swiz-lib__led--busy). Deferred (design-level, follow-up PR): --setup re-entry round-tripping of custom dirs/mirrors into the form (setup.rs), and worker-thread leak on timed-out deep checks (diagnose.py). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(i18n): translate back-filled keys in all 20 locales, drop inline fallbacks The reconciliation merge back-filled 16 new keys (about.self_check*, about.*bundle*, dub.num_speakers_*, errors.*) with English text in every non-English locale — CodeRabbit flagged 9 locales; fixed all 20. Interpolation tokens preserved and asserted during the rewrite. Also removed the two inline English fallback strings in App.jsx (firstrun.first_sound_*) so copy lives only in locales/*.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: mergetest <test@local>
86 lines
3.3 KiB
Python
86 lines
3.3 KiB
Python
"""Diagnostic bundle — everything a maintainer needs, in one drag-and-drop.
|
|
|
|
The prefilled GitHub Issues URL caps out around 8k characters, so logs can
|
|
never ride along with a report. This module zips the full picture instead:
|
|
|
|
omnivoice-diagnostics-<timestamp>.zip
|
|
├── meta.json app version, platform, python, generated-at
|
|
├── self_check.txt human-readable diagnose report
|
|
├── self_check.json same, structured
|
|
├── errors.json recent error journal (deduped, classified)
|
|
└── logs/
|
|
├── omnivoice.log.txt last 500 lines, scrubbed
|
|
└── crash_log.txt last 200 lines, scrubbed
|
|
|
|
Settings → About → "Save diagnostic bundle" builds it and reveals the file;
|
|
the user drags it onto their GitHub issue. Every text member is passed
|
|
through core.scrub — the bundle is built TO leave the machine, so it must
|
|
be safe by construction. The zip is written to OUTPUTS_DIR (user-visible,
|
|
already revealed-in-folder elsewhere in the app).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import platform
|
|
import sys
|
|
import time
|
|
import zipfile
|
|
|
|
from core.config import OUTPUTS_DIR, LOG_PATH, CRASH_LOG_PATH
|
|
from core.scrub import scrub_text
|
|
from core.version import APP_VERSION
|
|
|
|
_LOG_TAIL_LINES = 500
|
|
_CRASH_TAIL_LINES = 200
|
|
|
|
|
|
def _scrubbed_tail(path: str, max_lines: int) -> str:
|
|
"""Last `max_lines` of `path`, scrubbed. Missing/unreadable file → a
|
|
one-line note instead of a hard failure (the bundle must always build)."""
|
|
try:
|
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
lines = f.readlines()
|
|
except FileNotFoundError:
|
|
return f"(no file at {scrub_text(path)})\n"
|
|
except Exception as e:
|
|
return f"(could not read {scrub_text(path)}: {scrub_text(str(e))})\n"
|
|
return scrub_text("".join(lines[-max_lines:]))
|
|
|
|
|
|
def build_bundle(include_network: bool = False) -> str:
|
|
"""Build the zip and return its absolute path.
|
|
|
|
``include_network=False`` by default: the bundle is usually requested
|
|
exactly when something is wrong, and a hung hub probe shouldn't add 5s
|
|
to "save the evidence".
|
|
"""
|
|
from core.diagnose import run_diagnostics, format_text
|
|
from core import error_journal
|
|
|
|
report = run_diagnostics(include_network=include_network)
|
|
|
|
meta = {
|
|
"app_version": APP_VERSION,
|
|
"platform": scrub_text(platform.platform()),
|
|
"python": sys.version.split()[0],
|
|
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
}
|
|
|
|
stamp = time.strftime("%Y%m%d-%H%M%S")
|
|
os.makedirs(OUTPUTS_DIR, exist_ok=True)
|
|
out_path = os.path.join(OUTPUTS_DIR, f"omnivoice-diagnostics-{stamp}.zip")
|
|
|
|
with zipfile.ZipFile(out_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
zf.writestr("meta.json", json.dumps(meta, indent=2, ensure_ascii=False))
|
|
zf.writestr("self_check.txt", format_text(report))
|
|
zf.writestr("self_check.json", json.dumps(report, indent=2, ensure_ascii=False))
|
|
zf.writestr(
|
|
"errors.json",
|
|
json.dumps(error_journal.recent(50), indent=2, ensure_ascii=False),
|
|
)
|
|
zf.writestr("logs/omnivoice.log.txt", _scrubbed_tail(LOG_PATH, _LOG_TAIL_LINES))
|
|
zf.writestr("logs/crash_log.txt", _scrubbed_tail(CRASH_LOG_PATH, _CRASH_TAIL_LINES))
|
|
|
|
return out_path
|