From 7938e8138700673bbebcfa745412f6440375a4a6 Mon Sep 17 00:00:00 2001 From: jerrymares Date: Tue, 21 Jul 2026 20:35:48 -0500 Subject: [PATCH] expand DeckDoc diagnostic platform --- .github/workflows/pages.yml | 34 ++ .github/workflows/test.yml | 24 ++ .gitignore | 1 + README.md | 342 ++++++++++------ ROADMAP.md | 169 ++++---- bootprobe/README.md | 26 ++ bootprobe/build-rescue-image.sh | 39 ++ bootprobe/deckdoc-rescue-collect.sh | 140 +++++++ deckdoc.sh | 6 +- docs/assets/app.js | 375 ++++++++++++++++++ docs/assets/knowledge.js | 222 +++++++++++ docs/assets/questionnaire.js | 170 ++++++++ docs/assets/styles.css | 251 ++++++++++++ docs/index.html | 145 +++++++ docs/research/steamdeck-issue-deep-dive.md | 225 +++++++++++ docs/wiki/Audio-Problems.md | 79 ++++ docs/wiki/Black-Screen-After-Long-Shutdown.md | 97 +++++ docs/wiki/Collecting-and-Sharing-Evidence.md | 105 +++++ docs/wiki/Continuous-Incident-Probe.md | 70 ++++ docs/wiki/Controls-Bluetooth-and-Input.md | 43 ++ docs/wiki/Coverage-and-Gaps.md | 92 +++++ docs/wiki/Crashes-GPU-and-Memory.md | 102 +++++ docs/wiki/DeckDoc-Rescue.md | 75 ++++ docs/wiki/Display-and-Gamescope-Problems.md | 92 +++++ docs/wiki/Dock-USB-C-and-External-Displays.md | 67 ++++ docs/wiki/Getting-Started.md | 96 +++++ docs/wiki/Hardware-Failure-Decision-Guide.md | 51 +++ docs/wiki/Home.md | 100 ++++- docs/wiki/Module-Reference.md | 168 ++++++++ docs/wiki/Network-and-Resume-Problems.md | 89 +++++ .../Power-Thermal-and-Battery-Problems.md | 76 ++++ .../Privileged-Diagnostic-Authorization.md | 63 +++ docs/wiki/Reading-DeckDoc-Reports.md | 113 ++++++ docs/wiki/Recovery-and-Escalation.md | 75 ++++ docs/wiki/Research-and-Issue-Index.md | 104 +++++ docs/wiki/Storage-and-MicroSD-Problems.md | 70 ++++ docs/wiki/Triage-Flow.md | 74 ++++ docs/wiki/_Footer.md | 3 + docs/wiki/_Sidebar.md | 34 ++ modules/acpi_pm_state.sh | 45 ++- modules/coredump_analysis.sh | 36 ++ modules/dock_usb_c.sh | 149 +++++++ modules/dxvk_page_fault.sh | 27 +- modules/gamescope_session.sh | 26 +- modules/memory_swap.sh | 55 ++- modules/mmc_sd_card.sh | 20 +- modules/probe_incidents.sh | 73 ++++ modules/steam_client_logs.sh | 25 +- modules/thermal_fan.sh | 10 + modules/wifi_firmware.sh | 35 +- privileged/deckdoc-authorized | 86 ++++ privileged/deckdoc-authorized-client.sh | 65 +++ privileged/install-authorized.sh | 142 +++++++ probe/deckdoc-probe.service | 29 ++ probe/deckdoc-probe.sh | 279 +++++++++++++ probe/install-probe.sh | 72 ++++ setup.sh | 5 +- tests/fixtures/deckdoc-authorized.sudoers | 6 + tests/test_runner.sh | 326 ++++++++++++++- tests/validate_deckmd.js | 52 +++ tests/validate_links.js | 44 ++ 61 files changed, 5422 insertions(+), 292 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 .github/workflows/test.yml create mode 100644 bootprobe/README.md create mode 100755 bootprobe/build-rescue-image.sh create mode 100755 bootprobe/deckdoc-rescue-collect.sh create mode 100644 docs/assets/app.js create mode 100644 docs/assets/knowledge.js create mode 100644 docs/assets/questionnaire.js create mode 100644 docs/assets/styles.css create mode 100644 docs/index.html create mode 100644 docs/research/steamdeck-issue-deep-dive.md create mode 100644 docs/wiki/Audio-Problems.md create mode 100644 docs/wiki/Black-Screen-After-Long-Shutdown.md create mode 100644 docs/wiki/Collecting-and-Sharing-Evidence.md create mode 100644 docs/wiki/Continuous-Incident-Probe.md create mode 100644 docs/wiki/Controls-Bluetooth-and-Input.md create mode 100644 docs/wiki/Coverage-and-Gaps.md create mode 100644 docs/wiki/Crashes-GPU-and-Memory.md create mode 100644 docs/wiki/DeckDoc-Rescue.md create mode 100644 docs/wiki/Display-and-Gamescope-Problems.md create mode 100644 docs/wiki/Dock-USB-C-and-External-Displays.md create mode 100644 docs/wiki/Getting-Started.md create mode 100644 docs/wiki/Hardware-Failure-Decision-Guide.md create mode 100644 docs/wiki/Module-Reference.md create mode 100644 docs/wiki/Network-and-Resume-Problems.md create mode 100644 docs/wiki/Power-Thermal-and-Battery-Problems.md create mode 100644 docs/wiki/Privileged-Diagnostic-Authorization.md create mode 100644 docs/wiki/Reading-DeckDoc-Reports.md create mode 100644 docs/wiki/Recovery-and-Escalation.md create mode 100644 docs/wiki/Research-and-Issue-Index.md create mode 100644 docs/wiki/Storage-and-MicroSD-Problems.md create mode 100644 docs/wiki/Triage-Flow.md create mode 100644 docs/wiki/_Footer.md create mode 100644 docs/wiki/_Sidebar.md create mode 100755 modules/dock_usb_c.sh create mode 100755 modules/probe_incidents.sh create mode 100755 privileged/deckdoc-authorized create mode 100755 privileged/deckdoc-authorized-client.sh create mode 100755 privileged/install-authorized.sh create mode 100644 probe/deckdoc-probe.service create mode 100755 probe/deckdoc-probe.sh create mode 100755 probe/install-probe.sh create mode 100644 tests/fixtures/deckdoc-authorized.sudoers create mode 100644 tests/validate_deckmd.js create mode 100644 tests/validate_links.js diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..822c087 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,34 @@ +name: Deploy DeckMD to GitHub Pages + +on: + push: + branches: [master] + paths: + - "docs/**" + - ".github/workflows/pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: docs + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..61a2dd3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: DeckDoc tests + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Shell syntax + run: bash -n deckdoc.sh setup.sh modules/*.sh probe/*.sh bootprobe/*.sh privileged/* tests/*.sh + - name: Regression and safety suite + run: bash tests/test_runner.sh + - name: Documentation links + run: node tests/validate_links.js diff --git a/.gitignore b/.gitignore index 2431052..59915be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ logs/ *.log +.env.local diff --git a/README.md b/README.md index eec30a1..301198c 100644 --- a/README.md +++ b/README.md @@ -1,152 +1,242 @@ -# DeckDoc v3.1.0 - -**Bare-Metal Diagnostic + Remediation Scaffold for SteamOS / Steam Deck** - -Hardware telemetry, software crash analysis, and guarded remediation designed for Steam Deck failure modes. Fifteen diagnostic modules run in parallel and flush their output frequently so useful evidence survives many interrupted runs. Remediation is explicit, prechecked, backed up, and verified. - -## Steam Deck screen black but sound still works - -If the built-in Steam Deck screen goes black while the backlight, sound, controls, and game keep -working, DeckDoc can distinguish a live-render-to-physical-scanout gap from an application crash, -GPU reset, or failed panel state. On one Jupiter LCD Deck, forcing Gamescope composition collapsed -multiple full-screen hardware planes to one and restored the physical image without changing panel -power, brightness, resolution, TDP, or GPU clocks. - -See [Steam Deck black screen with sound working](docs/wiki/Steam-Deck-Black-Screen-Sound-Working.md) -for the evidence checklist, temporary command, persistent fix, rollback, and limits of this finding. -The mitigation is deliberately guarded: a black screen with no backlight, a disconnected eDP panel, -a GPU reset, or a blackout that already has only one scanout plane is a different failure. - -## Architecture - -``` -deckdoc/ -├── setup.sh # Environment scaffold + permissions -├── deckdoc.sh # Parallel runner + remediation dispatcher (--fix) -├── modules/ -│ # Hardware telemetry (v1.x) -│ ├── gpu_apu.sh # amdgpu ring timeout + CPU/GPU freq lock detection -│ ├── battery_pmic.sh # Raw battery telemetry (voltage/current/energy) -│ ├── thermal_fan.sh # hwmon temp sensors + fan RPM -│ ├── storage_smart.sh # NVMe SMART health (smartctl + sudo fallback) -│ └── fs_integrity.sh # BTRFS device stats + EXT4 state (btrfs + dumpe2fs) -│ # Software/OS diagnostics (v2.0 / v3.0) -│ ├── audio_sof.sh # SOF DSP panic detection (IPC error -22) -│ ├── display_blackout.sh # eDP link/backlight/CRTC/plane-path blackout correlation -│ ├── coredump_analysis.sh # systemd-coredump crash counting & profiling -│ ├── wifi_firmware.sh # ath11k/iwlwifi post-resume failure -│ ├── gamescope_session.sh # Gamescope/MangoApp crashes and current-boot restarts -│ ├── memory_swap.sh # Memory pressure, OOM events, swap analysis -│ ├── steam_client_logs.sh # Steam /tmp/dumps/ crash inventory -│ ├── mmc_sd_card.sh # SD card / mmc driver error detection -│ ├── acpi_pm_state.sh # ACPI suspend/resume, fan-after-wake failures -│ └── dxvk_page_fault.sh # DXVK/VKD3D GPU page fault classification -│ # Remediation modules -│ ├── rem_audio_sof.sh # SOF DSP driver reload — PRE_CHECK/BACKUP/EXECUTE/VERIFY/REPORT -│ └── rem_display_blackout.sh # Gamescope forced composition; no power controls -├── config/ -│ └── 99-deckdoc-display-stability.lua # Optional persistent Gamescope policy -├── tests/ -│ └── test_runner.sh # Mock sysfs unit tests -├── ROADMAP.md # Remediation roadmap -└── .gitignore -``` - -## Quick Start +# DeckDoc + +DeckDoc is a full-system Steam Deck diagnostic and incident-response platform. It helps answer the +larger question behind almost every Deck failure: **what actually failed, what evidence supports that +conclusion, and what is the safest next action?** + +It collects and correlates evidence across the APU/GPU, memory, storage and filesystems, battery and +power, thermals and fan, Wi-Fi, audio, controls, Steam and Proton, Gamescope, suspend/resume, display, +USB-C docks, and connected peripherals. It can capture a one-time system snapshot, preserve evidence +from intermittent failures, inspect a docked hardware path, or collect from outside the installed OS. +The result is one timestamped case that can separate likely application/configuration faults, +SteamOS or driver faults, peripheral faults, and evidence that warrants hardware escalation. + +The project currently ships 17 read-only diagnostic modules, an opt-in incident probe, dock/USB-C +analysis, an alpha rescue collector/image builder, a guided symptom checker, a diagnostic wiki, and +two tightly guarded remediations. DeckDoc is diagnosis-first: its coverage is intentionally much +broader than the small number of conditions it can safely change automatically. + +> Start with the [DeckDoc diagnostic center](docs/wiki/Home.md) if you have a symptom and do not yet +> know which subsystem is responsible. + +Or use [DeckMD](https://deucebucket.github.io/deckdoc/), the private in-browser checklist, to turn a +symptom into ranked diagnostic branches and the next evidence to collect. + +## Problems DeckDoc investigates + +| Area | Questions DeckDoc helps answer | +|---|---| +| Boot and stability | Did the Deck fail before SteamOS, panic, freeze, restart, or lose only one service? | +| Games and graphics | Did one title fail, did Gamescope restart, or did the AMD GPU fault and recover? | +| Memory and crashes | Was there an OOM kill, active pressure, swap churn, or a relevant new core dump? | +| Storage | Is the NVMe or microSD reporting health, controller, filesystem, TRIM, or read-only errors? | +| Power and thermals | Are battery, charging, PMIC, fan, temperature, or low-frequency signals abnormal? | +| Network and audio | Is the device absent, disconnected, driver-failed, or broken specifically after resume? | +| Suspend and resume | Which power transition failed, and which devices or services failed with it? | +| Controls and peripherals | Is the failure tied to Bluetooth, input configuration, USB, or a physical device path? | +| Docks and USB-C | Did PD, Alt Mode, USB topology, Ethernet, or the external display path renegotiate or reset? | +| Display | Is rendering alive while physical scanout failed, or is the symptom part of a wider GPU/session fault? | +| Intermittent incidents | What happened immediately before and after a rare failure that a later report would miss? | +| Hardware decisions | Does the evidence follow the OS/configuration, a peripheral, or the physical Deck across clean tests? | + +DeckDoc does not replace Valve Support, prove that hardware is healthy, repair arbitrary filesystems, +or make risky firmware, voltage, clock, charge, panel-power, or blind GPU-reset changes. + +## Quick start + +Run these commands from Desktop Mode or over SSH: ```bash -# Run diagnostics only -./deckdoc.sh - -# Correlate a physically black LCD while sound/rendering continues -sudo ./deckdoc.sh --display-black - -# Apply a reversible, session-only single-plane Gamescope mitigation -./deckdoc.sh --fix-display-blackout - -# Apply it now and persist it for later Game Mode sessions -./deckdoc.sh --persist-display-stability - -# Run diagnostics + remediation (requires root) -sudo ./deckdoc.sh --fix - -# Or just setup the environment +git clone https://github.com/deucebucket/deckdoc.git +cd deckdoc ./setup.sh + +# Full read-only report. Root reveals kernel/debugfs and device details. +sudo ./deckdoc.sh ``` -All output lands in `logs/` — one file per module plus a consolidated master report. +Reports are written under `logs/`. The file named `deckdoc_master_report_.log` is the +combined report; `module_*.log` files contain each subsystem's raw section. -## Detected Failure Modes (15 modules) +Before posting a report publicly, read [Collecting and sharing evidence](docs/wiki/Collecting-and-Sharing-Evidence.md). +Network names/addresses, usernames, paths, process names, and game titles may be present. -| Failure Mode | Module | Detection Method | -|---|---|---| -| **400MHz CPU Lock** | gpu_apu.sh | `scaling_cur_freq` ≤ 405000 kHz | -| **200MHz GPU SCLK Lock** | gpu_apu.sh | `pp_dpm_sclk` active state at 200 MHz | -| **amdgpu Ring Timeout** | gpu_apu.sh | Kernel errors via `journalctl -b 0 --priority=err` | -| **GPU Reset Outcome** | gpu_apu.sh | `succeeded` vs `failed` classification | -| **Battery Deep Discharge** | battery_pmic.sh | Raw voltage < 6.6V (bypasses % spoofing) | -| **PMIC Trickle-Charge** | battery_pmic.sh | `current_now` ≈ 10000 µA with low voltage | -| **Cell Degradation** | battery_pmic.sh | `energy_full` / `energy_full_design` ratio | -| **Thermal Threshold** | thermal_fan.sh | Exported hwmon high/critical thresholds; >90°C is only a high observation when no threshold is exposed | -| **Fan Stopped** | thermal_fan.sh | Exported fan input at 0 RPM, reported alongside live temperatures | -| **NVMe Health** | storage_smart.sh | `smartctl -H` pass/fail (sudo fallback) | -| **BTRFS Corruption** | fs_integrity.sh | `btrfs device stats` non-zero counters | -| **EXT4 State** | fs_integrity.sh | `dumpe2fs -h` filesystem state flags | -| **SOF DSP Panic** | audio_sof.sh | IPC error -22, DSP panic, pipeline resume failure | -| **Display Blackout** | display_blackout.sh | eDP/EDID/backlight/CRTC correlation, active hardware planes, current and historical display warnings | -| **Core Dump Analysis** | coredump_analysis.sh | Historical counts by executable plus current-boot SIGTRAP/SIGABRT/SIGSEGV severity | -| **WiFi Firmware Crash** | wifi_firmware.sh | ath11k firmware errors, wlan0 DOWN state | -| **Gamescope / MangoApp Health** | gamescope_session.sh | Current-boot restarts, Vulkan errors, MangoApp fdinfo permission aborts | -| **OOM / Memory Pressure** | memory_swap.sh | OOM events, swap thrashing, MemAvailable | -| **Steam Crash Dumps** | steam_client_logs.sh | Actual minidump/core files only; ignores healthy `/tmp/dumps/` bookkeeping | -| **SD Card Errors** | mmc_sd_card.sh | mmc driver errors, ext4 corruption on SD | -| **ACPI Resume Failure** | acpi_pm_state.sh | Fan-after-wake bug, PCI PM resume errors | -| **GPU Page Faults** | dxvk_page_fault.sh | UTCL2 client ID classification (CB/DB/CPF) | +## One project, five diagnostic modes -## Remediation +| Mode | Best for | Output | +|---|---|---| +| Full report | Current system-wide snapshot | 17 correlated module logs plus one master report | +| Continuous probe | Rare/transient failures | Trigger, pre/post journal window, and volatile incident state | +| Dock A/B | Third-party dock, PD, USB, Ethernet, display failures | Topology, exported negotiation, connector, and reset evidence | +| DeckDoc Rescue | Installed OS cannot boot or needs an outside-OS contrast | Private read-only rescue archive and installed journal image evidence | +| DeckMD + wiki | User does not know which subsystem to test | Ranked symptom branches, known patterns, safe checks, and escalation route | -Run `sudo ./deckdoc.sh --fix` to attempt automatic recovery of detected failures. Each remediation module follows a strict lifecycle: +## Core commands -1. **PRE_CHECK** — Verify the trigger condition still exists (race-safe) -2. **BACKUP** — Snapshot state before modification -3. **EXECUTE** — Perform the remediation action (with timeout guard) -4. **VERIFY** — Confirm the fix worked -5. **REPORT** — Log outcome: SUCCESS, FAILED, or PARTIAL +| Command | Effect | Changes the system? | +|---|---|---| +| `sudo ./deckdoc.sh` | Run all 17 diagnostic modules | No; creates local logs | +| `./deckdoc.sh` | Run with reduced access | No; some checks may be incomplete | +| `sudo ./probe/install-probe.sh install` | Opt in to the low-overhead incident watcher | Yes; installs/starts one constrained service | +| `sudo ./probe/install-probe.sh uninstall` | Stop/remove watcher, preserving incidents | Yes; service files only | +| `sudo ./bootprobe/deckdoc-rescue-collect.sh ...` | Collect outside-OS evidence from a compatible rescue environment | No writes to installed disk; creates private archive | +| `sudo ./privileged/install-authorized.sh install` | Approve DeckDoc's exact read-only privileged operations once | Yes; root-owned snapshot and narrow sudoers rules | +| `./privileged/deckdoc-authorized-client.sh report` | Run a complete authorized report later without sharing a password | No; creates a private local report | +| `bash tests/test_runner.sh` | Run mocked regression tests | No production-device changes | + +The optional authorization does not give DeckDoc—or an agent—a password or general sudo access. It +installs a root-owned, checksummed snapshot and allowlists only exact diagnostic operations. Arbitrary +arguments, paths, commands, shells, environment injection, and remediations are excluded. Updating the +snapshot or changing that allowlist requires another visible user approval. See +[Privileged diagnostic authorization](docs/wiki/Privileged-Diagnostic-Authorization.md). + +## Diagnostic coverage + +| Area | Module | Evidence and signatures | +|---|---|---| +| GPU/APU | `gpu_apu.sh` | amdgpu ring timeouts, reset outcome, low CPU/GPU frequency state | +| Display | `display_blackout.sh` | eDP status, EDID, backlight, CRTC, DRM planes, Gamescope, display warnings | +| Dock/USB-C | `dock_usb_c.sh` | USB topology, Type-C/PD/Alt Mode exports, external displays, Ethernet, path errors | +| Battery/PMIC | `battery_pmic.sh` | raw capacity, voltage, current, charge/energy counters | +| Thermals/fan | `thermal_fan.sh` | hwmon readings, plausible exported thresholds, fan RPM | +| NVMe | `storage_smart.sh` | SMART/NVMe health and error fields for `/dev/nvme0n1` | +| Filesystems | `fs_integrity.sh` | BTRFS device counters and ext4 filesystem state | +| Audio | `audio_sof.sh` | SOF panic, IPC timeout/-22, firmware state, ALSA and PipeWire presence | +| Crashes | `coredump_analysis.sh` | retained/current-boot/last-24h dumps, process families, signals, disk use | +| Wi-Fi | `wifi_firmware.sh` | `wlan`/`wl` presence, link info, bounded driver errors/version, Wi-Fi+SOF signature | +| Game Mode | `gamescope_session.sh` | Gamescope dumps and user-service restarts, Vulkan/Wayland errors, MangoApp signature | +| Memory | `memory_swap.sh` | MemAvailable, swap use, cumulative vs live I/O, current-boot OOM events | +| Incident history | `probe_incidents.sh` | latest opt-in trigger, volatile snapshot, bounded journal window | +| Steam/Proton | `steam_client_logs.sh` | real crash files, helper crash rate, Steam errors, prefix/lock inventory | +| microSD | `mmc_sd_card.sh` | mmc presence/mounts, driver/ext4/TRIM errors, read-only state | +| Suspend/resume | `acpi_pm_state.sh` | PM transitions/failures, fan-controller warnings, wake sources | +| Vulkan translation | `dxvk_page_fault.sh` | AMD VM/UTCL2 page-fault class, process hints, timeouts, reset result | + +See [Module reference](docs/wiki/Module-Reference.md) for prerequisites, limitations, and how to +interpret every section. + +## Symptom routes + +| Symptom | Start here | +|---|---| +| Will not power on, boot, or reach SteamOS | [Recovery and escalation](docs/wiki/Recovery-and-Escalation.md) | +| Game freezes, crashes, reboots, or returns to Library | [Crashes, GPU and memory](docs/wiki/Crashes-GPU-and-Memory.md) | +| No sound, especially after wake | [Audio problems](docs/wiki/Audio-Problems.md) | +| Wi-Fi missing or broken after wake | [Network and resume problems](docs/wiki/Network-and-Resume-Problems.md) | +| Overheating, fan stopped, charging, battery, sudden shutdown | [Power, thermal and battery](docs/wiki/Power-Thermal-and-Battery-Problems.md) | +| microSD errors, corrupt games, storage warnings | [Storage and microSD](docs/wiki/Storage-and-MicroSD-Problems.md) | +| Dock, USB-C, charging, Ethernet, or external display | [Dock and USB-C](docs/wiki/Dock-USB-C-and-External-Displays.md) | +| Controls, Bluetooth, touch, or gyro | [Controls and Bluetooth](docs/wiki/Controls-Bluetooth-and-Input.md) | +| Screen black while the rest of the system may still work | [Display problems](docs/wiki/Display-and-Gamescope-Problems.md) | +| First start after days off is black; second boot works | [Long-off startup blackout](docs/wiki/Black-Screen-After-Long-Shutdown.md) | +| Installed OS cannot boot or hardware needs outside-OS comparison | [DeckDoc Rescue](docs/wiki/DeckDoc-Rescue.md) | +| Hardware failure versus fixable software is unclear | [Hardware decision guide](docs/wiki/Hardware-Failure-Decision-Guide.md) | + +## Reading a report + +Treat a DeckDoc finding as a lead, not a verdict: + +1. Match the timestamp to the incident. Old core dumps and old journal warnings are context, not proof + of a current failure. +2. Correlate independent signals. An OOM victim plus live memory pressure, a filesystem error plus new + block I/O failures, or a dock-wide reset plus UCSI/display-link errors is stronger than any one line. +3. Distinguish absence from inaccessibility. A non-root run may be unable to read debugfs, SMART, + BTRFS, system journals, or another user's Game Mode services. +4. Prefer the smallest reversible experiment that tests the leading hypothesis. +5. Re-run diagnostics and physically verify the result before calling a remediation successful. + +The full interpretation guide is [Reading DeckDoc reports](docs/wiki/Reading-DeckDoc-Reports.md). + +## Diagnosis first, narrow remediation second + +DeckDoc's main product is evidence and decision support. Seventeen modules, the probe, Rescue, DeckMD, +and the wiki diagnose far more conditions than DeckDoc modifies. Only two signature-specific +remediations exist today: + +- a Vangogh SOF audio reload after a current-boot DSP panic or IPC `-22`; +- a session-only Gamescope forced-composition test after the validated live LCD scanout-gap precheck. + +Everything else ends in evidence, a safe manual contrast, an upstream-ready report, or escalation—not +an invented “fix.” + +| Specialized command | Guarded action | +|---|---| +| `sudo ./deckdoc.sh --fix` | Reload SOF audio only when the current diagnostic trigger is present | +| `sudo ./deckdoc.sh --display-black` | Collect additional evidence for a declared physical-panel blackout; read-only | +| `./deckdoc.sh --fix-display-blackout` | Run a reversible, session-only Gamescope composition test | +| `./deckdoc.sh --persist-display-stability` | Install the backed-up display policy only after the live test succeeds | + +Every remediation follows: + +```text +PRE_CHECK -> BACKUP -> EXECUTE -> VERIFY -> REPORT -> documented ROLLBACK +``` -The display remediation is intentionally separate from broad `--fix`. It requires `--fix-display-blackout` or `--persist-display-stability`, an explicitly reported symptom, a connected eDP panel, readable EDID, a nonzero backlight, and a live Gamescope session. It only sets Gamescope's `composite_force` policy, disabling direct/multi-plane scanout. The persistent Lua policy also uses Gamescope's documented `OnPostPaint` hook to restore the convar if a launcher-to-game transition clears it; live validation found that a one-time startup assignment was not sufficient. +The audio and display commands, exact prechecks, verification, and rollbacks are documented in the +[safe remediation policy](docs/wiki/Safe-Remediation-Policy.md). Unsupported hardware or a mismatched +signature is skipped rather than guessed. -To roll back persistence, remove `~/.config/gamescope/scripts/99-deckdoc-display-stability.lua` and start a new Game Mode session. For the current session, run `gamescopectl composite_force 0`. +DeckDoc never automatically writes panel power, backlight brightness, refresh rate, resolution, TDP, +CPU/GPU clocks, charging behavior, firmware, or GPU-reset sysfs nodes. See the +[safe remediation policy](docs/wiki/Safe-Remediation-Policy.md). -### Display safety boundary +## Architecture -DeckDoc's display remediation does **not** write panel/backlight power, brightness, refresh rate, TDP, GPU/CPU clocks, charging controls, firmware, or GPU reset sysfs nodes. A black panel with a failed EDID, zero backlight, or inactive modeset is recorded but not forced through this mitigation. +```text +deckdoc.sh + |-- launches 17 diagnostic modules in parallel + |-- flushes module output into logs/module_*.log + |-- consolidates a timestamped master report + `-- dispatches explicit remediation modes sequentially + +modules/ diagnostic and remediation shell modules +probe/ opt-in event-triggered watcher and constrained service installer +bootprobe/ outside-OS collector and unsigned alpha ArchISO builder +privileged/ one-time approved, exact-command diagnostic broker +config/ optional Gamescope policy template +docs/wiki/ GitHub-wiki-ready diagnostic center +tests/test_runner.sh mocked behavior and safety regression checks +remediation_backups/ created only when a remediation records state +logs/ generated reports; ignored by Git +``` -## Bare-Metal Design Philosophy +The runner registers a `sync` trap and modules flush after discrete evidence groups. This improves the +chance that partial evidence survives a crash or power interruption; it cannot guarantee persistence +after every kernel, device, or filesystem failure. -The Steam Deck's aggressive PMIC and SMU protective mechanisms can trigger instantaneous power loss or kernel panic within seconds of boot. Traditional diagnostic tools fail because they rely on system stability to aggregate and format results. +## Requirements and tested scope -DeckDoc's approach: -- **Parallel execution** — all 15 diagnostic modules launch simultaneously via `&` + `wait`; remediation runs sequentially after -- **Synchronous I/O** — `sync` invoked after every discrete hardware read -- **Trap handler** — `panic_sync` registered on EXIT/HUP/INT/QUIT/TERM -- **No daemonization** — runs once, terminates, leaves no persistent process +- SteamOS 3.x on Steam Deck is the target environment. +- Bash 4+, `systemd`/`journalctl`, and standard Linux userland are assumed. +- Root is recommended for a complete report, but fixes always require an explicit flag. +- `smartctl` is needed for NVMe SMART; `btrfs` and `dumpe2fs` enable filesystem checks. +- `aplay`, PipeWire tools, `iw`, `ip`, `lspci`, and `coredumpctl` enrich their related sections. +- Read-only coverage and regression fixtures include Jupiter LCD and Galileo OLED differences; + model-specific findings and remediations remain evidence-first. +- Device paths such as `/dev/nvme0n1`, DRM card indices, driver names, thresholds, and exported sysfs + nodes vary. Missing or different hardware should be reported as a coverage gap. -This improves the chance that evidence remains on disk if a run is interrupted; it cannot guarantee persistence across every storage, kernel, or power failure. +## Development -## Requirements +Run the checks before submitting a change: -- Bash 4+ -- SteamOS 3.x (Arch Linux-based) -- `smartctl` for NVMe health checks -- `btrfs` for BTRFS filesystem stats -- Root access for full diagnostic scope +```bash +bash -n deckdoc.sh setup.sh modules/*.sh probe/*.sh bootprobe/*.sh privileged/* tests/*.sh +bash tests/test_runner.sh +node tests/validate_links.js +git diff --check +``` -## Next: Remediation +New diagnostic knowledge should include a symptom, exact evidence, time scope, confidence boundary, +safe next step, rollback if applicable, and primary references. The +[research and issue index](docs/wiki/Research-and-Issue-Index.md) maps repository issues and upstream +reports to implemented modules and remaining work. -See [ROADMAP.md](ROADMAP.md) for the v3.0 roadmap — shifting from diagnosis to automated remediation and self-healing. +## Roadmap and project status -Engineering research and runbooks live under [docs/wiki](docs/wiki/Home.md); those files are also the -source of truth for the GitHub wiki. +The current roadmap is in [ROADMAP.md](ROADMAP.md). The research-backed priorities are a model/capability +manifest, evidence access ledger, unified incident timeline, safe redacted packager, storage risk gate, +and production hardening/signing of the probe and Rescue image. ## License diff --git a/ROADMAP.md b/ROADMAP.md index 5502d34..6d54e5b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,97 +1,84 @@ -# DeckDoc v3.1 — Roadmap: Diagnosis → Safe Remediation - -v2.0 added 9 new diagnostic modules covering software/OS failure modes. v3.0 shifts focus from **detecting** failures to **automatically remediating** them — and building the feedback loop that tells you whether the fix worked. - -## Implemented (v2.0 / v3.0) - -| Module | Detects | Status | -|---|---|---| -| gpu_apu.sh | CPU 400MHz lock, GPU 200MHz SCLK lock, GPU reset outcome | Delivered | -| battery_pmic.sh | Deep discharge, trickle-charge, cell degradation | Delivered | -| thermal_fan.sh | Thermal trip > 90°C, fan failure (0 RPM) | Delivered | -| storage_smart.sh | NVMe SMART health (sudo fallback) | Delivered | -| fs_integrity.sh | BTRFS corruption, EXT4 state (sudo fallback) | Delivered | -| audio_sof.sh | SOF DSP panic, IPC error -22, pipeline resume failure | Delivered | -| display_blackout.sh | eDP disconnection, backlight=0, wrong ACPI sleep state, modesetting errors | Delivered | -| coredump_analysis.sh | systemd-coredump crash counts, signal profiling | Delivered | -| wifi_firmware.sh | ath11k/iwlwifi firmware crash, wlan0 state | Delivered | -| gamescope_session.sh | Session restarts, Vulkan descriptor failures | Delivered | -| memory_swap.sh | OOM events, memory pressure, swap analysis | Delivered | -| steam_client_logs.sh | /tmp/dumps/ crash inventory, stdout errors | Delivered | -| mmc_sd_card.sh | mmc driver errors, ext4 corruption on SD | Delivered | -| acpi_pm_state.sh | Suspend/resume failures, fan-after-wake bug | Delivered | -| dxvk_page_fault.sh | GPU page fault classification (CB/DB/CPF) | Delivered | - -## Delivered (v3.0) - -| Module | Action | Status | -|---|---|---| -| **rem_audio_sof.sh** | Reload snd_sof_amd_vangogh, verify aplay recovery, PRE_CHECK/BACKUP/EXECUTE/VERIFY/REPORT lifecycle | Delivered | -| deckdoc.sh --fix flag | Runs remediation modules after diagnostics, requires explicit flag | Delivered | -| **rem_display_blackout.sh** | Force one composed Gamescope scanout plane after live-panel prechecks; persistent policy survives per-app convar resets | Delivered | -| display-black signature | Correlate physical black with live eDP, EDID, backlight, CRTC, and multi-plane state | Delivered | -| MangoApp fdinfo signature | Separate MangoApp overlay aborts from Gamescope compositor crashes | Delivered | - -## v3.0 — Remediation Modules (Remaining) - -### P0 — Must Have - -| Module | Trigger | Remediation Action | -|---|---|---| -| **rem_wifi_firmware.sh** | wifi_firmware.sh: wlan0 DOWN or firmware crash | `sudo modprobe -r ath11k_pci && sudo modprobe ath11k_pci`, then verify `ip link show wlan0` shows UP. | -| **rem_coredump_cleanup.sh** | coredump_analysis.sh: >100 dumps | `sudo rm /var/lib/systemd/coredump/*.zst` older than 30 days, report freed space. | -| **rem_gpu_recovery_advice.sh** | gpu_apu.sh: GPU reset failed (hard lock) | Preserve logs, stop remediation, and advise an orderly reboot. DeckDoc will not power-cycle GPU or panel sysfs nodes. | - -### P1 — Should Have - -| Module | Trigger | Remediation Action | -|---|---|---| -| **rem_oom_protection.sh** | memory_swap.sh: MemAvailable < 1GB | Recommend closing games, list top memory consumers by %mem. Non-destructive advisory only. | -| **rem_fan_recovery.sh** | acpi_pm_state.sh + thermal_fan.sh: fan 0 RPM after resume | `sudo systemctl restart jupiter-fan-control`, verify RPM returns in 10s. | -| **rem_steam_cache.sh** | steam_client_logs.sh: shader cache errors | Identify the affected title cache and offer a recoverable, title-scoped move after confirmation. | -| **rem_btrfs_scrub.sh** | fs_integrity.sh: BTRFS corruption > 0 | `sudo btrfs scrub start /` and report progress. | - -### P2 — Nice to Have - -| Module | Trigger | Remediation Action | -|---|---|---| -| **rem_battery_calibrate.sh** | battery_pmic.sh: `energy_full` / `energy_full_design` ratio < 60% or voltage desync | Guide user through full discharge/charge calibration cycle. | -| **rem_sd_repair.sh** | mmc_sd_card.sh: EXT4 errors on mmc | Resolve and display the exact unmounted partition, then require explicit confirmation before repair. | -| **rem_gamescope_restart.sh** | gamescope_session.sh: >3 session restarts | Kill stale gamescope processes, restart session cleanly. | -| **rem_dxvk_cache.sh** | dxvk_page_fault.sh: CB/DB page faults in specific game | Clear DXVK state cache for that title ID. | - -### Non-Functional Improvements - -- `--fix` flag: `./deckdoc.sh --fix` runs all remediation modules after diagnosis -- `--watch` mode: Run in a loop with configurable interval for thermal/memory trend monitoring -- JSON output: Machine-parseable output alongside human-readable logs -- USB bootable mode: Run from recovery media to diagnose unbootable SteamOS installations -- Report upload: Optional `gh issue` creation with diagnostic output attached - -## Architecture for Remediation - -Each remediation module follows a strict lifecycle: +# DeckDoc v3.2 roadmap — full-system diagnostics and incident response -``` -1. PRE_CHECK — Verify the trigger condition still exists (race-safe) -2. BACKUP — Snapshot state before modification (if destructive) -3. EXECUTE — Perform the remediation action (with timeout guard) -4. VERIFY — Confirm the fix worked (re-check trigger condition) -5. REPORT — Log outcome: SUCCESS, FAILED, or PARTIAL -``` +DeckDoc's product is evidence, correlation, and safe decision support. It should help a user separate +application/configuration faults, SteamOS/driver faults, accessory or dock faults, and strong hardware +suspicion without turning one log line into a verdict. Automatic remediation stays intentionally +narrow and is not the measure of project coverage. + +## Current platform + +| Capability | Current state | +|---|---| +| Full report | 17 read-only modules across GPU, power, thermal, storage, filesystems, audio, display, docks, crashes, Wi-Fi, Gamescope, memory, probe history, Steam, microSD, resume, and GPU page faults | +| Incident probe | Opt-in bounded event capture prototype; private, resource-limited, no remediation | +| Dock analysis | USB/Type-C/PD/Alt Mode/display/Ethernet evidence and current-boot path-error correlation | +| DeckDoc Rescue | Read-only collector plus unsigned ArchISO alpha builder for outside-OS comparison | +| DeckMD | Static local-only symptom checker with progressive questions and ranked diagnostic branches | +| Diagnostic wiki | Symptom routes, evidence interpretation, safe contrasts, recovery, and hardware decisions | +| Privileged collection | One-time approved, exact-command, root-owned diagnostic broker prototype | +| Remediation | Two guarded signature-specific paths: SOF reload and Gamescope forced-composition test | + +## P0 — trustworthy evidence foundation + +### [#15 Model and capability manifest](https://github.com/deucebucket/deckdoc/issues/15) + +Identify model, OS/build/slot, drivers, devices, and supported evidence sources before interpreting +their presence or absence. Modules must not assume LCD/OLED behavior, device indices, or driver names. + +### [#17 Unified timeline and access ledger](https://github.com/deucebucket/deckdoc/issues/17) + +Normalize boot IDs, time scopes, sources, events, and permission/retention state. “No matching event,” +“not retained,” “permission denied,” and “not applicable” must remain different outcomes. + +### [#22 Safe redacted bundle and storage-risk gate](https://github.com/deucebucket/deckdoc/issues/22) -Remediation modules never run automatically — they require `--fix` flag or explicit invocation. +Stop normal writes when storage evidence threatens data, then produce separate private/raw and reviewed, +redacted bundles without automatic upload. -## Permanent safety policy +## P1 — productionize the new diagnostic modes + +- [#18 Continuous probe](https://github.com/deucebucket/deckdoc/issues/18): measure overhead, harden + suspend/restart/rotation behavior, expand fixtures, and integrate the incident timeline. +- [#16 DeckDoc Rescue](https://github.com/deucebucket/deckdoc/issues/16): pin and reproduce builds, + sign releases, boot-test LCD/OLED, and document authenticated docked-Ethernet support. +- [#20 Dock/USB-C/PD](https://github.com/deucebucket/deckdoc/issues/20): validate official and + third-party paths and generate direct-vs-dock differential summaries. +- [#19 Privileged authorization](https://github.com/deucebucket/deckdoc/issues/19): adversarially + review the allowlist, snapshot integrity, output path, environment, and revocation flow. +- [#23 DeckMD](https://github.com/deucebucket/deckdoc/issues/23): deploy Pages, share a versioned + vocabulary, expand rules, and complete keyboard/touch/responsive/accessibility validation. + +## P2 — close broad subsystem gaps + +[#21](https://github.com/deucebucket/deckdoc/issues/21) covers three evidence families: + +- staged networking from device and association through route, gateway, DNS, VPN/captive portal, and + service reachability; +- Bluetooth, HID, Steam Input, touch, gyro, and controller lifecycle without logging raw input; +- SteamOS update, slot, image, immutable-root, free-space, and previous-image health. + +Future coverage should also add structured JSON output, battery trend baselines, performance timelines, +firmware/recovery contrasts, and an upstream-ready issue packet built from the redacted bundle. + +## Remediation policy + +A fix is eligible only when DeckDoc has a current, model-compatible signature; a bounded reversible +action; a backup where state changes; direct verification; and a documented rollback. Unsupported or +ambiguous cases end in evidence and escalation. + +```text +PRE_CHECK -> BACKUP -> EXECUTE -> VERIFY -> REPORT -> documented ROLLBACK +``` -- Never change panel voltage, brightness, display power, TDP, clocks, charging behavior, or firmware as a blackout experiment. -- Never attempt a blind GPU/panel sysfs power cycle. Preserve evidence and prefer an orderly reboot when the GPU is genuinely wedged. -- Keep display fixes in the compositor plane-selection layer when evidence shows valid rendered/captured frames but failed physical scanout. -- Require exact device resolution and recoverable backups before filesystem or cache mutation. +DeckDoc will not add generic driver unloads, blind GPU resets, automatic cache deletion, mounted +filesystem repair, clock/voltage/power changes, firmware flashing, or automatic report upload as +routine remedies. Advisory actions such as closing a memory-heavy process are described as advice, +not disguised as remediation modules. -## Completed (v1.0.1) +## Definition of a credible hardware decision -- gpu_apu.sh: dmesg → `journalctl -k -b 0 --priority=err` -- gpu_apu.sh: Separate recoverable vs hard lock classification -- storage_smart.sh: Add `sudo -n` fallback -- fs_integrity.sh: Add `sudo -n` fallback +DeckDoc may report a strong hardware suspicion when a repeatable failure persists across clean +software/configuration tests, outside the installed OS or in firmware where applicable, and across +known-good external parts. Confirmed hardware failure requires direct hardware/service evidence or a +validated repair outcome. A timeout, core dump, persistent BTRFS counter, swap allocation, hot chassis, +zero idle fan, or black screen alone is never confirmation. diff --git a/bootprobe/README.md b/bootprobe/README.md new file mode 100644 index 0000000..ac40d02 --- /dev/null +++ b/bootprobe/README.md @@ -0,0 +1,26 @@ +# DeckDoc Rescue image (alpha) + +DeckDoc Rescue is a separate, bootable evidence environment for a Deck that cannot boot its installed +OS or whose behavior needs an outside-OS comparison. It is not a Valve recovery image and performs no +repair, re-image, filesystem check, unlock, mount, firmware change, or write to the installed disk. + +The collector works from a compatible Linux rescue environment today: + +```bash +sudo ./deckdoc-rescue-collect.sh --installed-disk /dev/nvme0n1 --output-dir /path/to/removable-media +``` + +The alpha ArchISO builder requires an Arch Linux build host with the official `archiso` package: + +```bash +sudo ./build-rescue-image.sh ./out +``` + +The resulting rolling, unsigned image is for controlled development testing. Do not publish it as a +trusted release until package versions are pinned, artifacts are reproducible and signed, checksums are +published, and it has boot/hardware validation on both Jupiter LCD and Galileo OLED Decks. + +For remote operation, use authenticated SSH over docked Ethernet. The image intentionally does not +enable an unauthenticated web server or a default password. Direct USB gadget networking is future, +capability-gated work: Linux requires a USB Device Controller plus configfs gadget support, and the +transport must never be assumed from the connector alone. diff --git a/bootprobe/build-rescue-image.sh b/bootprobe/build-rescue-image.sh new file mode 100755 index 0000000..f465634 --- /dev/null +++ b/bootprobe/build-rescue-image.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$(id -u)" -ne 0 ]; then + echo "Run the ArchISO build with sudo on an Arch Linux build host." >&2 + exit 1 +fi +if ! command -v mkarchiso >/dev/null 2>&1; then + echo "mkarchiso is required. Install the official Arch Linux archiso package." >&2 + exit 1 +fi + +SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RELENG_PROFILE="${DECKDOC_ARCHISO_PROFILE:-/usr/share/archiso/configs/releng}" +OUTPUT_DIR="${1:-${SOURCE_DIR}/out}" +if [ ! -d "$RELENG_PROFILE" ]; then + echo "ArchISO releng profile not found: ${RELENG_PROFILE}" >&2 + exit 1 +fi +mkdir -p "$OUTPUT_DIR" +OUTPUT_DIR=$(readlink -f "$OUTPUT_DIR") +BUILD_ROOT=$(mktemp -d -p /var/tmp deckdoc-rescue-build.XXXXXX) +case "$BUILD_ROOT" in /var/tmp/deckdoc-rescue-build.*) ;; *) exit 1 ;; esac +trap 'rm -rf -- "$BUILD_ROOT"' EXIT +PROFILE="${BUILD_ROOT}/profile" +cp -a "$RELENG_PROFILE" "$PROFILE" + +install -d -m 755 "${PROFILE}/airootfs/usr/local/bin" "${PROFILE}/airootfs/root" +install -m 755 "${SOURCE_DIR}/deckdoc-rescue-collect.sh" "${PROFILE}/airootfs/usr/local/bin/deckdoc-rescue-collect" +install -m 644 "${SOURCE_DIR}/README.md" "${PROFILE}/airootfs/root/DeckDoc-Rescue-README.md" + +for package in smartmontools nvme-cli btrfs-progs e2fsprogs usbutils pciutils iw ethtool openssh; do + if ! grep -qx "$package" "${PROFILE}/packages.x86_64"; then printf '%s\n' "$package" >> "${PROFILE}/packages.x86_64"; fi +done + +echo "Building unsigned DeckDoc Rescue alpha image with the current Arch repositories." +echo "The resulting ISO is not a Valve recovery image and must not be presented as one." +mkarchiso -v -r -w "${BUILD_ROOT}/work" -o "$OUTPUT_DIR" "$PROFILE" +echo "Image written under ${OUTPUT_DIR}. Verify its checksum and boot-test both LCD and OLED before release." diff --git a/bootprobe/deckdoc-rescue-collect.sh b/bootprobe/deckdoc-rescue-collect.sh new file mode 100755 index 0000000..b598f1c --- /dev/null +++ b/bootprobe/deckdoc-rescue-collect.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -uo pipefail + +umask 077 + +OUTPUT_ROOT="/tmp" +INSTALLED_DISK="/dev/nvme0n1" +while [ "$#" -gt 0 ]; do + case "$1" in + --output-dir) + [ "$#" -ge 2 ] || { echo "--output-dir requires a path" >&2; exit 2; } + OUTPUT_ROOT="$2" + shift 2 + ;; + --installed-disk) + [ "$#" -ge 2 ] || { echo "--installed-disk requires a block device" >&2; exit 2; } + INSTALLED_DISK="$2" + shift 2 + ;; + -h|--help) + cat <<'EOF' +Usage: deckdoc-rescue-collect.sh [--output-dir PATH] [--installed-disk /dev/DEVICE] + +Collects live rescue-environment evidence and attempts read-only journal extraction from the installed +disk through journalctl --image. It never mounts, repairs, unlocks, or writes the installed disk. +The resulting archive is private and unredacted. /tmp is volatile; copy it before powering off. +EOF + exit 0 + ;; + *) echo "Unknown argument: $1" >&2; exit 2 ;; + esac +done + +case "$OUTPUT_ROOT" in ''|/|/dev|/dev/*|/proc|/proc/*|/sys|/sys/*) echo "Unsafe output directory: ${OUTPUT_ROOT}" >&2; exit 2 ;; esac +if [ ! -d "$OUTPUT_ROOT" ] || [ ! -w "$OUTPUT_ROOT" ]; then + echo "Output directory must already exist and be writable: ${OUTPUT_ROOT}" >&2 + exit 1 +fi +if [ ! -b "$INSTALLED_DISK" ]; then + echo "Installed disk is not a block device: ${INSTALLED_DISK}" >&2 + exit 1 +fi + +STAMP=$(date -u +%Y%m%dT%H%M%SZ) +CASE_DIR="${OUTPUT_ROOT%/}/deckdoc-rescue-${STAMP}" +ARCHIVE="${CASE_DIR}.tar.gz" +mkdir -m 700 "$CASE_DIR" || exit 1 + +run_section() { + local title="$1" + shift + echo "--- ${title} ---" + "$@" 2>&1 || echo "INACCESSIBLE_OR_FAILED: $*" +} + +{ + echo "DeckDoc Rescue evidence manifest" + echo "capture_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "rescue_boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null || echo inaccessible)" + echo "installed_disk=${INSTALLED_DISK}" + echo "collection_policy=read-only-no-mount-no-repair" + echo "privacy=private-unredacted" + echo "transport=copy archive to removable media or transfer over authenticated docked Ethernet" +} > "${CASE_DIR}/manifest.txt" + +{ + run_section "kernel" uname -a + if [ -r /etc/os-release ]; then run_section "rescue OS" grep -E '^(NAME|VERSION|BUILD_ID|ID)=' /etc/os-release; fi + echo "--- Deck/model capability ---" + for field in product_name product_version board_name board_vendor bios_version; do + [ -r "/sys/devices/virtual/dmi/id/${field}" ] && echo "${field}=$(cat "/sys/devices/virtual/dmi/id/${field}")" + done + run_section "PCI devices and drivers" lspci -nnk + run_section "USB topology" lsusb -t + run_section "block topology" lsblk -o NAME,PATH,TYPE,TRAN,SIZE,RO,FSTYPE,FSVER,LABEL,MOUNTPOINTS + run_section "mounts" findmnt -lo SOURCE,TARGET,FSTYPE,OPTIONS + + echo "--- Type-C and PD exports ---" + for port in /sys/class/typec/port[0-9]*; do + [ -d "$port" ] || continue + echo "port=$(basename "$port")" + for field in data_role power_role port_type power_operation_mode orientation usb_power_delivery_revision; do + [ -r "${port}/${field}" ] && echo " ${field}=$(cat "${port}/${field}")" + done + done + for supply in /sys/class/power_supply/*; do + [ -d "$supply" ] || continue + echo "supply=$(basename "$supply")" + for field in type status online capacity voltage_now current_now power_now usb_type; do + [ -r "${supply}/${field}" ] && echo " ${field}=$(cat "${supply}/${field}")" + done + done + + echo "--- DRM connectors ---" + for connector in /sys/class/drm/card*-*; do + [ -d "$connector" ] || continue + echo "$(basename "$connector") status=$(cat "${connector}/status" 2>/dev/null || echo inaccessible) edid_bytes=$(wc -c < "${connector}/edid" 2>/dev/null || echo inaccessible)" + done + echo "--- hwmon ---" + for input in /sys/class/hwmon/hwmon*/temp*_input /sys/class/hwmon/hwmon*/fan*_input; do + [ -r "$input" ] && echo "${input}=$(cat "$input")" + done + run_section "network link inventory" ip -details -brief link + run_section "USB Ethernet candidates" sh -c 'for n in /sys/class/net/*; do [ -d "$n" ] || continue; printf "%s driver=%s state=%s\n" "$(basename "$n")" "$(basename "$(readlink -f "$n/device/driver" 2>/dev/null || echo unknown)")" "$(cat "$n/operstate" 2>/dev/null || echo inaccessible)"; done' +} > "${CASE_DIR}/live-hardware.log" 2>&1 + +{ + run_section "rescue current-boot journal" journalctl -b 0 -o short-iso-precise --no-pager + run_section "rescue kernel ring" dmesg -T + if [ -d /sys/fs/pstore ]; then run_section "pstore inventory" find /sys/fs/pstore -maxdepth 1 -type f -printf '%f %s bytes\n'; fi +} > "${CASE_DIR}/live-journal.log" 2>&1 + +{ + run_section "NVMe smartctl" smartctl -x "$INSTALLED_DISK" + if command -v nvme >/dev/null 2>&1; then run_section "NVMe smart log" nvme smart-log "$INSTALLED_DISK"; fi + run_section "block read-only flags" lsblk -o NAME,PATH,RO,SIZE,FSTYPE,MOUNTPOINTS "$INSTALLED_DISK" +} > "${CASE_DIR}/storage-health.log" 2>&1 + +{ + echo "DeckDoc uses systemd's image reader; it does not mount or repair the installed filesystems." + run_section "installed boot index" journalctl --image="$INSTALLED_DISK" --list-boots --no-pager +} > "${CASE_DIR}/installed-boot-index.log" 2>&1 + +{ + run_section "installed previous boot" journalctl --image="$INSTALLED_DISK" -b -1 -o short-iso-precise --no-pager + run_section "installed current/last boot" journalctl --image="$INSTALLED_DISK" -b 0 -o short-iso-precise --no-pager +} > "${CASE_DIR}/installed-journals.log" 2>&1 + +if command -v efibootmgr >/dev/null 2>&1; then efibootmgr -v > "${CASE_DIR}/uefi-boot-entries.log" 2>&1 || true; fi + +( + cd "$CASE_DIR" || exit 1 + sha256sum ./*.log manifest.txt > SHA256SUMS +) +tar -C "$OUTPUT_ROOT" -czf "$ARCHIVE" "$(basename "$CASE_DIR")" +chmod 600 "$ARCHIVE" +sha256sum "$ARCHIVE" > "${ARCHIVE}.sha256" + +echo "DeckDoc Rescue capture complete: ${ARCHIVE}" +echo "This archive is unredacted. Review it before sharing, and copy it out of /tmp before shutdown." diff --git a/deckdoc.sh b/deckdoc.sh index 3f45e03..c61436a 100755 --- a/deckdoc.sh +++ b/deckdoc.sh @@ -32,7 +32,7 @@ fi DECKDOC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export DECKDOC_DIR MODULES_DIR="${DECKDOC_DIR}/modules" -LOG_DIR="${DECKDOC_DIR}/logs" +LOG_DIR="${DECKDOC_LOG_DIR:-${DECKDOC_DIR}/logs}" REPORT_FILE="${LOG_DIR}/deckdoc_master_report_$(date +%s).log" mkdir -p "${LOG_DIR}" @@ -44,7 +44,7 @@ panic_sync() { trap panic_sync EXIT HUP INT QUIT TERM echo "========================================" > "${REPORT_FILE}" -echo "DeckDoc v3.1.0 - Diagnostics + Safe Remediation" >> "${REPORT_FILE}" +echo "DeckDoc v3.2.0 - Diagnostics + Safe Remediation" >> "${REPORT_FILE}" echo "Timestamp: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "${REPORT_FILE}" echo "Reported symptom: display-black=${DISPLAY_BLACK_REPORTED}" >> "${REPORT_FILE}" echo "========================================" >> "${REPORT_FILE}" @@ -60,10 +60,12 @@ sync # Software/OS diagnostic modules (v2.0 / v3.0) "${MODULES_DIR}/audio_sof.sh" > "${LOG_DIR}/module_audio.log" 2>&1 & "${MODULES_DIR}/display_blackout.sh" > "${LOG_DIR}/module_display.log" 2>&1 & +"${MODULES_DIR}/dock_usb_c.sh" > "${LOG_DIR}/module_dock.log" 2>&1 & "${MODULES_DIR}/coredump_analysis.sh" > "${LOG_DIR}/module_coredump.log" 2>&1 & "${MODULES_DIR}/wifi_firmware.sh" > "${LOG_DIR}/module_wifi.log" 2>&1 & "${MODULES_DIR}/gamescope_session.sh" > "${LOG_DIR}/module_gamescope.log" 2>&1 & "${MODULES_DIR}/memory_swap.sh" > "${LOG_DIR}/module_memory.log" 2>&1 & +"${MODULES_DIR}/probe_incidents.sh" > "${LOG_DIR}/module_probe.log" 2>&1 & "${MODULES_DIR}/steam_client_logs.sh" > "${LOG_DIR}/module_steam.log" 2>&1 & "${MODULES_DIR}/mmc_sd_card.sh" > "${LOG_DIR}/module_mmc.log" 2>&1 & "${MODULES_DIR}/acpi_pm_state.sh" > "${LOG_DIR}/module_acpi.log" 2>&1 & diff --git a/docs/assets/app.js b/docs/assets/app.js new file mode 100644 index 0000000..32ed6b7 --- /dev/null +++ b/docs/assets/app.js @@ -0,0 +1,375 @@ +(() => { + "use strict"; + + const questions = window.DECKDOC_QUESTIONNAIRE || []; + const knowledge = window.DECKDOC_KNOWLEDGE || []; + const selected = new Set(); + const optionById = new Map(); + const groupByOption = new Map(); + const groupById = new Map(questions.map((q) => [q.id, q])); + + questions.forEach((group) => group.options.forEach(([id, label]) => { + optionById.set(id, label); + groupByOption.set(id, group.id); + })); + + const exclusives = [ + ["none-unsafe", "smoke", "swelling", "liquid", "sparking", "port-damage", "hot-off"], + ["lcd", "oled", "model-unknown"], + ["steamos", "windows", "other-os"], + ["docked", "handheld"], + ["one-title", "all-titles"] + ]; + + const suggestionReasons = { + "sound-works": "Separates a dead/frozen system from one failed output path", + "input-works": "Shows whether the session is still accepting controls", + "ssh-works": "Confirms the kernel and network path remain alive", + "stream-works": "Tests whether rendered frames exist away from the panel", + "screen-backlight": "Narrows LCD panel power versus scanout; not applicable to OLED", + "screen-no-light": "Moves an LCD case toward backlight/panel power evidence", + "external-works": "Separates the internal panel path from the GPU/session", + "internal-works": "Separates external Alt Mode/dock from the internal display", + "during-game": "Distinguishes boot/transition failures from load or title paths", + "after-wake": "Moves device loss toward suspend/resume reinitialization", + "first-after-days": "Identifies the long-off first-start research pattern", + "second-boot-works": "Recovery on the second start is a major discriminator", + "mode-switch": "Points to Gamescope/KWin session transition", + "dock-transition": "Points to Type-C, Alt Mode, hub, or hotplug state", + "one-title": "Moves the first branch toward title/Proton/configuration", + "all-titles": "Moves the first branch toward shared OS/GPU/memory/storage", + "hard-lock": "Separates a process exit from whole-system recovery failure", + "returns-library": "Often leaves a title, Steam, or Gamescope boundary", + "sig-gpu-reset-ok": "Recovery outcome changes the severity and downstream interpretation", + "sig-gpu-reset-fail": "Repeated failed reset is an escalation boundary", + "sig-oom": "Identifies a killed process instead of guessing from swap allocation", + "connected-icon": "Separates firmware/device loss from route/DNS/service stages", + "device-missing": "Stronger than a disconnected or administratively down device", + "local-network": "Separates local link/gateway from DNS or Steam service", + "fan-zero": "Needs temperature context before it means anything", + "hot-now": "Zero RPM plus ≥70°C is a stop-load condition", + "io-errors": "Separates a Steam library path problem from block/media risk", + "read-only": "A forced read-only transition is a data-risk signal", + "multi-dock-failure": "Several dock functions failing together reveal the common upstream path", + "known-good-accessory": "Known-good A/B moves the boundary away from the accessory", + "firmware-also-fails": "Persistence outside the installed OS raises hardware suspicion", + "previous-image-fixes": "Same hardware working on the previous image is strong regression evidence", + "plugins-off-fixes": "A reversible clean run can isolate third-party state", + "windows": "Prevents SteamOS-only signatures and fixes from being misapplied", + "lcd": "Backlight and Vangogh-specific paths may apply", + "oled": "Avoids treating OLED as an LCD with a missing backlight" + }; + + const related = window.DECKDOC_RELATED_CHECKS || {}; + + const evidenceBoosts = { + "sig-display-gap": ["live-internal-display-gap", 34], + "sig-gpu-timeout": ["cross-title-gpu-session", 16], + "sig-gpu-reset-ok": ["cross-title-gpu-session", 12], + "sig-gpu-reset-fail": ["cross-title-gpu-session", 30], + "sig-page-fault": ["cross-title-gpu-session", 16], + "sig-gamescope-core": ["cross-title-gpu-session", 18], + "sig-session-restarts": ["cross-title-gpu-session", 14], + "sig-oom": ["cross-title-gpu-session", 24], + "sig-sof": ["sof-resume-audio", 34], + "sig-wifi-fw": ["wifi-device-resume", 30], + "sig-hot-fan": ["hot-zero-fan", 35], + "sig-dock": ["dock-shared-path", 34], + "sig-ext4": ["storage-data-risk", 28], + "sig-smart": ["storage-data-risk", 34] + }; + + const genericRoutes = { + display: ["Display symptom needs its timing and survivor checks", "path", "Select what stays alive, when it goes black, LCD/OLED, and external-display behavior.", "wiki/Display-and-Gamescope-Problems"], + boot: ["Boot/power symptom needs a preboot boundary", "os", "Record LEDs/chime/BIOS reachability, update history, and whether recovery or Rescue boots.", "wiki/Recovery-and-Escalation"], + crash: ["Crash scope needs one-title versus system-wide evidence", "app", "Select when it crashes, what survives, how many titles reproduce, and current-incident log signatures.", "wiki/Crashes-GPU-and-Memory"], + audio: ["Audio symptom needs device-versus-route evidence", "os", "Select wake timing, device missing versus wrong route, model, and SOF evidence.", "wiki/Audio-Problems"], + network: ["Network symptom needs staged localization", "path", "Select device presence, link state, local gateway, wake timing, and firmware evidence.", "wiki/Network-and-Resume-Problems"], + thermal: ["Thermal symptom needs a live trend and fan context", "hardware", "Select load, temperature, RPM, suspend, and charging context. Stop load if the fan is stopped while hot.", "wiki/Power-Thermal-and-Battery-Problems"], + "charge-problem": ["Charging symptom needs direct-versus-dock evidence", "path", "Compare a known-good direct PD supply with the dock/cable path and record exported telemetry.", "wiki/Power-Thermal-and-Battery-Problems"], + storage: ["Storage symptom needs a data-risk gate", "hardware", "Select device missing/read-only/I/O errors and stop writes when data may be at risk.", "wiki/Storage-and-MicroSD-Problems"], + dock: ["Dock symptom needs topology and A/B evidence", "path", "Select which dock functions fail together and compare direct known-good paths.", "wiki/Dock-USB-C-and-External-Displays"], + input: ["Input symptom needs test-UI and cross-environment evidence", "app", "Select one title/control, firmware behavior, wake timing, and Bluetooth/touch branch.", "wiki/Controls-Bluetooth-and-Input"], + performance: ["Performance symptom needs load-correlated evidence", "os", "Select scope, load, clocks, warm/cold scene, memory pressure, storage activity, and temperature.", "wiki/Crashes-GPU-and-Memory"], + update: ["Update symptom needs slot/build comparison", "os", "Record exact builds and whether previous image, stable channel, recovery, or Rescue changes the result.", "wiki/DeckDoc-Rescue"] + }; + + const stack = document.querySelector("#question-stack"); + const suggestionRail = document.querySelector("#suggestion-rail"); + const rankedResults = document.querySelector("#ranked-results"); + + function escapeHtml(value) { + return String(value).replace(/[&<>"]/g, (c) => ({"&":"&","<":"<",">":">",'"':"""}[c])); + } + + function guideHref(route) { + if (/^https:\/\//.test(route)) return route; + return `https://github.com/deucebucket/deckdoc/${String(route).replace(/^wiki\//, "wiki/")}`; + } + + function renderQuestions() { + stack.innerHTML = questions.map((group, index) => ` +
+ + ${escapeHtml(group.eyebrow)}

${escapeHtml(group.title)}

${escapeHtml(group.hint)}

+ 0 selected +
+
+ ${group.options.map(([id, label]) => ``).join("")} +
+
`).join(""); + + stack.addEventListener("click", (event) => { + const button = event.target.closest("[data-option]"); + if (!button) return; + toggleOption(button.dataset.option); + }); + } + + function toggleOption(id, force = null) { + const willSelect = force === null ? !selected.has(id) : force; + if (willSelect) { + const exclusive = exclusives.find((set) => set.includes(id)); + if (exclusive) exclusive.forEach((other) => selected.delete(other)); + if (id === "none-unsafe") ["smoke","swelling","liquid","sparking","port-damage","hot-off"].forEach((x) => selected.delete(x)); + if (["smoke","swelling","liquid","sparking","port-damage","hot-off"].includes(id)) selected.delete("none-unsafe"); + selected.add(id); + } else { + selected.delete(id); + } + update(); + } + + function currentSuggestions() { + const candidates = []; + const seen = new Set(); + const primary = questions.find((q) => q.id === "symptom").options.map(([id]) => id).filter((id) => selected.has(id)); + primary.forEach((symptom) => (related[symptom] || []).forEach((id) => { + if (!selected.has(id) && !seen.has(id)) { seen.add(id); candidates.push(id); } + })); + knowledge.forEach((rule) => { + if (!rule.symptoms.some((id) => selected.has(id))) return; + [...(rule.requiresAll || []), ...(rule.contextsAny || [])].forEach((id) => { + if (optionById.has(id) && !selected.has(id) && !seen.has(id)) { seen.add(id); candidates.push(id); } + }); + }); + return candidates.slice(0, 10); + } + + function renderSuggestions(ids) { + document.querySelectorAll(".option-card.suggested").forEach((el) => el.classList.remove("suggested")); + ids.forEach((id) => document.querySelector(`[data-option="${CSS.escape(id)}"]`)?.classList.add("suggested")); + if (!ids.length) { + suggestionRail.innerHTML = `Select one or more primary symptoms to reveal the most useful follow-up checks.`; + return; + } + suggestionRail.innerHTML = ids.map((id) => ` + `).join(""); + } + + function ruleScore(rule) { + const symptomHits = rule.symptoms.filter((id) => selected.has(id)); + if (!symptomHits.length) return null; + if ((rule.requiresAll || []).some((id) => !selected.has(id))) return null; + const contextHits = (rule.contextsAny || []).filter((id) => selected.has(id)); + let score = 18 + symptomHits.length * 10 + (rule.requiresAll || []).length * 12 + contextHits.length * 5; + Object.entries(evidenceBoosts).forEach(([evidence, [ruleId, boost]]) => { + if (rule.id === ruleId && selected.has(evidence)) score += boost; + }); + if (selected.has("firmware-also-fails") && rule.layer === "hardware") score += 14; + if (selected.has("previous-image-fixes") && rule.layer === "os") score += 14; + if (selected.has("one-title") && rule.layer === "app") score += 12; + if (selected.has("multi-dock-failure") && rule.id === "dock-shared-path") score += 12; + return { ...rule, score, matched: [...symptomHits, ...(rule.requiresAll || []), ...contextHits].filter((id) => selected.has(id)) }; + } + + function getResults() { + const results = knowledge.map(ruleScore).filter(Boolean).sort((a, b) => b.score - a.score); + const primary = questions.find((q) => q.id === "symptom").options.map(([id]) => id).filter((id) => selected.has(id)); + const matchedSymptoms = new Set(results.flatMap((r) => r.symptoms)); + primary.forEach((symptom) => { + if (matchedSymptoms.has(symptom)) return; + const generic = genericRoutes[symptom]; + if (!generic) return; + results.push({ + id: `generic-${symptom}`, title: generic[0], layer: generic[1], why: generic[2], link: generic[3], + kicker: "More discriminating answers needed", confidence: "Unranked branch", score: 12, + evidence: ["Use the adaptive next-check rail to add timing, survivor, scope, and evidence facts."], + steps: ["Run a full DeckDoc report while the symptom is present when safe."], + avoid: "Do not apply a fix until its model, time scope, and preconditions match.", matched: [symptom] + }); + }); + return results.sort((a, b) => b.score - a.score).slice(0, 6); + } + + function layerWeights(results) { + const weights = { app: 2, os: 2, path: 2, hardware: 2 }; + results.forEach((r, index) => { weights[r.layer] += Math.max(8, r.score / (index + 1)); }); + if (selected.has("one-title")) weights.app += 18; + if (selected.has("after-update") || selected.has("previous-image-fixes")) weights.os += 18; + if (selected.has("sound-works") && selected.has("display")) weights.path += 18; + if (selected.has("multi-dock-failure")) weights.path += 18; + if (selected.has("firmware-also-fails") || selected.has("physical-event")) weights.hardware += 24; + if (selected.has("sig-gpu-reset-fail") || selected.has("sig-smart")) weights.hardware += 16; + const max = Math.max(...Object.values(weights), 1); + Object.keys(weights).forEach((key) => { weights[key] = Math.round(weights[key] / max * 100); }); + return weights; + } + + function renderConstellation(weights) { + const labels = {app:"Application/config", os:"Driver/OS", path:"Device/path", hardware:"Hardware suspicion"}; + Object.entries(weights).forEach(([key, value]) => { + document.querySelector(`#node-${key}`).style.setProperty("--power", String(value / 100)); + }); + document.querySelector("#layer-legend").innerHTML = Object.entries(weights).map(([key, value]) => ` +
${labels[key]}${value}
`).join(""); + } + + function renderResults(results) { + const primarySelected = questions.find((q) => q.id === "symptom").options.some(([id]) => selected.has(id)); + if (!primarySelected) { + rankedResults.innerHTML = `
Start with what failed.

Select a primary symptom. The ranking will become useful after timing and “what still works” answers.

`; + return; + } + if (!results.length) { + rankedResults.innerHTML = `
No safe pattern match yet.

Add timing, survivor, scope, and evidence checks. Unknown is better than a false diagnosis.

`; + return; + } + rankedResults.innerHTML = results.map((r, index) => ` +
+
+ ${String(index + 1).padStart(2, "0")} +
${escapeHtml(r.kicker)}

${escapeHtml(r.title)}

+ ${escapeHtml(r.confidence)} +
+

${escapeHtml(r.why)}

+
+ Open evidence and action boundary · ${r.matched.length} selected facts matched +
+

Collect

    ${r.evidence.map((x) => `
  • ${escapeHtml(x)}
  • `).join("")}
+

Safe next steps

    ${r.steps.map((x) => `
  • ${escapeHtml(x)}
  • `).join("")}
+

Your matched facts

    ${r.matched.map((id) => `
  • ${escapeHtml(optionById.get(id) || id)}
  • `).join("")}
+
+

Avoid: ${escapeHtml(r.avoid)}

+ Open the full diagnostic guide → +
+
`).join(""); + } + + function renderSafety() { + const unsafe = ["smoke","swelling","liquid","sparking","port-damage","hot-off"].filter((id) => selected.has(id)); + const banner = document.querySelector("#safety-banner"); + banner.hidden = !unsafe.length; + if (unsafe.length) document.querySelector("#safety-copy").textContent = `${unsafe.map((id) => optionById.get(id)).join(", ")}. Disconnect power if safe, do not charge/open/stress-test it, and contact Steam Support.`; + } + + function renderFacts() { + const facts = [...selected].filter((id) => optionById.has(id)); + document.querySelector("#selection-count").textContent = `${facts.length} selected`; + document.querySelector("#selected-facts").innerHTML = facts.length ? facts.map((id) => `${escapeHtml(optionById.get(id))}`).join("") : `No incident facts selected yet.`; + } + + function updateProgress() { + let touched = 0; + questions.forEach((group) => { + const count = group.options.filter(([id]) => selected.has(id)).length; + if (count) touched += 1; + document.querySelector(`[data-count-for="${group.id}"]`).textContent = `${count} selected`; + }); + document.querySelector("#answered-count").textContent = String(touched); + document.querySelector("#group-count").textContent = String(questions.length); + document.querySelector("#progress-bar").style.width = `${touched / questions.length * 100}%`; + } + + function updateUrl() { + const url = new URL(window.location.href); + const facts = [...selected].filter((id) => optionById.has(id)); + if (facts.length) url.searchParams.set("s", facts.join(",")); else url.searchParams.delete("s"); + history.replaceState(null, "", url); + } + + function update() { + document.querySelectorAll("[data-option]").forEach((button) => button.setAttribute("aria-pressed", selected.has(button.dataset.option) ? "true" : "false")); + const suggestions = currentSuggestions(); + renderSuggestions(suggestions); + renderSafety(); + updateProgress(); + renderFacts(); + const results = getResults(); + renderResults(results); + renderConstellation(layerWeights(results)); + document.querySelector("#os-warning").hidden = !selected.has("windows"); + document.querySelector("#suggestion-explainer").textContent = suggestions.length ? "These unanswered checks create the largest split between the current branches. Click one to add it." : "Choose a primary symptom and DeckMD will offer the answers that split its major branches."; + updateUrl(); + } + + function openOption(id) { + const groupId = groupByOption.get(id); + const detail = document.querySelector(`[data-group="${CSS.escape(groupId)}"]`); + if (detail) detail.open = true; + toggleOption(id, true); + document.querySelector(`[data-option="${CSS.escape(id)}"]`)?.scrollIntoView({behavior:"smooth", block:"center"}); + } + + function caseSummary() { + const lines = ["DeckMD incident checklist", "========================"]; + questions.forEach((group) => { + const facts = group.options.filter(([id]) => selected.has(id)).map(([,label]) => `- ${label}`); + if (facts.length) lines.push(`\n${group.title}`, ...facts); + }); + const results = getResults(); + if (results.length) lines.push("\nRanked diagnostic branches", ...results.slice(0,5).map((r, i) => `${i+1}. ${r.title} — ${r.confidence}`)); + lines.push("\nGenerated locally by DeckMD. Rankings are triage priorities, not diagnoses."); + return lines.join("\n"); + } + + async function copyText(text, button) { + try { + await navigator.clipboard.writeText(text); + const old = button.textContent; + button.textContent = "Copied"; + setTimeout(() => { button.textContent = old; }, 1600); + } catch (_) { + const area = document.createElement("textarea"); + area.value = text; document.body.append(area); area.select(); document.execCommand("copy"); area.remove(); + } + } + + renderQuestions(); + + const initial = new URLSearchParams(location.search).get("s"); + if (initial) initial.split(",").forEach((id) => { if (optionById.has(id)) selected.add(id); }); + + suggestionRail.addEventListener("click", (event) => { + const chip = event.target.closest("[data-suggestion]"); + if (chip) openOption(chip.dataset.suggestion); + }); + + document.querySelector("#option-search").addEventListener("input", (event) => { + const query = event.target.value.trim().toLowerCase(); + document.querySelectorAll(".question-group").forEach((group) => { + let matches = 0; + group.querySelectorAll(".option-card").forEach((button) => { + const visible = !query || button.dataset.label.includes(query); + button.hidden = !visible; + if (visible) matches += 1; + }); + group.hidden = Boolean(query && !matches); + if (query && matches) group.open = true; + }); + }); + + document.querySelector("#expand-all").addEventListener("click", (event) => { + const groups = [...document.querySelectorAll(".question-group:not([hidden])")]; + const expand = groups.some((group) => !group.open); + groups.forEach((group) => { group.open = expand; }); + event.currentTarget.textContent = expand ? "Collapse all sections" : "Expand all sections"; + }); + document.querySelector("#reset-all").addEventListener("click", () => { selected.clear(); update(); }); + document.querySelector("#copy-summary").addEventListener("click", (event) => copyText(caseSummary(), event.currentTarget)); + document.querySelector("#copy-link").addEventListener("click", (event) => copyText(location.href, event.currentTarget)); + + update(); +})(); diff --git a/docs/assets/knowledge.js b/docs/assets/knowledge.js new file mode 100644 index 0000000..5e45d01 --- /dev/null +++ b/docs/assets/knowledge.js @@ -0,0 +1,222 @@ +window.DECKDOC_KNOWLEDGE = [ + { + id: "live-internal-display-gap", + title: "Internal display path is alive, but pixels are not reaching the panel", + kicker: "Display / Gamescope", + layer: "path", + confidence: "Strong match when captured live", + symptoms: ["display"], + requiresAll: ["sound-works"], + contextsAny: ["external-works", "steamos", "game-mode"], + why: "Audio or input continuing while the internal screen is black moves the first branch away from a dead system. Connected eDP, readable EDID, active CRTC, backlight on LCD, live Gamescope, and no GPU reset make the scanout path the highest-value target.", + evidence: ["Run sudo ./deckdoc.sh --display-black while the panel is physically black.", "Compare internal and external display behavior.", "Read module_display.log, module_gamescope.log, module_gpu.log, and the latest probe incident."], + steps: ["Preserve the live report before restarting.", "If DeckDoc emits LIVE_RENDER_TO_PHYSICAL_SCANOUT_GAP, test session-only forced composition.", "Confirm visible pixels yourself; software cannot see the panel."], + avoid: "Do not write panel power, backlight, clocks, modes, or GPU reset controls from a generic guide.", + link: "wiki/Steam-Deck-Black-Screen-Sound-Working" + }, + { + id: "long-off-first-start-blackout", + title: "First-start blackout after several days off", + kicker: "Boot / display research case", + layer: "path", + confidence: "Community pattern — not yet verified", + symptoms: ["display"], + requiresAll: ["first-after-days", "second-boot-works"], + contextsAny: ["sound-works", "windows", "steamos"], + why: "A failed first start after a long powered-off interval followed by a normal second boot is different from an in-session multi-plane blackout. The report spans LCD/Windows and OLED/SteamOS, so the shared boundary may be earlier power, firmware, display initialization, or stale boot state.", + evidence: ["Record exact off duration, model, OS, charger/dock state, and power-button behavior.", "Collect the failed/current and previous boot journals before another cycle.", "Compare cold start, immediate restart, and long-off start without stacking tweaks."], + steps: ["Use the long-off startup protocol.", "If the failure returns, preserve a probe/manual marker immediately after recovery.", "Treat the second boot as recovery—not proof of root cause."], + avoid: "Do not apply the LCD forced-composition fix unless its live signature independently matches.", + link: "wiki/Black-Screen-After-Long-Shutdown" + }, + { + id: "one-title-proton", + title: "One title or compatibility path is failing", + kicker: "Game / Proton / configuration", + layer: "app", + confidence: "Likely when the rest of the system is stable", + symptoms: ["crash"], + requiresAll: ["one-title"], + contextsAny: ["returns-library", "steamos", "windows"], + why: "A failure isolated to one title, save, scene, mod set, launch option, anti-cheat path, or Proton/API version points first to application compatibility rather than Deck hardware.", + evidence: ["Capture App ID, game build, compatibility tool, exact scene, and crash time.", "Read module_steam.log, module_coredump.log, module_dxvk.log, and module_gpu.log together.", "Check whether unrelated known-good titles reproduce."], + steps: ["Preserve saves and prefix state.", "Verify game files and remove launch options/mods one at a time.", "Test one Valve-supported Proton/API contrast at a time."], + avoid: "Do not delete compatdata before confirming cloud/local save status, and do not call one VM fault failed VRAM.", + link: "wiki/Crashes-GPU-and-Memory" + }, + { + id: "cross-title-gpu-session", + title: "System-wide GPU, Gamescope, or memory instability", + kicker: "Kernel / compositor / shared memory", + layer: "os", + confidence: "Needs timestamped kernel evidence", + symptoms: ["crash"], + contextsAny: ["all-titles", "whole-system", "hard-lock"], + why: "Several unrelated titles, Desktop Mode, or the whole session failing moves the branch above one game. The earliest AMDGPU timeout/page fault/reset, OOM victim, storage error, or Gamescope restart matters more than downstream errors.", + evidence: ["Capture the current or previous boot with an exact incident time.", "Classify GPU reset as succeeded, failed, or absent.", "Separate live memory pressure from cumulative swap and retained cores."], + steps: ["Return to stock clocks/TDP and disable third-party overlays for one control run.", "Compare stable/current versus previous image if the onset followed an update.", "Escalate repeated failed resets or cross-title hard locks."], + avoid: "Do not trigger debugfs/PCI GPU reset, alter voltage/clocks, or stress repeatedly after a failed reset.", + link: "wiki/Crashes-GPU-and-Memory" + }, + { + id: "sof-resume-audio", + title: "SOF audio device failed during resume", + kicker: "Audio DSP / resume", + layer: "os", + confidence: "Strong only with current-boot SOF signature", + symptoms: ["audio"], + requiresAll: ["after-wake"], + contextsAny: ["device-missing", "steamos"], + why: "A missing ALSA card after wake plus a current-boot SOF panic, IPC timeout, or IPC -22 localizes the audio DSP/driver path. A wrong output route with healthy devices is a different branch.", + evidence: ["Read module_audio.log and align SOF lines with the resume window.", "Check ALSA cards and PipeWire sinks before changing routing.", "Record LCD/OLED model; the guarded reload is Vangogh-specific."], + steps: ["Switch the output route away and back if devices still exist.", "Use DeckDoc --fix only when its exact SOF precheck triggers.", "Verify cards, sinks, sound, and post-action errors."], + avoid: "Do not force-remove a busy module or apply the LCD Vangogh driver name to unknown/OLED hardware.", + link: "wiki/Audio-Problems" + }, + { + id: "wifi-device-resume", + title: "Wireless device or firmware did not return after wake", + kicker: "Wi-Fi / firmware / PM", + layer: "os", + confidence: "Needs interface and driver evidence", + symptoms: ["network"], + requiresAll: ["after-wake"], + contextsAny: ["device-missing", "interface-down"], + why: "A vanished interface or bounded ath11k/ath12k/iwlwifi/rtw88/b43/brcmfmac failure moves the branch to device reinitialization. An associated interface with no internet belongs to route, gateway, DNS, AP, or service checks.", + evidence: ["Read module_wifi.log and module_acpi.log.", "Record adapter/driver/firmware, interface presence, and exact wake time.", "If audio also failed, compare timestamps; same boot does not prove one cause."], + steps: ["Preserve evidence, then toggle Wi-Fi once.", "Test a known-good hotspot.", "Use a normal restart if the device is absent."], + avoid: "Do not unload a guessed wireless driver, especially over SSH.", + link: "wiki/Network-and-Resume-Problems" + }, + { + id: "network-stages", + title: "Wi-Fi is linked; the failure is farther up the network path", + kicker: "Route / gateway / DNS / AP / Steam", + layer: "path", + confidence: "Likely when the interface remains associated", + symptoms: ["network"], + requiresAll: ["connected-icon"], + contextsAny: ["steamos", "windows", "after-wake"], + why: "Association alone does not prove address, route, gateway, DNS, captive-portal, VPN, or Steam service health. Test those stages in order before calling it firmware.", + evidence: ["Record interface, address, default route, gateway reachability, and resolver status.", "Compare one known-good hotspot or dock Ethernet.", "Redact SSID, BSSID, MAC, and local addresses."], + steps: ["Toggle only the affected connection after capture.", "Test local gateway before DNS or public services.", "Compare Steam connectivity with general network access."], + avoid: "Do not replace global DNS or delete every saved network as a first step.", + link: "wiki/Network-and-Resume-Problems" + }, + { + id: "hot-zero-fan", + title: "Fan is stopped while a sensor is hot", + kicker: "Thermal safety / fan control", + layer: "hardware", + confidence: "High-risk observation, root cause unconfirmed", + symptoms: ["thermal"], + requiresAll: ["fan-zero", "hot-now"], + contextsAny: ["after-wake", "while-charging", "whole-system"], + why: "Zero RPM can be normal at idle. Zero RPM while a readable sensor is at least 70 C is different and requires stopping load; after wake it can be control/firmware or physical fan evidence.", + evidence: ["Read module_acpi.log and module_thermal.log together.", "Record sensor label, temperature trend, RPM, suspend time, and charge-limit state.", "Physically confirm airflow only when safe."], + steps: ["Stop the workload and allow the Deck to cool.", "Shut down normally if the fan remains stopped as temperature rises.", "Escalate recurrence on stock settings."], + avoid: "Do not run a stress test, block/force PWM, disable thermal protection, or open the unit while hot.", + link: "wiki/Power-Thermal-and-Battery-Problems" + }, + { + id: "storage-data-risk", + title: "Storage evidence may threaten data", + kicker: "NVMe / microSD / filesystem", + layer: "hardware", + confidence: "Risk gate — isolate device versus filesystem next", + symptoms: ["storage"], + contextsAny: ["read-only", "io-errors", "device-missing"], + why: "New block I/O/media errors, ext4 errors, a forced read-only transition, or device disappearance matter more than a Steam library path problem. Persistent SMART/BTRFS counters still need a baseline and time context.", + evidence: ["Read module_storage.log, module_fs.log, and module_mmc.log.", "Capture the first error through any read-only transition.", "Check the device in another trusted reader only after safe shutdown when applicable."], + steps: ["Stop writes and back up readable data.", "Use read-only health inspection.", "Escalate new media errors or repeated disappearance."], + avoid: "Do not format, remount RW, run badblocks, mounted fsck, or btrfs check --repair.", + link: "wiki/Storage-and-MicroSD-Problems" + }, + { + id: "dock-shared-path", + title: "The dock, upstream cable, or power path is the common boundary", + kicker: "Dock / USB-C / PD / Alt Mode", + layer: "path", + confidence: "Strong after a direct-vs-dock A/B", + symptoms: ["dock"], + contextsAny: ["multi-dock-failure", "external-display-fails", "charge-problem", "ethernet-drops"], + why: "Display, USB, Ethernet, and charging share an upstream dock/cable/power path. Several resetting together implicates that common path more than one peripheral; one branch failing while the others stay stable narrows it.", + evidence: ["Run a docked report and one direct/known-good comparison.", "Read module_dock.log for topology, roles, exported PD, external connectors, Ethernet, and aligned errors.", "Record exact dock, PSU, cable, display mode, and peripheral set."], + steps: ["Remove nonessential devices and test one known-good path at a time.", "Try the official/known-good charger directly.", "Cross-test the dock on another host and the Deck with another adapter."], + avoid: "Do not flash unofficial dock firmware, force timings, unbind xHCI remotely, or treat calculated watts as rail certification.", + link: "wiki/Dock-USB-C-and-External-Displays" + }, + { + id: "input-layout", + title: "Steam Input or one title layout is the first branch", + kicker: "Controls / configuration", + layer: "app", + confidence: "Likely if the controller test passes", + symptoms: ["input"], + requiresAll: ["one-title"], + contextsAny: ["steamos", "game-mode"], + why: "If Steam's Controller Test sees the control and other titles/layouts work, the physical input path is functioning for that contrast.", + evidence: ["Run Settings → Controller → Test Controller Inputs.", "Record the exact control, title, layout, and before/after wake state.", "Compare the default template without deleting the current layout."], + steps: ["Save the layout, then test a default template.", "Disable title-specific remaps one at a time.", "Compare an external controller if useful."], + avoid: "Do not write calibration values from another unit or publish raw input streams.", + link: "wiki/Controls-Bluetooth-and-Input" + }, + { + id: "input-hardware-boundary", + title: "Built-in input hardware or firmware needs escalation", + kicker: "Controls / firmware / hardware", + layer: "hardware", + confidence: "Strong suspicion only across environments", + symptoms: ["input"], + requiresAll: ["firmware-also-fails"], + contextsAny: ["whole-system", "after-wake"], + why: "The same physical control failing in Steam's test, multiple titles, and firmware/recovery moves beyond a title layout. A whole controller disappearing only after wake remains a device/resume branch.", + evidence: ["Record the exact control and Controller Test result.", "Compare firmware/recovery and an external controller.", "Capture HID/USB errors without recording typed values."], + steps: ["Restart once and retest stock behavior.", "Use official controller firmware flow only.", "Escalate a consistent physical-control failure across environments."], + avoid: "Do not flash unofficial firmware, open the unit, or log raw keyboard/touch content.", + link: "wiki/Controls-Bluetooth-and-Input" + }, + { + id: "boot-update-regression", + title: "Boot or update regression needs previous-image evidence", + kicker: "Boot / deployment / immutable OS", + layer: "os", + confidence: "Strong if the previous image changes the result", + symptoms: ["boot"], + contextsAny: ["after-update", "boot-loop", "logo-freeze"], + why: "A symptom beginning after an update is a regression candidate, not proof. The same hardware and user data working on the previous image is a much stronger discriminator.", + evidence: ["Record current/previous SteamOS build, kernel, BIOS, and client channel.", "Preserve current and previous boot journals.", "Record whether firmware, previous image, Valve recovery, or DeckDoc Rescue boots."], + steps: ["Use the official previous-image option before destructive recovery.", "Back up data before repair/re-image.", "Use DeckDoc Rescue for read-only outside-OS comparison."], + avoid: "Do not interrupt updates, run system-wide pacman replacement, or re-image before evidence/backup.", + link: "wiki/DeckDoc-Rescue" + }, + { + id: "no-power-preboot", + title: "No-power diagnosis starts before software", + kicker: "Power / battery / EC / board", + layer: "hardware", + confidence: "Needs physical and firmware checks", + symptoms: ["boot"], + requiresAll: ["no-response"], + contextsAny: ["while-charging", "first-after-days"], + why: "No LED, chime, fan, haptics, or display may leave no OS log. Charge indication, official PSU direct, model-specific power timing, and BIOS reachability define the first safe boundary.", + evidence: ["Record LCD/OLED model, LED behavior, charger/dock path, and last successful boot.", "Try the official PSU direct and Valve's model-specific timing.", "Check whether Volume+ plus power reaches BIOS."], + steps: ["If the depleted-battery LED flashes, charge with the provided PSU for at least 15 minutes.", "Use Valve's current basic troubleshooting sequence.", "Escalate no response after known-good power and official checks."], + avoid: "Do not inject power, probe/short the port, repeatedly force-cycle, disconnect the battery, or open the unit.", + link: "wiki/Recovery-and-Escalation" + }, + { + id: "outside-os-hardware-contrast", + title: "Use an outside-OS comparison before declaring hardware", + kicker: "Hardware decision boundary", + layer: "hardware", + confidence: "Decision framework", + symptoms: ["boot", "display", "crash", "storage", "input", "dock"], + contextsAny: ["whole-system", "hard-lock", "firmware-also-fails", "boot-loop"], + why: "Persistence in BIOS or official recovery, across OS slots and stock config with known-good external parts, raises hardware suspicion. A single scary log line does not.", + evidence: ["Compare firmware/recovery/DeckDoc Rescue with installed OS.", "Change one external component or software layer at a time.", "Preserve physical observations software cannot see."], + steps: ["Classify as config/app, driver/OS, strong hardware suspicion, or confirmed service diagnosis.", "Stop risky testing when storage, heat, power, or electrical safety is involved.", "Build a sanitized escalation packet."], + avoid: "Never call a timeout, core, BTRFS counter, swap use, zero idle fan, or black screen alone confirmed hardware failure.", + link: "wiki/Hardware-Failure-Decision-Guide" + } +]; diff --git a/docs/assets/questionnaire.js b/docs/assets/questionnaire.js new file mode 100644 index 0000000..1c12008 --- /dev/null +++ b/docs/assets/questionnaire.js @@ -0,0 +1,170 @@ +window.DECKDOC_QUESTIONNAIRE = [ + { + id: "safety", + eyebrow: "Stop gate", + title: "Is the Deck physically unsafe?", + hint: "Select every observation. Any match stops software troubleshooting.", + tone: "danger", + options: [ + ["smoke", "Smoke or electrical smell"], ["swelling", "Swelling or case separation"], + ["liquid", "Liquid ingress"], ["sparking", "Sparking or arcing"], + ["port-damage", "Damaged USB-C port or cable"], ["hot-off", "Abnormally hot while idle or off"], + ["none-unsafe", "None of these"] + ] + }, + { + id: "symptom", + eyebrow: "01 · Symptom", + title: "What is actually failing?", + hint: "Choose observed symptoms, not your suspected diagnosis.", + options: [ + ["display", "Internal screen black, dim, flickering, or corrupted"], + ["boot", "No power, boot loop, logo freeze, or cannot reach SteamOS"], + ["crash", "Game closes, freezes, hard-locks, or returns to Library"], + ["audio", "No speakers, headphones, microphone, or audio device"], + ["network", "Wi-Fi missing, disconnecting, slow, or no internet"], + ["thermal", "Hot, fan loud/stopped, throttling, or sudden shutdown"], + ["charge-problem", "Will not charge, slow charging, battery drain, or jumps"], + ["storage", "NVMe/microSD missing, read-only, corrupt, or I/O errors"], + ["dock", "Dock, USB-C, Ethernet, USB, charging, or external display"], + ["input", "Buttons, sticks, pads, touch, gyro, Bluetooth, or controller"], + ["performance", "Low FPS, stutter, low clocks, or long stalls"], + ["update", "Update failed, changes vanished, rollback, or image health"] + ] + }, + { + id: "timing", + eyebrow: "02 · Timing", + title: "When does it happen?", + hint: "The transition immediately before failure is often more useful than the final screen.", + options: [ + ["first-after-days", "First power-on after several days off"], + ["cold-boot", "Every cold boot"], ["boot-loop", "During boot or in a boot loop"], + ["logo-freeze", "At the logo"], ["after-wake", "Immediately after wake/resume"], + ["during-suspend", "While trying to sleep or immediate wake"], + ["during-game", "In the middle of gaming"], ["game-launch", "When launching a game"], + ["returns-library", "At return to Library or game exit"], + ["mode-switch", "Switching Game Mode ↔ Desktop Mode"], + ["dock-transition", "Docking or undocking"], ["display-hotplug", "Connecting an external display"], + ["after-update", "Started directly after an OS/client/BIOS update"], + ["after-plugin", "Started after a plugin, mod, overlay, or root change"], + ["under-load", "Only under sustained load"], ["idle", "While idle"], + ["while-charging", "Only while charging"], ["battery-only", "Only on battery"], + ["random", "No repeatable transition yet"] + ] + }, + { + id: "alive", + eyebrow: "03 · What survives", + title: "What still works during the failure?", + hint: "These answers separate a dead system, a frozen session, and one failed output path.", + options: [ + ["sound-works", "Game/UI audio continues"], ["input-works", "Controls still make sounds or haptics"], + ["ssh-works", "SSH still connects"], ["stream-works", "Streaming/recording shows moving frames"], + ["external-works", "External display works while internal is black"], + ["internal-works", "Internal display works while external is black"], + ["fan-reacts", "Fan still reacts to load"], ["power-led", "Power/charge LED behaves normally"], + ["screen-backlight", "LCD backlight is visibly on"], ["screen-no-light", "LCD has no visible backlight"], + ["connected-icon", "Wi-Fi says connected"], ["local-network", "Gateway/local network still works"], + ["whole-system", "Nothing responds / whole system appears dead"], + ["hard-lock", "Frozen image/audio loop; no SSH or input"], ["no-response", "No LED, chime, fan, haptic, or display"] + ] + }, + { + id: "scope", + eyebrow: "04 · Scope", + title: "How broad and repeatable is it?", + hint: "Controlled contrasts move the hardware/software boundary.", + options: [ + ["one-title", "One title, save, scene, layout, or app only"], + ["all-titles", "Several unrelated games"], ["desktop-too", "Desktop Mode too"], + ["game-mode", "Game Mode only"], ["second-boot-works", "Forced off, then second boot works"], + ["restart-fixes", "Normal restart temporarily fixes it"], + ["previous-image-fixes", "Previous SteamOS image fixes it"], + ["stable-fixes", "Stable channel fixes it"], ["plugins-off-fixes", "Disabling plugins/mods fixes it"], + ["firmware-also-fails", "Also fails in BIOS/firmware or official recovery"], + ["rescue-works", "Works in Valve recovery or DeckDoc Rescue"], + ["other-deck", "Same accessory fails on another host"], + ["known-good-accessory", "Known-good replacement cable/dock/card/display also fails"], + ["repro-always", "Reproduces every time"], ["repro-rare", "Rare/intermittent"], + ["new-problem", "New after previously working"], ["physical-event", "Started after drop, liquid, opening, or port damage"] + ] + }, + { + id: "environment", + eyebrow: "05 · Environment", + title: "Which hardware and software path?", + hint: "Model and OS decide which signatures and fixes can safely apply.", + options: [ + ["lcd", "Steam Deck LCD / Jupiter"], ["oled", "Steam Deck OLED / Galileo"], + ["model-unknown", "Model unknown"], ["steamos", "SteamOS"], ["windows", "Windows"], + ["other-os", "Another Linux/OS"], ["docked", "Docked"], ["handheld", "Handheld/direct"], + ["official-dock", "Official Valve Dock"], ["third-party-dock", "Third-party dock/hub"], + ["uses-external-display", "External monitor/TV connected"], + ["ethernet-drops", "Dock Ethernet drops"], ["vpn", "VPN/custom DNS/captive portal"], + ["beta-channel", "Beta/Preview client or OS"], ["third-party", "Decky/plugins/mods/overlays/root changes"], + ["charge-limit", "Custom battery charge limit active"], ["micro-sd", "microSD involved"], + ["replacement-ssd", "Replacement/additional SSD"], ["bluetooth-device", "Bluetooth accessory involved"] + ] + }, + { + id: "details", + eyebrow: "06 · Exact behavior", + title: "Which narrower observations match?", + hint: "Choose only what you directly saw or measured.", + options: [ + ["device-missing", "Device/interface disappears entirely"], ["interface-down", "Interface exists but is DOWN"], + ["fan-zero", "Fan reports or appears 0 RPM"], ["hot-now", "Temperature is at least 70°C while fan is stopped"], + ["read-only", "Storage changed to read-only"], ["io-errors", "Block/MMC/NVMe/filesystem errors appeared"], + ["battery-jump", "Battery percentage jumps or capacity collapses"], + ["slow-charge", "Charging power is low or repeatedly renegotiates"], + ["external-display-fails", "External display blank/wrong mode/flicker"], + ["multi-dock-failure", "Multiple dock functions reset together"], + ["audio-route", "Audio devices exist but wrong output/profile selected"], + ["bluetooth-pair", "Bluetooth discovery/pair/connect fails"], + ["bluetooth-latency", "Bluetooth works but has latency/dropouts"], + ["touch-phantom", "Phantom or mapped-wrong touch"], ["one-control", "One physical control fails"], + ["low-clocks", "Clocks stay low under measured load"], ["stutter-warm", "Stutter improves after repeating the same scene"], + ["full-disk", "Filesystem or inode space is full"], ["immutable-root", "Read-only root or OS changes disappeared"] + ] + }, + { + id: "evidence", + eyebrow: "07 · Evidence", + title: "What did DeckDoc or the logs actually show?", + hint: "A scary string is a lead. Select only current-incident evidence with matching timestamps.", + options: [ + ["sig-display-gap", "LIVE_RENDER_TO_PHYSICAL_SCANOUT_GAP"], + ["sig-panel-incomplete", "PANEL_OR_MODESET_STATE_INCOMPLETE"], + ["sig-gpu-timeout", "AMDGPU ring/job timeout"], ["sig-gpu-reset-ok", "GPU reset succeeded"], + ["sig-gpu-reset-fail", "GPU reset failed or skipped"], ["sig-page-fault", "GPU VM/UTCL2 page fault"], + ["sig-gamescope-core", "Current Gamescope SIGABRT/SIGSEGV"], + ["sig-session-restarts", "More than one Gamescope session start"], + ["sig-oom", "OOM killer / Out of memory"], ["sig-live-swap", "Live vmstat swap I/O"], + ["sig-sof", "SOF panic / IPC timeout / -22"], ["sig-wifi-fw", "Wireless firmware crash/failure"], + ["sig-hot-fan", "LIVE_ZERO_RPM_WITH_HOT_SENSOR_AFTER_SUSPEND"], + ["sig-dock", "TOPOLOGY_CHANGE_WITH_DOCK_PATH_ERROR"], + ["sig-ext4", "EXT4-fs error on mmc/storage"], ["sig-smart", "New SMART media/critical warning"], + ["sig-btrfs", "Nonzero BTRFS device counter without a baseline"], + ["sig-core-old", "Only retained/old core dumps"], ["sig-no-errors", "No matching errors in readable window"], + ["sig-inaccessible", "Relevant source was inaccessible or not retained"] + ] + } +]; + +// Ordered follow-ups define the progressive checklist. The first entries are +// the highest-value branch separators for each primary symptom. +window.DECKDOC_RELATED_CHECKS = { + display: ["sound-works", "screen-backlight", "screen-no-light", "input-works", "ssh-works", "stream-works", "external-works", "during-game", "after-wake", "first-after-days", "second-boot-works", "mode-switch", "dock-transition", "lcd", "oled"], + boot: ["no-response", "power-led", "sound-works", "logo-freeze", "boot-loop", "first-after-days", "second-boot-works", "after-update", "firmware-also-fails", "rescue-works"], + crash: ["one-title", "all-titles", "during-game", "game-launch", "returns-library", "hard-lock", "ssh-works", "sig-gpu-timeout", "sig-gpu-reset-ok", "sig-gpu-reset-fail", "sig-oom"], + audio: ["after-wake", "device-missing", "audio-route", "sig-sof", "lcd", "oled", "bluetooth-device"], + network: ["after-wake", "device-missing", "interface-down", "connected-icon", "local-network", "sig-wifi-fw", "docked", "vpn"], + thermal: ["fan-zero", "hot-now", "under-load", "after-wake", "while-charging", "charge-limit", "sig-hot-fan"], + "charge-problem": ["docked", "third-party-dock", "slow-charge", "battery-jump", "while-charging", "battery-only", "port-damage"], + storage: ["micro-sd", "replacement-ssd", "device-missing", "read-only", "io-errors", "sig-ext4", "sig-smart", "full-disk"], + dock: ["third-party-dock", "official-dock", "multi-dock-failure", "external-display-fails", "ethernet-drops", "charge-problem", "dock-transition", "known-good-accessory", "sig-dock"], + input: ["one-title", "one-control", "firmware-also-fails", "after-wake", "bluetooth-pair", "bluetooth-latency", "touch-phantom"], + performance: ["one-title", "all-titles", "under-load", "low-clocks", "stutter-warm", "sig-live-swap", "sig-oom"], + update: ["after-update", "boot-loop", "previous-image-fixes", "stable-fixes", "immutable-root", "rescue-works"] +}; diff --git a/docs/assets/styles.css b/docs/assets/styles.css new file mode 100644 index 0000000..09a3378 --- /dev/null +++ b/docs/assets/styles.css @@ -0,0 +1,251 @@ +:root { + color-scheme: dark; + --ink: #f1f0e9; + --muted: #9da3a3; + --dim: #696f70; + --bg: #090b0c; + --panel: #111416; + --panel-2: #161a1d; + --line: #2a3033; + --blue: #67c1f5; + --blue-deep: #1a6f9e; + --amber: #f4ba5c; + --coral: #ff6b5e; + --green: #82d6a2; + --radius: 18px; + --shadow: 0 24px 80px rgba(0, 0, 0, .35); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { + margin: 0; + min-width: 320px; + color: var(--ink); + background: + radial-gradient(circle at 76% 2%, rgba(48, 120, 158, .16), transparent 25rem), + linear-gradient(rgba(255,255,255,.018) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,.018) 1px, transparent 1px), var(--bg); + background-size: auto, 32px 32px, 32px 32px, auto; + line-height: 1.55; +} + +a { color: inherit; } +button, input { font: inherit; } +button { color: inherit; } +.skip-link { position: fixed; left: 1rem; top: -5rem; z-index: 100; padding: .75rem 1rem; background: var(--ink); color: var(--bg); } +.skip-link:focus { top: 1rem; } + +.site-header { + width: min(1440px, calc(100% - 3rem)); + margin: 0 auto; + height: 86px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid rgba(255,255,255,.08); +} +.brand { display: flex; align-items: center; gap: .8rem; text-decoration: none; } +.brand-mark { display: grid; place-items: center; width: 38px; height: 38px; border: 1px solid var(--blue); border-radius: 50%; color: var(--blue); font-weight: 900; letter-spacing: -.08em; } +.brand strong { display: block; font-size: 1.05rem; letter-spacing: .04em; } +.brand small { display: block; color: var(--muted); font-size: .64rem; letter-spacing: .18em; text-transform: uppercase; } +nav { display: flex; align-items: center; gap: 1.5rem; } +nav a { color: var(--muted); text-decoration: none; font-size: .86rem; } +nav a:hover, nav a:focus-visible { color: var(--ink); } +.nav-cta { padding: .58rem .85rem; border: 1px solid var(--line); border-radius: 8px; } + +.hero { + width: min(1320px, calc(100% - 3rem)); + min-height: 690px; + margin: 0 auto; + display: grid; + grid-template-columns: 1.2fr .8fr; + gap: clamp(3rem, 8vw, 9rem); + align-items: center; + padding: 7rem 0 8rem; +} +.overline { margin: 0 0 .7rem; color: var(--blue); font-size: .68rem; font-weight: 800; letter-spacing: .18em; text-transform: uppercase; } +.hero h1 { margin: 0; max-width: 820px; font-family: "Arial Narrow", "Roboto Condensed", Impact, sans-serif; font-size: clamp(3.7rem, 7vw, 7.8rem); line-height: .88; letter-spacing: -.045em; text-transform: uppercase; } +.hero h1 em { color: transparent; -webkit-text-stroke: 1px var(--blue); font-style: normal; } +.hero-lede { max-width: 680px; margin: 2rem 0 0; color: #c2c6c5; font-size: clamp(1rem, 1.6vw, 1.22rem); } +.hero-actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2rem; } +.button { display: inline-flex; align-items: center; justify-content: center; min-height: 46px; padding: .72rem 1rem; border: 1px solid var(--line); border-radius: 10px; background: transparent; text-decoration: none; cursor: pointer; transition: transform .16s ease, border-color .16s ease, background .16s ease; } +.button:hover { transform: translateY(-1px); border-color: #536066; } +.button.primary { color: #051014; background: var(--blue); border-color: var(--blue); font-weight: 800; } +.button.ghost { color: var(--ink); background: rgba(255,255,255,.025); } +.button.small { min-height: 38px; padding: .5rem .75rem; font-size: .78rem; } +.trust-row { display: flex; flex-wrap: wrap; gap: 1.5rem; margin: 2.3rem 0 0; padding: 0; list-style: none; color: var(--dim); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; } +.trust-row li::before { content: "✓"; margin-right: .4rem; color: var(--green); } + +.hero-instrument { position: relative; padding: 1rem; border: 1px solid #344047; background: linear-gradient(145deg, rgba(30,36,39,.92), rgba(11,13,14,.96)); border-radius: 24px 24px 6px 24px; box-shadow: var(--shadow); transform: rotate(1.5deg); } +.hero-instrument::before { content: ""; position: absolute; inset: 8px; pointer-events: none; border: 1px solid rgba(255,255,255,.04); border-radius: 18px 18px 3px 18px; } +.instrument-head { display: flex; justify-content: space-between; padding: .35rem .5rem 1rem; color: var(--muted); font: 700 .68rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .14em; } +.live-dot { color: var(--green); } +.live-dot::before { content: ""; display: inline-block; width: 6px; height: 6px; margin-right: .45rem; border-radius: 50%; background: var(--green); box-shadow: 0 0 12px var(--green); } +.trace-line { display: grid; grid-template-columns: 34px 1fr 70px 1.1fr; gap: .7rem; align-items: center; min-height: 72px; padding: 0 1rem; border-top: 1px solid var(--line); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } +.trace-line span { color: var(--dim); font-size: .72rem; } +.trace-line b { font-size: .8rem; letter-spacing: .08em; } +.trace-line i { display: block; height: 1px; background: linear-gradient(90deg, var(--blue), transparent); } +.trace-line small { color: var(--muted); } +.instrument-callout { margin-top: 1rem; padding: 1rem; border-radius: 12px; background: var(--blue); color: #071116; } +.instrument-callout span { display: block; font-size: .62rem; font-weight: 900; text-transform: uppercase; letter-spacing: .15em; } +.instrument-callout strong { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88rem; } + +.checker-shell { width: min(1440px, calc(100% - 2rem)); margin: 0 auto 5rem; padding: clamp(1rem, 3vw, 2.5rem); border: 1px solid var(--line); border-radius: 28px; background: rgba(13,16,18,.94); box-shadow: var(--shadow); } +.checker-topbar, .results-header { display: flex; align-items: end; justify-content: space-between; gap: 2rem; } +.checker-topbar h2, .results-header h2 { margin: 0; font-family: "Arial Narrow", "Roboto Condensed", sans-serif; font-size: clamp(2rem, 4vw, 3.8rem); line-height: 1; letter-spacing: -.035em; text-transform: uppercase; } +.progress-block { min-width: 220px; color: var(--muted); font-size: .78rem; text-align: right; } +.progress-track { height: 4px; margin-top: .65rem; overflow: hidden; border-radius: 99px; background: var(--line); } +.progress-track i { display: block; width: 0; height: 100%; background: var(--blue); transition: width .25s ease; } + +.safety-banner { grid-template-columns: auto 1fr; gap: 1rem; align-items: start; margin: 1.5rem 0; padding: 1rem; border: 1px solid rgba(255,107,94,.55); border-radius: 14px; background: rgba(255,107,94,.08); } +.safety-banner:not([hidden]) { display: grid; } +.safety-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%; color: var(--bg); background: var(--coral); font-weight: 950; } +.safety-banner p { margin: .25rem 0 0; color: #efb8b2; } + +.suggestion-panel { margin: 2rem 0 1rem; padding: 1.1rem; border: 1px solid rgba(103,193,245,.26); border-radius: var(--radius); background: linear-gradient(120deg, rgba(103,193,245,.08), rgba(103,193,245,.01)); } +.suggestion-heading { display: flex; justify-content: space-between; gap: 2rem; align-items: end; } +.suggestion-heading h3 { margin: 0; font-size: 1.25rem; } +.suggestion-heading > p { max-width: 650px; margin: 0; color: var(--muted); font-size: .86rem; } +.suggestion-rail { display: flex; gap: .6rem; overflow-x: auto; margin-top: 1rem; padding-bottom: .25rem; scrollbar-width: thin; } +.suggestion-chip { flex: 0 0 auto; max-width: 300px; padding: .68rem .8rem; text-align: left; border: 1px solid #33454e; border-radius: 10px; color: #cfeefe; background: #111b20; cursor: pointer; } +.suggestion-chip:hover, .suggestion-chip:focus-visible { border-color: var(--blue); } +.suggestion-chip small { display: block; margin-top: .25rem; color: #7594a3; font-size: .68rem; } +.suggestion-empty { color: var(--dim); font-size: .84rem; } + +.checker-tools { display: flex; gap: .75rem; align-items: center; margin: 1rem 0; } +.search-box { flex: 1; display: flex; align-items: center; gap: .5rem; min-height: 42px; padding: 0 .8rem; border: 1px solid var(--line); border-radius: 10px; background: #0c0f11; } +.search-box input { width: 100%; border: 0; outline: 0; color: var(--ink); background: transparent; } +.text-button { border: 0; color: var(--blue); background: none; cursor: pointer; font-size: .78rem; } +.danger-text { color: #d98e87; } + +.question-stack { display: grid; gap: .75rem; } +.question-group { border: 1px solid var(--line); border-radius: 16px; background: var(--panel); overflow: clip; } +.question-group[open] { border-color: #394248; } +.question-group summary { display: flex; align-items: center; justify-content: space-between; gap: 2rem; padding: 1.15rem 1.25rem; cursor: pointer; list-style: none; } +.question-group summary::-webkit-details-marker { display: none; } +.question-group summary::after { content: "+"; color: var(--blue); font-size: 1.35rem; } +.question-group[open] summary::after { content: "−"; } +.question-title h3 { margin: 0; font-size: 1.08rem; } +.question-title p { margin: .2rem 0 0; color: var(--muted); font-size: .78rem; } +.group-count { color: var(--dim); font: 700 .68rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; } +.question-body { padding: 0 1rem 1rem; } +.option-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .55rem; } +.option-card { position: relative; min-height: 64px; padding: .78rem 2.2rem .78rem .8rem; text-align: left; border: 1px solid #2c3337; border-radius: 10px; color: #bac0c0; background: #0d1012; cursor: pointer; transition: border-color .14s ease, color .14s ease, background .14s ease; } +.option-card::after { content: ""; position: absolute; right: .8rem; top: 50%; width: 14px; height: 14px; border: 1px solid #4b555a; border-radius: 3px; transform: translateY(-50%); } +.option-card:hover, .option-card:focus-visible { border-color: #526169; color: var(--ink); } +.option-card[aria-pressed="true"] { border-color: var(--blue); color: #e7f7ff; background: rgba(103,193,245,.09); } +.option-card[aria-pressed="true"]::after { border-color: var(--blue); background: var(--blue); box-shadow: inset 0 0 0 3px #101a1f; } +.question-group[data-tone="danger"] .option-card[aria-pressed="true"] { border-color: var(--coral); background: rgba(255,107,94,.08); } +.question-group[data-tone="danger"] .option-card[aria-pressed="true"]::after { border-color: var(--coral); background: var(--coral); } +.option-card.suggested { box-shadow: inset 3px 0 var(--amber); } +.option-card[hidden], .question-group[hidden] { display: none; } + +.results { margin-top: 3rem; padding-top: 2.5rem; border-top: 1px solid var(--line); } +.result-actions { display: flex; gap: .5rem; } +.os-warning { margin: 1rem 0; padding: .85rem 1rem; border-left: 3px solid var(--amber); color: #e4cfaa; background: rgba(244,186,92,.07); } +.result-grid { display: grid; grid-template-columns: 380px minmax(0, 1fr); gap: 1rem; margin-top: 1.5rem; } +.constellation { min-height: 560px; padding: 1.2rem; border: 1px solid var(--line); border-radius: var(--radius); background: #0b0e10; } +.constellation h3 { margin: 0; } +.orbit { position: relative; width: 270px; height: 270px; margin: 2.2rem auto; border-radius: 50%; } +.orbit-ring { position: absolute; inset: 12%; border: 1px solid #263036; border-radius: 50%; } +.orbit-ring.two { inset: 30%; border-style: dashed; } +.orbit-core { position: absolute; inset: 43%; display: grid; place-items: center; border-radius: 50%; color: var(--dim); background: #181e21; font-weight: 900; } +.signal-node { --power: .25; position: absolute; display: grid; place-items: center; width: calc(45px + var(--power) * 25px); height: calc(45px + var(--power) * 25px); border: 1px solid currentColor; border-radius: 50%; opacity: calc(.4 + var(--power) * .6); transform: translate(-50%, -50%) scale(calc(.82 + var(--power) * .25)); transition: all .3s ease; box-shadow: 0 0 calc(var(--power) * 28px) color-mix(in srgb, currentColor 35%, transparent); } +.signal-node span { font: 850 .65rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; } +.signal-node.app { left: 20%; top: 26%; color: #a3dcae; } +.signal-node.os { left: 76%; top: 22%; color: var(--blue); } +.signal-node.path { left: 82%; top: 76%; color: var(--amber); } +.signal-node.hardware { left: 20%; top: 78%; color: var(--coral); } +.layer-legend { display: grid; gap: .5rem; } +.layer-row { display: grid; grid-template-columns: 72px 1fr 32px; align-items: center; gap: .5rem; font-size: .72rem; } +.layer-meter { height: 4px; background: #252b2e; } +.layer-meter i { display: block; height: 100%; background: var(--blue); } +.constellation-note { margin: 1rem 0 0; color: var(--dim); font-size: .72rem; } +.ranked-results { display: grid; gap: .75rem; align-content: start; } +.empty-result { min-height: 300px; display: grid; place-items: center; padding: 2rem; border: 1px dashed #374046; border-radius: var(--radius); color: var(--muted); text-align: center; } +.empty-result strong { display: block; color: var(--ink); font-size: 1.4rem; } +.result-card { border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel-2); overflow: hidden; } +.result-card-head { display: grid; grid-template-columns: auto 1fr auto; gap: .85rem; align-items: start; padding: 1rem; } +.rank { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid #465157; border-radius: 50%; font: 800 .72rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; } +.result-card h3 { margin: .2rem 0 0; font-size: 1.15rem; line-height: 1.25; } +.result-kicker { color: var(--blue); font-size: .64rem; font-weight: 850; letter-spacing: .14em; text-transform: uppercase; } +.confidence { max-width: 170px; padding: .35rem .5rem; border: 1px solid #465157; border-radius: 999px; color: var(--muted); font-size: .66rem; text-align: center; } +.result-why { margin: 0; padding: 0 1rem 1rem 3.85rem; color: #b8bdbc; } +.result-card details { border-top: 1px solid var(--line); } +.result-card summary { padding: .75rem 1rem; color: var(--blue); cursor: pointer; font-size: .78rem; } +.result-detail { display: grid; grid-template-columns: repeat(3, 1fr); gap: .8rem; padding: 0 1rem 1rem; } +.result-detail h4 { margin: 0 0 .4rem; font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; } +.result-detail ul { margin: 0; padding-left: 1rem; color: var(--muted); font-size: .78rem; } +.avoid-box { margin: 0 1rem 1rem; padding: .7rem .8rem; border-left: 2px solid var(--coral); color: #dfaaa5; background: rgba(255,107,94,.05); font-size: .76rem; } +.result-link { display: inline-flex; margin: 0 1rem 1rem; color: var(--blue); font-size: .78rem; } +.case-digest { margin-top: 1rem; border: 1px solid var(--line); border-radius: 12px; } +.case-digest summary { display: flex; justify-content: space-between; padding: .8rem 1rem; cursor: pointer; } +.case-digest summary span { color: var(--muted); font-size: .74rem; } +#selected-facts { display: flex; flex-wrap: wrap; gap: .4rem; padding: 0 1rem 1rem; } +.fact-chip { padding: .35rem .5rem; border: 1px solid #374045; border-radius: 6px; color: #aeb5b4; font-size: .7rem; } + +.method-strip { width: min(1320px, calc(100% - 3rem)); margin: 0 auto 7rem; display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.method-strip > div { padding: 2rem 1.5rem; border-right: 1px solid var(--line); } +.method-strip > div:last-child { border-right: 0; } +.method-strip span { color: var(--blue); font: 700 .68rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; } +.method-strip strong { display: block; margin-top: .8rem; font-family: "Arial Narrow", sans-serif; font-size: 1.4rem; text-transform: uppercase; } +.method-strip p { color: var(--muted); font-size: .82rem; } +footer { width: min(1320px, calc(100% - 3rem)); margin: 0 auto; padding: 2.5rem 0 3.5rem; display: grid; grid-template-columns: 1fr 1.5fr 1fr; gap: 2rem; color: var(--muted); border-top: 1px solid var(--line); font-size: .78rem; } +footer strong { color: var(--ink); font-size: 1rem; } +footer p { margin: .35rem 0; } +.footer-links { display: flex; justify-content: end; gap: 1rem; } +.noscript { position: fixed; inset: auto 1rem 1rem; padding: 1rem; background: var(--coral); color: #150302; text-align: center; } + +@media (max-width: 1000px) { + .hero { grid-template-columns: 1fr; min-height: auto; padding-top: 5rem; } + .hero-instrument { max-width: 620px; } + .option-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .result-grid { grid-template-columns: 1fr; } + .constellation { min-height: auto; } + .method-strip { grid-template-columns: repeat(2, 1fr); } + .method-strip > div:nth-child(2) { border-right: 0; } + footer { grid-template-columns: 1fr 1fr; } + .footer-links { justify-content: start; } +} + +@media (max-width: 680px) { + .site-header { width: calc(100% - 2rem); height: 70px; } + nav > a:not(.nav-cta) { display: none; } + .hero { width: calc(100% - 2rem); gap: 3rem; padding: 4rem 0; } + .hero h1 { font-size: clamp(3.2rem, 16vw, 5.5rem); } + .trace-line { grid-template-columns: 28px 1fr; } + .trace-line i, .trace-line small { display: none; } + .checker-shell { width: calc(100% - 1rem); padding: 1rem; border-radius: 18px; } + .checker-topbar, .results-header, .suggestion-heading { align-items: start; flex-direction: column; gap: 1rem; } + .progress-block { width: 100%; text-align: left; } + .checker-tools { flex-wrap: wrap; } + .search-box { flex-basis: 100%; } + .option-grid { grid-template-columns: 1fr; } + .question-group summary { gap: 1rem; padding: 1rem; } + .result-actions { width: 100%; } + .result-actions .button { flex: 1; } + .result-card-head { grid-template-columns: auto 1fr; } + .confidence { grid-column: 2; } + .result-why { padding-left: 1rem; } + .result-detail { grid-template-columns: 1fr; } + .method-strip { width: calc(100% - 2rem); grid-template-columns: 1fr; } + .method-strip > div { border-right: 0; border-bottom: 1px solid var(--line); } + footer { width: calc(100% - 2rem); grid-template-columns: 1fr; } +} + +@media print { + body { color: #111; background: #fff; } + .site-header, .hero, .suggestion-panel, .checker-tools, .question-stack, .result-actions, .method-strip, footer { display: none !important; } + .checker-shell { width: 100%; margin: 0; border: 0; box-shadow: none; background: #fff; } + .results { border: 0; } + .result-grid { grid-template-columns: 1fr; } + .constellation { display: none; } + .result-card { break-inside: avoid; color: #111; background: #fff; } + .result-card details { display: block; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; } +} diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..429ecca --- /dev/null +++ b/docs/index.html @@ -0,0 +1,145 @@ + + + + + + + + + DeckMD — Steam Deck symptom checker by DeckDoc + + + + + + + + + +
+
+
+

Steam Deck symptom intelligence · runs entirely in your browser

+

Don’t guess the fix.
Narrow the failure.

+

A symptom can cross six layers before it reaches your screen. Check what + happened, what survived, and when it changed. DeckMD ranks the next diagnostic branch and + shows the evidence needed to confirm it.

+ +
    +
  • No uploads
  • No analytics
  • No account
  • No automatic fixes
  • +
+
+
+
PATH TRACELOCAL
+
01SYMPTOMwhat you see
+
02TRANSITIONwhat changed
+
03SURVIVORSwhat still works
+
04EVIDENCEwhat the Deck logged
+
Resultapp → OS → path → hardware
+
+
+ +
+
+
+

Interactive triage matrix

+

Build the incident fingerprint

+
+
+ 0 of 8 sections touched + +
+
+ + + +
+
+

Adaptive follow-up

Next best checks

+

Choose a primary symptom and DeckMD will offer the answers that split its major branches.

+
+
+
+ +
+ + + +
+ +
+ +
+
+

Live interpretation

What your answers point toward

+
+ + +
+
+ + + +
+ +
+
+ +
+ Your selected incident facts 0 selected +
+
+
+
+ +
+
01Observe

Describe physical behavior before naming a cause.

+
02Correlate

Align the symptom with the same boot and timestamp.

+
03Contrast

Change one model, path, version, or peripheral at a time.

+
04Verify

Re-run evidence and physically confirm the outcome.

+
+
+ +
+
DeckMD

An evidence-first interface for the open DeckDoc toolkit.

+

This is not medical advice, despite the name. It is also not Valve, Steam Support, a warranty + verdict, or electrical test equipment.

+ +
+ + + diff --git a/docs/research/steamdeck-issue-deep-dive.md b/docs/research/steamdeck-issue-deep-dive.md new file mode 100644 index 0000000..790a19d --- /dev/null +++ b/docs/research/steamdeck-issue-deep-dive.md @@ -0,0 +1,225 @@ +# Steam Deck issue deep dive: evidence, decision boundaries, and detector backlog + +**Research cutoff:** 2026-07-21. **Target:** Steam Deck LCD (Jupiter) and OLED (Galileo), SteamOS +3.x. **Current DeckDoc baseline:** 15 read-only modules and two guarded remediations. + +This audit is a diagnostic map, not a list of folk fixes. A linked issue proves only what its attached +logs and maintainer response establish. Reporter diagnoses, correlations, temperature guesses, and +“same here” comments remain hypotheses. Upstream bug reports are useful reproducible observations; +they are not proof that every similar Deck has the same root cause. A hardware verdict requires either +device-local electrical/physical evidence, persistence outside the installed OS, or Valve service +diagnosis—not merely a scary log string. + +## Evidence rules + +- **Incident time first.** Capture local time, timezone, boot ID, monotonic kernel timestamp, SteamOS + build/channel, client build, BIOS/firmware, LCD/OLED identity, dock/charger/display, title, Proton + version, and exact action. [`journalctl --list-boots` and `-b`](https://github.com/systemd/systemd/blob/main/man/journalctl.xml) + separate current (`-b 0`) from prior boots (`-b -1`); retained cores and persistent BTRFS counters + are historical until correlated. +- **Three layers before a verdict.** Symptom + subsystem evidence + controlled contrast. For example, + a frozen game plus `amdgpu ... timeout` plus recurrence across titles is stronger than a crash dump; + a live Gamescope process, active CRTC, connected eDP, and nonzero LCD backlight during a physically + black panel localize a render-to-scanout gap but still cannot inspect the panel electrically. +- **Absence is scoped.** “No error” means no error in the readable source and time window. Non-root + reports can miss kernel/debugfs/SMART/BTRFS data; volatile journals cannot explain an older reboot. +- **Prefer upstream semantics.** Kernel PM states are defined by the + [Linux sleep-state documentation](https://docs.kernel.org/admin-guide/pm/sleep-states.html), DRM + connector/CRTC properties by [DRM/KMS](https://docs.kernel.org/gpu/drm-kms.html), core-dump retention + by [`systemd-coredump`](https://github.com/systemd/systemd/blob/main/man/systemd-coredump.xml), + Proton logging/compatdata behavior by [Valve Proton](https://github.com/ValveSoftware/Proton), and + Gamescope’s compositor/direct-scanout role by [Valve Gamescope](https://github.com/ValveSoftware/gamescope). +- **Known-good contrast beats reset folklore.** Stable vs beta, stock vs third-party changes disabled, + docked vs direct, internal vs external display, cold boot vs resume, one title vs many, one AP vs + hotspot, and recovery environment vs installed OS are reversible discriminators. Valve’s + [basic guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28) explicitly starts with + updates, restart, and disabling third-party Desktop Mode software. +- **Recovery has a data-loss ladder.** Previous SteamOS is lower risk than repair/re-image; factory + reset, erase-user-data and re-image destroy data. Use Valve’s + [SteamOS recovery guide](https://help.steampowered.com/en/faqs/view/1B71-EDF2-EB6D-2BB3) + and record the pre-recovery state. + +### Confidence labels used below + +| Label | Meaning | +|---|---| +| **Confirmed mechanism** | Upstream docs define the signature, or a Valve maintainer ties the supplied trace to the mechanism. | +| **Strong local inference** | Two or more independent device signals fit one boundary; physical root cause still unproved. | +| **Reporter hypothesis** | Correlation, suspected component, workaround, or anecdote without causal confirmation. | + +## Cross-symptom decision matrix + +| Observable at incident | Most likely layer to investigate first | Evidence that moves the boundary | Evidence against / next branch | +|---|---|---|---| +| No LED/chime/fan/display | power input, battery/EC, board | official PSU direct, charge LED behavior, BIOS reachability; Valve documents LCD/OLED power-hold differences in the [basic guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28) | BIOS or recovery boots: installed OS/boot path, not “dead board” | +| Chime/haptics/audio, internal panel black | internal display path or compositor | eDP/EDID/backlight (LCD)/CRTC/planes live; external display; Gamescope state | missing connector/EDID/backlight/CRTC points lower; GPU timeout/reset points GPU path | +| Internal works, external does not | dock/cable/PD/Alt Mode/mode | USB topology, DRM connector, EDID, negotiated mode; cross-test device/cable/display per [Valve dock guide](https://help.steampowered.com/en/faqs/view/4C18-08B5-DEC9-3AF4) | other device also fails on dock: dock/cable/display; direct USB-C succeeds: dock branch | +| Whole image freezes, returns, then session restarts | GPU hang/reset/Gamescope recovery | earliest `amdgpu` timeout/reset lines and Gamescope core/restart; Valve maintainer says post-reset errors may be secondary in [SteamOS #1312](https://github.com/ValveSoftware/SteamOS/issues/1312) | title process alone dumps with no kernel/Gamescope event: game/Proton branch | +| Game exits to Library only | title, Proton, anti-cheat, mod, game | title dump, `PROTON_LOG`, exit code, same title/version across compatibility tools | multiple unrelated titles + GPU/OOM/I/O events: system layer | +| Sudden power-off | thermal protection, exhausted/failed power, kernel panic, board | last sensor/power samples, pstore/journal tail, charger state, recurrence under controlled load | normal shutdown target and clean journal: user/software shutdown; no retained logs is inconclusive | +| Slow/stutter without crash | thermal/power cap, memory pressure, storage I/O, shader compilation | load-normalized clocks/temp/power, PSI, swap I/O, disk latency, Steam download/shader activity | idle low clocks or allocated swap alone are normal, not evidence | +| Device missing after resume | PM/device reinitialization | paired suspend entry/exit and device driver error in same monotonic window; cold-boot restore | missing before suspend or still missing after recovery boot: config/hardware branch | +| Storage disappears/read-only/I/O errors | media/controller/filesystem | block/MMC/NVMe errors, RO transition, SMART, filesystem counters, another reader/system | mount/path/library issue with healthy block layer: config/client branch | +| Wi-Fi “connected,” internet broken | routing/DNS/AP/captive portal before firmware | link + address + route + gateway + DNS stages | interface vanished or firmware/PCI error: driver/device branch | +| Controls fail in one title/layout | Steam Input/game config | Steam controller test passes; clean layout/new title works | firmware menus/controller test also fail: firmware/input hardware branch | +| Symptom starts exactly after update | regression candidate, never proof alone | before/after build, previous-image A/B result, same hardware/config | persists on previous image/recovery: hardware/user-data/config branch | + +## Failure-family audit + +Each row gives the **strongest positive signature**, a falsifier or alternative, time scope, a decision +boundary, safe reversible test, forbidden action, escalation addition, current coverage, and one missing +detector. `CB` means current boot; `PB` previous boot; `H` persistent/historical. + +### Boot, power, battery, thermal, fan, and performance + +| Family / user symptom | Evidence and scope | Hardware / driver / config boundary | Safe reversible test; forbid | Escalation addition; DeckDoc coverage -> gap | +|---|---|---|---|---| +| No power / power button ignored | Charge LED + official PSU direct + BIOS accessibility are strongest. A flashing LED on press means depleted battery per [Valve basic guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28). **CB/live**, often no OS log. | BIOS/recovery reachable argues against dead mainboard; installed OS failure remains. No response after known-good PD source and official timing is hardware/EC suspicion, not proof. | Charge ≥15 min; use Valve LCD/OLED-specific hold timing; try Volume+ BIOS. **Forbid:** random charger injection, opening unit, battery disconnect, repeated forced cycles before data capture. | LED color/pattern, charger wattage, last successful boot. `battery_pmic` partial -> add preboot questionnaire + USB-PD/Type-C status detector. | +| Boot loop / logo / emergency shell | PB journal, boot target failure, mount errors, slot/build identity. Valve exposes previous-image and recovery options in [recovery guide](https://help.steampowered.com/en/faqs/view/1B71-EDF2-EB6D-2BB3). **PB/H**. | Previous image boots: regression/current deployment. Recovery boots but both images fail: installed storage/filesystem/user config. Recovery also fails: media/firmware/hardware branch. | Photograph exact error; boot previous image; recovery “repair” only after backup. **Forbid:** re-image/factory reset before evidence, blind `fsck` on mounted root, editing boot entries from guesses. | Both slot versions, boot menu result, PB journal/export. `fs_integrity` partial -> add deployment/slot/boot-health inventory. | +| Battery drains fast / percentage jumps | `POWER_SUPPLY_*` energy/charge/full/design, current, voltage and cycle data over time; workload and refresh rate. Valve notes charge may intentionally fall below 100% while continuously plugged in ([basic guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28)). **live/trend**. | One percentage snapshot cannot prove bad cells. Repeatable capacity collapse at comparable load plus abnormal full/design ratio raises battery suspicion; one title/high refresh is workload/config. | Reboot, stock performance settings, known workload, record 10–15 min trend. **Forbid:** forced discharge, calibration loops, changing charge limits/EC/BIOS, generic voltage cutoffs. | Charger, load, FPS/refresh cap, elapsed Wh. `battery_pmic` snapshot -> model-aware energy trend + plausibility detector. | +| Will not charge / slow charge | Online/status/current plus Type-C/PD role/partner data; official adapter direct. Steam Deck uses USB-PD, not QC/Fast Charge ([Valve](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28)). **live**. | Charges direct but not docked: dock/cable/PD topology. Multiple known-good PD supplies + port debris ruled out + no contract: port/controller/hardware suspicion. | Direct official PSU, inspect port without metal, shut down and retest. **Forbid:** untrusted high-voltage supplies, probing/shorting port, firmware downgrade. | Supply/cable/dock IDs and Type-C sysfs. `battery_pmic` partial -> USB-PD contract/role and charger-power detector. | +| Hot, fan loud/zero, throttling, shutdown | Sensor-specific temp/max/crit, fan RPM/PWM, load-normalized CPU/GPU clocks, thermal-zone events and journal tail. Kernel hwmon ABI defines exported fields ([kernel](https://docs.kernel.org/hwmon/sysfs-interface.html)). **live/trend/PB**. | Zero RPM at idle or missing hwmon export is not failed fan. Rising temperature under sustained load with no RPM on a known fan channel is strong hardware/control evidence; update-correlated fan controller errors suggest driver/EC. | Clear vents, stock TDP/fan profile, short monitored load, stop before exported critical threshold. **Forbid:** blocking fan, overriding PWM, repasting/opening, disabling thermal protection. | Ambient/load/time curve, shutdown time, sensor labels. `thermal_fan` snapshot -> 60 s labeled trend, fan response slope, shutdown-correlation detector. | +| Low clocks / stutter / “200/400 MHz” | Sustained per-engine clocks under measured load + temp/power/current + PSI/swap/I/O, not an idle sample. AMDGPU exposes utilization/frequency/debug semantics in [kernel GPU docs](https://docs.kernel.org/gpu/amdgpu/index.html). **live/trend**. | Low idle clock is normal. Low clocks only while hot/power-limited is policy/protection; fixed low clocks across cold stock workloads and boots raises firmware/hardware suspicion. | Compare stock profile, plugged/unplugged, two workloads; record 1 s series. **Forbid:** manual clock/voltage/SMU writes or disabling limits. | Performance profile, charger, FPS cap. `gpu_apu`,`memory_swap`,`thermal_fan` partial -> correlated performance sampler and cause ranking. | + +### Internal/external display, GPU/APU, dock, and USB-C + +| Family / user symptom | Evidence and scope | Hardware / driver / config boundary | Safe reversible test; forbid | Escalation addition; coverage -> gap | +|---|---|---|---|---| +| Internal LCD black but audio/input live | During physical black: eDP connected, readable EDID, nonzero backlight, active CRTC/plane, live Gamescope, no preceding GPU reset. DRM meanings come from [DRM/KMS](https://docs.kernel.org/gpu/drm-kms.html). **live/CB**. | All scanout state live = compositor/scanout/panel-link boundary, not proof of software. No backlight on LCD, missing EDID/CRTC, or failure in BIOS/recovery moves toward panel/cable/hardware. OLED has no conventional backlight. | Mark `--display-black`, test external display, session-only forced composition only when prechecks pass. **Forbid:** sysfs backlight/panel-power writes, guessed modesets, persistent tweak before session test. | Photo/video, external result, physical observation. `display_blackout` strong LCD partial + `rem_display_blackout` -> OLED-aware emitted-pixel/manual branch and model capability manifest. | +| Black screen on Desktop/Game Mode transition | Gamescope/KWin start/exit, core, DRM hotplug/modeset, active session and user journal. A SteamOS report documents backlight/live-system behavior but its cause remains reporter-level ([#1324](https://github.com/ValveSoftware/SteamOS/issues/1324)). **CB**. | Session process/core/modeset error with other mode working: software/config. Same black in BIOS/recovery: hardware. | Switch mode once, SSH capture, disable third-party session plugins, compare previous image. **Forbid:** delete all KDE/Steam configs before copying them, re-image first. | Exact transition, displays, config diff. `gamescope_session`,`display_blackout`,`coredump` partial -> session-transition timeline detector. | +| Artifacts/flicker/scanout corruption | Photo plus DRM mode/planes, Gamescope composition state, kernel version, `amdgpu` errors. Gamescope #1368 reproduces direct-scanout artifacts on multiple AMD/display setups and calls its kernel attribution a **best guess**, while force composition changes outcome ([issue](https://github.com/ValveSoftware/gamescope/issues/1368)). **CB/repro**. | Artifact absent in BIOS/recovery and changes with composition/mode = software path. Persistent before OS/on captures from external frame source = display/GPU hardware suspicion. | Stock refresh/resolution; session-only composition; internal/external A/B. **Forbid:** over/underclock, EDID deletion without backup, permanent kernel flags from anecdotes. | Photo, mode, plane count, kernel/Mesa/Gamescope. `display_blackout`,`gpu_apu` partial -> artifact incident mode/composition diff detector. | +| External display blank / wrong mode / HDR | DRM connector/EDID/link-status, USB topology, mode list, dock firmware, cable and direct-USB-C A/B. Valve lists cable/input/mode limitations and reset/cross-device tests ([dock guide](https://help.steampowered.com/en/faqs/view/4C18-08B5-DEC9-3AF4)). **live/CB**. | Another host also fails through dock = dock/cable/display. Direct Deck USB-C succeeds = dock. Connector absent with other functions working = Alt Mode/link. | Power-cycle dock per Valve, known-good cable/input, conservative 1080p60, one display. **Forbid:** flash unofficial dock firmware, force unsupported timings/HDR/DSC, hot-plug damaged cable repeatedly. | Full topology/EDID hash and good/bad combinations. `display_blackout` connector-only -> dock/USB/PD/Alt-Mode module. | +| USB peripherals disconnect / dock instability | `usb`/`xhci` resets, descriptor errors, device tree before/after, Type-C partner/role, power budget; correlate exact monotonic time. **CB**. | One device/cable only = peripheral. Whole hub resets with multiple peripherals = dock/controller/power. Repeat direct on Deck = Deck port/controller/OS. | Remove nonessential devices, direct-test one known-good low-power device, update official dock firmware. **Forbid:** `usbreset` loops, unbind host controller remotely, powered-hub backfeed. | `lsusb -t`, IDs, cable/dock PSU. no dedicated coverage -> topology/event-correlation detector. | +| GPU hang, freeze, black/recover, Gamescope restart | Earliest `amdgpu` ring timeout/page fault/reset begin/outcome, then Gamescope/core timeline. In SteamOS #1312 a Valve maintainer says post-reset Gamescope/fan errors may be secondary and requests earlier AMDGPU lines ([issue](https://github.com/ValveSoftware/SteamOS/issues/1312)). **CB/PB**. | One title/API/Proton only and reset succeeds: driver/game first. Cross-title, stock, repeated failed reset or machine check raises kernel/hardware; a reset line alone does not identify faulty silicon. | Capture PB journal after reboot, A/B Proton/API/title and stable/previous image. **Forbid:** manual PCI/GPU reset/sysfs power writes, voltage/clocks, repeated stress after failed resets. | Full journal from ≥2 min before event, dumps, title/API. `gpu_apu`,`dxvk_page_fault`,`gamescope_session`,`coredump` good partial -> unified causal timeline/first-error detector. | +| Visual glitch/game crash blamed on VRAM/GPU | VM fault address/status/client IDs, guilty process if logged, ring and reset result. Kernel AMDGPU docs expose mechanisms, not a board-failure verdict ([AMDGPU](https://docs.kernel.org/gpu/amdgpu/index.html)). **CB**. | Fault confined to one renderer/title/build points software/submission. ECC/PCI/AER/machine-check evidence or broad stock recurrence is stronger hardware evidence; Deck shared memory makes “VRAM failure” from symptoms speculative. | Clean launch options/mods, alternate Proton/render API, multiple known titles. **Forbid:** memory “repair,” UMA/BIOS tweaks as diagnosis, RMA claim from one VM fault. | Fault block untruncated + process/title/version. `dxvk_page_fault` partial -> decoder keyed to current kernel plus cross-title recurrence grouping. | + +### Audio, Wi-Fi, Bluetooth, suspend, and input + +| Family / user symptom | Evidence and scope | Hardware / driver / config boundary | Safe reversible test; forbid | Escalation addition; coverage -> gap | +|---|---|---|---|---| +| Speakers/headphones silent | ALSA cards/devices, PipeWire/WirePlumber nodes/default route, mute/volume, external audio sinks, SOF firmware/IPC/panic. Valve says disconnect alternate BT/USB-C/3.5 mm sinks and switch output ([basic guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28)). **live/CB**. | ALSA device exists and alternate sink works: routing/profile/config. SOF IPC/panic and missing card after resume: driver/DSP. Speaker absent in recovery/known-good image while headphones work: speaker/wiring suspicion. | Switch output away/back, restart, compare headphones/speakers; guarded Vangogh reload only on exact trigger/model. **Forbid:** force-remove busy audio modules, delete all PipeWire config, apply LCD driver name to OLED. | Sink/source/profile, jack state, resume time. `audio_sof` strong partial + guarded remediation -> codec/jack/route and OLED model detector. | +| Mic missing/wrong after 3.5 mm | PipeWire source profile/port and ALSA capture devices. Valve documents LCD internal mic unavailable with 3.5 mm, while OLED can use both ([basic guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28)); SteamOS #1346 shows profile/port evidence but workaround comments are not a root-cause confirmation ([issue](https://github.com/ValveSoftware/SteamOS/issues/1346)). **live**. | Expected LCD behavior vs OLED route bug vs physical mic decided by model and available ports, not “missing mic” alone. | Record model, unplug jack, inspect/select exposed profile, short local recording. **Forbid:** editing UCM/WirePlumber files before backup, assuming LCD behavior on OLED. | `pactl`/WP JSON, jack/headset. `audio_sof` partial -> model-aware capture route/profile detector. | +| Wi-Fi missing/disconnects | PCI/USB adapter ID, driver/firmware, interface transitions, NetworkManager state, signal, deauth reason, paired resume window. **CB**. | Interface present + associated but no internet = route/DNS/AP first. Firmware/PCI error or vanished interface = driver/device. One AP only = AP/security/channel compatibility. | Cold boot, toggle Wi-Fi, known-good hotspot, disable third-party networking, compare pre/post-resume. **Forbid:** unload guessed module, delete all connections before capture, regulatory/channel hacks. | AP band/security (redact SSID/BSSID), adapter/driver/fw. `wifi_firmware`,`acpi_pm_state` partial -> staged link/IP/route/gateway/DNS detector and model-aware drivers. | +| Connected but no internet / slow Wi-Fi | Association + IP + default route + gateway reachability + DNS resolution + throughput/signal/retry; each stage timestamped. **live/trend**. | Gateway fails = link/AP/routing. Gateway passes but DNS fails = resolver. Only Steam fails = client/service. Low signal/retries and 2.4/5/6 GHz context distinguish RF. | Compare hotspot, gateway vs literal IP vs DNS, dock Ethernet. **Forbid:** public ping flood, global DNS replacement as first step, exposing network identifiers. | Stage results, AP band/channel, VPN/captive portal. no current coverage beyond link -> connectivity-stage module. | +| Bluetooth cannot pair/reconnect / latency | Adapter/driver/firmware, `bluetoothd` journal, rfkill, discovery/pair/connect transitions, codec/profile. Valve recommends show-all, device firmware, reconnect/re-pair, then A2DP codec change for latency ([basic guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28)); dock placement/2.4 GHz coexistence matters ([dock guide](https://help.steampowered.com/en/faqs/view/4C18-08B5-DEC9-3AF4)). **CB/live**. | One device only = peripheral firmware/profile. Adapter absent/firmware error = Deck driver/device. Docked-only/interference-dependent = RF topology. | Reconnect; after capture forget only affected device; update peripheral firmware; line-of-sight/docked A/B. **Forbid:** erase all bonds, reset adapter USB blindly, force codecs unsupported by device. | Device class/vendor (redact address), profile/codec. no module -> Bluetooth state-machine/coexistence detector. | +| Suspend fails / immediate wake / dead after wake | Paired `PM: suspend entry`/exit, chosen sleep state, wake source, driver suspend/resume error, device state before/after. Linux defines `freeze`, `standby`, `mem` and disk states ([kernel PM](https://docs.kernel.org/admin-guide/pm/sleep-states.html)). **CB/PB**. | A nearby error is not causal by proximity alone. Repeat with one device removed identifies config/peripheral. Device absent only after resume is reinit path; failure across previous image/recovery raises firmware/hardware. | Reboot, remove dock/SD/BT one at a time, short controlled sleep, capture immediately. **Forbid:** repeated suspend loops on overheating/failing storage, changing ACPI/kernel params from generic PC guides. | Sleep state, wake source, dock/power/title. `acpi_pm_state` partial -> monotonic suspend transaction and per-device delta detector. | +| Buttons/sticks/trackpads/touch/gyro fail | Steam’s Controller Test, kernel input device inventory, event activity (without recording user content), calibration/firmware, whether BIOS/recovery and multiple titles reproduce. Valve provides Controller Test and notes controller firmware needs ([basic guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28)). **live**. | Test passes but one layout/title fails = Steam Input/config. Test and firmware UI fail for one physical control = hardware/calibration suspicion. Whole device missing after resume = driver/USB/HID. | Default template, Controller Test, restart, compare before/after wake; external controller A/B. **Forbid:** calibration writes, controller firmware flashing outside official flow, logging raw keyboard events. | Control name, test screen result, firmware, wake/update. no module -> privacy-preserving input inventory/event-count/firmware detector. | +| Touchscreen phantom/missing | DRM/input mapping, touch device presence, bounded contact counts, dmesg HID/I2C errors, BIOS/recovery behavior. **live/CB**. | Wrong rotation/display mapping in Desktop only = config. Missing/phantom touches across recovery with clean/dry panel = digitizer/hardware suspicion. | Clean/dry screen, restart, internal-only display, Controller/UI test. **Forbid:** raw input capture containing gestures/typing, pressure on panel, calibration-file deletion. | Screen condition, charger/dock state, video. no module -> touch device/mapping/error detector. | + +### Storage, NVMe, microSD, and filesystems + +| Family / user symptom | Evidence and scope | Hardware / driver / config boundary | Safe reversible test; forbid | Escalation addition; coverage -> gap | +|---|---|---|---|---| +| NVMe warnings / I/O errors / disappearance | NVMe controller resets/timeouts, block I/O errors, SMART critical warning/media/data-integrity/error-log fields, device identity/temperature. The [smartmontools NVMe printer](https://www.smartmontools.org/browser/trunk/smartmontools/nvmeprint.cpp) is the upstream implementation that labels these fields. **CB + lifetime H counters**. | Error-log count alone can be historical/nonfatal. New media errors/critical warnings plus I/O failures across boots strongly implicate drive/path; mount/library failure without block evidence is software. | Stop writes, back up, read-only SMART, cold boot/recovery visibility. **Forbid:** destructive self-test/format, firmware flash, repeated benchmarks, `badblocks` on live data. | Raw SMART, exact device, PB journal, backup status. `storage_smart` fixed path -> dynamic NVMe discovery, counter baselines/deltas, controller-reset timeline. | +| microSD not seen / read-only / corrupt games | mmc enumeration, card CID/model/size, RO state, mmc/SDHCI timeouts/CRC, ext4 errors, mount and Steam library state. Valve specifies UHS-I class 3+ and formats through Settings ([basic guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28)). **CB/H FS**. | Card fails in another trusted reader/system = card. Multiple cards fail only in Deck = slot/driver. Block device healthy but Steam library absent = mount/client config. | Power down before reseat, read-only copy/backup, known-good card/reader A/B. **Forbid:** format when data matters, repeated trim/write tests, counterfeit-capacity test before imaging, bending/cleaning with liquid. | Card identity/size, another-reader result, new errors. `mmc_sd_card` good partial -> card-health delta, dynamic FS mapping, explicit library/mount split. | +| Filesystem errors / forced read-only | First kernel error, mount flags, ext4 superblock state/error counters or BTRFS device stats, underlying block errors. BTRFS says device stats are persistent I/O error-class records ([`btrfs-device(8)`](https://btrfs.readthedocs.io/en/latest/btrfs-device.html)); ext4 error behavior is documented in [kernel ext4](https://docs.kernel.org/filesystems/ext4/index.html). **CB + H counters**. | FS counter with no timestamp does not date incident. Underlying I/O/media errors shift to device; clean device plus reproducible metadata error shifts FS/software. | Stop writes, backup, read-only inspection; offline check only from correct recovery guidance. **Forbid:** `fsck` on mounted FS, `btrfs check --repair`, clearing counters before capture, remount-RW to “see if fixed.” | First error through RO transition, device SMART. `fs_integrity` partial -> mount graph, counter baseline/delta, RO transition classifier. | +| Full disk / update/game install fails | `df` + inode use, Steam library/content logs, journal size/coredump retention, immutable root vs `/home` allocation. `systemd-coredump` explains external core storage/limits ([upstream source](https://github.com/systemd/systemd/blob/main/man/systemd-coredump.xml)). **live/H**. | Space exhaustion is config/retention, not failing flash. Free space with I/O/FS errors moves storage path. | Use Steam Storage UI; inventory largest categories and cores; remove only known cache/content through supported UI after capture. **Forbid:** recursive deletion under system paths, hand-editing immutable root, deleting unknown compatdata/saves. | Per-filesystem blocks/inodes and top categories, redacted. `coredump_analysis`,`steam_client_logs` partial -> capacity/inode/retention detector. | + +### Steam client, Gamescope, Proton, games, updates, and immutable OS + +| Family / user symptom | Evidence and scope | Hardware / driver / config boundary | Safe reversible test; forbid | Escalation addition; coverage -> gap | +|---|---|---|---|---| +| Steam client/Game Mode crashes or restarts | Client dump/core, user-unit exit/status, Gamescope start/stop/core and session boundary. Core means signal termination, not cause ([systemd-coredump](https://github.com/systemd/systemd/blob/main/man/systemd-coredump.xml)). **CB/H retained**. | Steam-only core with kernel clean = client/plugin/config first. Gamescope core after AMD reset is likely downstream; earliest event rules. | Stable client, disable plugins/themes, reproduce once, preserve dump/backtrace. **Forbid:** delete all userdata/config, upload unsanitized dumps, infer current incident from an old core. | Client/build/channel, plugin list, exact core metadata. `coredump_analysis`,`gamescope_session`,`steam_client_logs` partial -> incident joiner by boot/time/process ancestry. | +| Gamescope crash / black session | Gamescope backtrace/core, exact build/args/backend, Wayland/DRM/Vulkan preceding errors. Gamescope #1434 includes a reproducible cursor-enter crash, requested backtrace and fixing PR; later scaling comments may describe a separate issue ([issue](https://github.com/ValveSoftware/gamescope/issues/1434)). **CB/repro**. | Repro tied to action/build/backend and fixed by known version = software. No process failure and panel also black in BIOS = not Gamescope. | Stable/previous image, stock session args, remove overlays, capture backtrace. **Forbid:** transplant unrelated launch flags, persistent compositor policy from one anecdote. | Repro action + full backtrace + DRM state. `gamescope_session`,`coredump` partial -> symbol/build/backtrace fingerprint detector. | +| One game fails to launch / crashes | Title-specific Steam log, dump, exit code, `PROTON_LOG=1` output, Proton version, prefix age, launch options/mods/anti-cheat. Valve’s [Proton issue template](https://github.com/ValveSoftware/Proton/issues) requests system information and logs; DXVK and VKD3D-Proton are distinct translation layers ([DXVK](https://github.com/doitsujin/dxvk), [vkd3d-proton](https://github.com/HansKristian-Work/vkd3d-proton)). **live/H per title**. | One title/Proton/API only = compatibility/game/config. Multiple unrelated titles with shared GPU/OOM/I/O signature = system. Anti-cheat/service refusal is not hardware. | Verify files via Steam, clear launch options/mods, try Valve-supported Proton versions one at a time, preserve prefix first. **Forbid:** delete compatdata containing saves before backup/cloud check, install random DLLs/scripts, call one crash hardware. | App ID, Proton and game build, sanitized log/dump, save status. `steam_client_logs`,`dxvk_page_fault`,`coredump` partial -> title-scoped bundle and prefix mutation inventory. | +| Shader stutter / poor performance | Frame-time trace, shader pre-caching/download state, CPU/GPU utilization, translation-layer log, cache warm/cold A/B. **live/trend**. | First-run/cache-warm improvement = compilation. Persistent load-normalized cap with thermal/PSI/I/O evidence moves system. Average FPS alone hides stutter cause. | Repeat same scene after warm-up; stock cap/resolution; compare one Proton version. **Forbid:** delete all shader caches first, disable safety limits, benchmark while downloads run. | Frame-time sample, scene, cache/download state. performance modules partial -> frame-time/context collector (opt-in) and background-activity detector. | +| OOM / app killed / swap thrash | `oom-kill` victim/cgroup, PSI, MemAvailable, swap-in/out, kernel allocation failure at incident. Kernel PSI defines CPU/memory/I/O stall signals ([kernel PSI](https://docs.kernel.org/accounting/psi.html)). **CB/live**. | Allocated or 50% used swap alone is not failure. OOM victim plus sustained memory PSI is decisive for memory pressure; GPU reset or I/O error may be separate. | Close overlays/browser/mods, stock title, short PSI/vmstat trend. **Forbid:** disable OOM protections, arbitrary huge swap, memory stress after instability. | Victim, cgroup, workload, PSI window. `memory_swap` partial -> PSI/cgroup victim/time-series detector. | +| Update failed / rollback / regression | Old/new OS, kernel, BIOS, client and Mesa/Gamescope versions; updater/deployment logs; selected slot; same repro on previous image. Valve documents previous-image/recovery hierarchy ([recovery](https://help.steampowered.com/en/faqs/view/1B71-EDF2-EB6D-2BB3)). **CB/PB/H**. | Previous image fixes with unchanged user data/hardware = strong regression. Both slots fail but clean user/recovery succeeds = user config. All environments fail = hardware/peripheral branch. | Retry on reliable power/network; previous image A/B; stable channel; back up. **Forbid:** interrupt update, manual package replacement on immutable root, re-image before export. | Exact builds/slot/updater status and A/B table. no dedicated module -> deployment/slot/update health detector. | +| Read-only/immutable root or modifications lost | Mount source/options, overlay/state, `steamos-readonly` status if present, pacman/local modification inventory. SteamOS system immutability is policy; inability to edit root is not corruption. **live**. | Expected read-only root = configuration model. Unexpected mount/verity/deployment error plus boot failure = image/storage. Third-party root changes widen unsupported state. | Record changes; use Flatpak/user config; revert only known customization; official repair path. **Forbid:** disable read-only as a generic fix, system-wide `pacman -Syu`, overwrite OS files, hide modifications from reports. | Modification manifest and readonly state. no module -> immutable-root/deviation detector with secret-safe hashes. | +| Plugins/mods/overlays cause UI or title issues | Plugin loader/service versions, injected processes/layers/env, difference with all third-party components disabled. Valve’s first steps explicitly request disabling third-party Desktop Mode applications ([basic guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28)). **CB/repro**. | Clean stock repro = not eliminated but less likely. Symptom disappears with one component disabled and returns on re-enable = strong config causality. | Disable, do not delete; binary search components; record versions. **Forbid:** purge userdata, blame plugin from mere presence, upload tokens/config secrets. | Plugin/layer/version list and A/B result. `steam_client_logs` minimal -> third-party/injected-layer inventory detector. | + +## Hardware-failure decision standard + +Classify conservatively: + +1. **Fixable configuration / application:** stock hardware and OS paths function; failure follows a + title, layout, route, plugin, AP, mode, prefix, cable, or setting; reversible A/B changes outcome. +2. **Driver / firmware / OS:** a timestamped kernel/session/device transition fails, a previous build + changes outcome, or a reproducible upstream signature matches. This is still not proof of a + particular source file unless a maintainer/bisect confirms it. +3. **Strong hardware suspicion:** reproducible in BIOS or official recovery, across both OS slots and + stock config, with known-good external components; or device-local evidence such as new media + errors, failed enumeration across environments, consistent physical-control failure, or electrical + abnormality. Stop risky testing and escalate. +4. **Confirmed hardware failure:** Valve/service-center diagnosis, component-level electrical test by + a qualified technician, or replacement of the isolated component resolves the controlled repro. + +Never label `amdgpu` timeout, coredump, nonzero BTRFS counter, swap use, hot chassis, low idle clock, +zero idle fan RPM, or “black screen” alone as hardware failure. + +## Universal escalation packet + +Generate one sanitized archive containing: + +- UTC/local incident time, timezone, boot ID and `journalctl --list-boots`; current and previous boot + journal windows around the event (kernel + relevant user session), plus pstore if present; +- LCD/OLED/model/board/APU, BIOS, SteamOS build/channel/slot, kernel, Mesa, Gamescope, Steam client, + Proton/title build, dock firmware, adapter/driver/firmware, charger/cable/display IDs as applicable; +- the smallest exact repro, frequency, last known good, update/resume/dock correlation, and a compact + known-good A/B table; +- DeckDoc master report plus relevant module logs, untruncated first-error block, core metadata and + backtrace where available; keep raw dump local unless support requests it; +- physical observations software cannot see: LEDs/chime/fan, pixels/backlight, heat/odor/liquid/drop, + port/cable condition, and BIOS/recovery result; +- backup status and every test/change already attempted, with rollback result. + +Redact usernames, home paths, SSIDs/BSSIDs/MAC/IPs, hostnames, serials unless Valve Support requires +them, account/session tokens, launch credentials, chat text, and game save content. Preserve hashes +and relative timing so two reports can still be compared. + +## DeckDoc detector backlog (issue-ready) + +Priorities use **P0** = prevents unsafe/wrong verdicts, **P1** = high-frequency localization, **P2** = +depth/convenience. Each detector must emit `OBSERVED`, `NOT_OBSERVED`, or `INACCESSIBLE`; include boot +and time scope; never turn absence into “healthy.” + +| Priority | Proposed issue / detector | Minimum safe output and acceptance boundary | Extends | +|---|---|---|---| +| P0 | Model/capability manifest | LCD/OLED, board/APU, panel/backlight capability, Wi-Fi/BT/audio/storage drivers, dynamic DRM/block paths; fixtures for Jupiter/Galileo and unknown hardware | all modules; removes fixed-path/model assumptions | +| P0 | Incident timeline correlator | normalize realtime/monotonic + boot ID; order first AMDGPU/OOM/I/O/PM/device event before downstream cores/restarts; never claim causation from proximity | GPU, DXVK, Gamescope, cores, PM, storage | +| P0 | Evidence scope/access ledger | per source: CB/PB/H/live, requested window, permission/retention/read failure; distinguish empty from unreadable | runner/report schema | +| P0 | Safe escalation packager/redactor | manifest + selected logs + deterministic redaction preview; exclude raw cores/secrets by default; no system mutation | new report packaging | +| P0 | Storage risk gate | detect fresh block/media/FS/RO transition; recommend stop-write/backup; explicitly forbid mounted `fsck`, BTRFS repair, destructive tests | `storage_smart`,`fs_integrity`,`mmc` | +| P1 | Dynamic storage + counter delta | all NVMe/mmc/mount graph; SMART/BTRFS/ext4 counters with prior-report delta; no “new” claim without baseline | storage modules | +| P1 | Suspend transaction analyzer | pair entry/exit by monotonic time, selected sleep state/wake source, per-device before/after and errors; avoid loose “after suspend” attribution | `acpi_pm_state` | +| P1 | USB-C/dock/PD topology | Type-C role/partner/PD power where exported, USB tree resets, DRM connectors/EDID, dock firmware and direct-vs-dock questionnaire | new module | +| P1 | Network stage classifier | interface -> association -> address -> route -> gateway -> DNS; opt-in endpoint; redact identifiers; separate RF/firmware from internet | `wifi_firmware` | +| P1 | Bluetooth state-machine | rfkill/adapter/firmware, discover/pair/connect/profile/codec, resume and coexistence window; redact addresses | new module | +| P1 | Input/controller inventory | Steam input devices, driver/firmware, bounded event counts per control, test instructions; never record key values/text | new module | +| P1 | Display model-aware classifier | OLED path without fake backlight requirement; connector/EDID/CRTC/planes/Gamescope/GPU timeline; external A/B; physical-observation field | `display_blackout` | +| P1 | Boot/deployment/update health | slot/current/previous build, update status/logs, immutable mount state, recovery result questionnaire; no automatic rollback | new module | +| P1 | Load-correlated performance sampler | opt-in 60 s 1 Hz temp/fan/clocks/power/memory PSI/I/O, workload marker and stop thresholds; no stress generator | GPU/battery/thermal/memory | +| P1 | Title-scoped compatibility bundle | App ID/build, Proton version, sanitized per-title log, dump metadata, launch options hash, prefix age/lock/mutation inventory; warn about saves | Steam/DXVK/cores | +| P2 | Audio route/profile/jack model | ALSA -> PipeWire/WirePlumber graph, default route/profile/ports, jack and model-specific mic behavior; Vangogh remediation remains gated | `audio_sof` | +| P2 | Gamescope/core fingerprinting | build/backend/args, restart boundary, symbolized signature when locally available, distinguish normal end dump from crash | Gamescope/cores | +| P2 | Capacity/retention analyzer | blocks + inodes per FS, journal/core/cache categories, supported cleanup guidance only | cores/Steam/FS | +| P2 | Third-party deviation inventory | plugins, Vulkan layers, overlays, injected env/services, readonly-root changes as presence—not blame; secret-safe hashes | Steam/session | +| P2 | Trend/report comparison | compatible manifests, counter deltas and incident-aligned diff; never compare different models/paths as equivalent | runner/schema | + +### Suggested issue order + +1. Capability manifest, scope/access ledger, incident timeline, and escalation packager establish a safe + evidence substrate. +2. Storage risk gate, dock/PD, suspend transaction, network stages, Bluetooth, input, and deployment + health close the largest dangerous/uncovered families. +3. Model-aware display, dynamic storage, performance trend, and title-scoped bundle turn frequent + reports into reproducible upstream packets. +4. Audio routing, fingerprinting, capacity, deviations, and trend comparison deepen diagnosis after the + schema is stable. + +## Primary-source index + +The audit intentionally excludes Reddit, blogs, repair-shop claims, search snippets, and generic PC +fix lists. Representative upstream reports below are evidence examples, not universal diagnoses. + +| Authority | Diagnostic use | +|---|---| +| [Valve: Steam Deck basic use/troubleshooting](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28) | model-specific power timing; charging, display, audio/mic, BT, input, Wi-Fi, microSD | +| [Valve: Docking Steam Deck](https://help.steampowered.com/en/faqs/view/4C18-08B5-DEC9-3AF4) | dock reset, cable/device/display cross-tests, external modes, BT placement | +| [Valve: SteamOS recovery](https://help.steampowered.com/en/faqs/view/1B71-EDF2-EB6D-2BB3) | previous image, factory reset, repair and re-image/data-loss boundaries | +| [Valve SteamOS issues](https://github.com/ValveSoftware/SteamOS/issues), [Steam for Linux issues](https://github.com/ValveSoftware/steam-for-linux/issues) | reporter logs and Valve maintainer triage; treat unconfirmed diagnoses as hypotheses | +| [Valve Gamescope](https://github.com/ValveSoftware/gamescope), [Proton](https://github.com/ValveSoftware/Proton) | compositor/direct-scanout architecture, issue traces, compatibility logging | +| [Linux DRM/KMS](https://docs.kernel.org/gpu/drm-kms.html), [AMDGPU](https://docs.kernel.org/gpu/amdgpu/index.html), [PM sleep](https://docs.kernel.org/admin-guide/pm/sleep-states.html), [hwmon](https://docs.kernel.org/hwmon/sysfs-interface.html), [PSI](https://docs.kernel.org/accounting/psi.html) | kernel signal semantics | +| [systemd journalctl](https://github.com/systemd/systemd/blob/main/man/journalctl.xml), [systemd-coredump](https://github.com/systemd/systemd/blob/main/man/systemd-coredump.xml) | boot/time selection, retention and meaning of core metadata | +| [BTRFS docs](https://btrfs.readthedocs.io/en/latest/), [ext4 docs](https://docs.kernel.org/filesystems/ext4/index.html), [smartmontools](https://www.smartmontools.org/) | persistent counters, filesystem and SMART boundaries | +| [Mesa](https://docs.mesa3d.org/), [DXVK](https://github.com/doitsujin/dxvk), [vkd3d-proton](https://github.com/HansKristian-Work/vkd3d-proton), [SOF](https://thesofproject.github.io/latest/) | graphics translation/driver and DSP evidence; version-specific upstream context | diff --git a/docs/wiki/Audio-Problems.md b/docs/wiki/Audio-Problems.md new file mode 100644 index 0000000..5fbca2c --- /dev/null +++ b/docs/wiki/Audio-Problems.md @@ -0,0 +1,79 @@ +# Audio problems and SOF DSP failures + +Audio can fail at the game, Steam, PipeWire, ALSA, codec, Bluetooth, USB, or kernel DSP layer. DeckDoc's +strongest audio diagnosis is the SOF DSP failure that can follow suspend/resume. + +## First classify the symptom + +- One game only or all system audio? +- Speakers, 3.5 mm, USB, HDMI/DP, Bluetooth, or every output? +- Output missing, muted, distorted, crackling, or silent? +- Did it begin immediately after wake? +- Does `Settings -> Audio` still list the expected output? + +Run: + +```bash +sudo ./deckdoc.sh +``` + +Read `module_audio.log`, then correlate `module_acpi.log`, `module_coredump.log`, and the incident time. + +## SOF DSP signature + +Strong kernel signals include: + +```text +DSP panic +ipc tx ... failed ... -22 +ipc ... timed out +Failed to restore pipeline after resume +Failed to acquire HW lock +``` + +Confidence rises when ALSA cards/playback devices and PipeWire nodes disappear in the same boot after a +resume event. A healthy card with only the wrong sink selected is a different problem. + +Upstream SteamOS reports document post-sleep audio loss and the Vangogh IPC `-22` pattern, sometimes +alongside a wireless failure. + +## Safe response + +Preserve a diagnostic report first. A normal restart is the safest general recovery and does not prove +root cause. + +DeckDoc can attempt its Vangogh-specific driver reload only when the trigger is present: + +```bash +sudo ./deckdoc.sh --fix +``` + +The remediation backs up module/card state, reloads `snd_sof_amd_vangogh`, checks for new errors, and +verifies audio-card presence. It can fail when the module is busy or firmware is wedged. If the driver +or device model differs, skip this fix rather than substituting a guessed module name. + +After a reported success, confirm actual sound through the intended output. Device enumeration alone is +not proof that speakers/headphones work. + +## Other audio branches + +- **Only one game:** check in-game output, Proton/game logs, and title-specific compatibility. +- **Bluetooth only:** DeckDoc does not yet diagnose Bluetooth; record adapter/profile/codec and reconnect + behavior. +- **Dock/HDMI only:** correlate external connector state and test the dock/cable/display chain. +- **3.5 mm or speaker only:** compare outputs and consider a codec/jack/hardware path. +- **PipeWire node missing but ALSA healthy:** inspect the active user session and PipeWire services. + +Avoid deleting all PipeWire configuration, changing kernel modules blindly, or reinstalling SteamOS +before determining whether the failure is global, output-specific, or resume-correlated. + +## Escalation evidence + +Include model, SteamOS version/channel, output device, wake context, exact time, SOF journal excerpt, +ALSA/PipeWire presence, and whether a restart or guarded reload recovered it. + +## References + +- [SteamOS #1376: No sound after sleep](https://github.com/ValveSoftware/SteamOS/issues/1376) +- [SteamOS #2313: SOF Vangogh IPC -22 and Wi-Fi after resume](https://github.com/ValveSoftware/SteamOS/issues/2313) +- [SOF project issue #9095](https://github.com/thesofproject/sof/issues/9095) diff --git a/docs/wiki/Black-Screen-After-Long-Shutdown.md b/docs/wiki/Black-Screen-After-Long-Shutdown.md new file mode 100644 index 0000000..751486b --- /dev/null +++ b/docs/wiki/Black-Screen-After-Long-Shutdown.md @@ -0,0 +1,97 @@ +# Black screen after several days powered off + +Status: community-reported signature; not yet reproduced or root-caused by DeckDoc. + +## Reported behavior + +A user described a Deck that starts to a black screen with the display illuminated and audible system +sound only after it has been unused for several days. Holding the power button to force it off, then +powering it on again, restores the display. An immediate shutdown/power-on cycle does not reproduce it. +The reporter sees it mostly on an LCD Deck running Windows and occasionally on an OLED Deck running +SteamOS. + +This timing makes it a different branch from DeckDoc's validated in-game blackout: + +| Long-off startup report | Validated live-render blackout | +|---|---| +| Begins during first power-on after days off | Began during an already-running Game Mode workload | +| First boot after a long powered-off interval matters | App/plane transition and live session mattered | +| Reported across Windows/LCD and SteamOS/OLED systems | Validated on one Jupiter LCD SteamOS system | +| Force-off/second boot recovers | Gamescope forced composition recovered the live session | +| No live DRM/plane evidence captured yet | eDP, EDID, backlight, CRTC, planes, recording captured | + +Do not apply the Gamescope `composite_force` workaround by default. Windows does not use Gamescope, +OLED does not have the same LCD backlight path, and no multi-plane/live-render signature has been +captured for this startup case. + +## What the report suggests—and does not prove + +The delayed first-start condition makes cold initialization, firmware/embedded-controller state, +panel/link initialization, residual power state, battery/charger state, boot mode, or OS startup path +reasonable branches to test. A successful second boot proves recovery, not which branch caused it. + +Reports from two different Deck models and operating systems weaken a narrow SteamOS-Gamescope-only +explanation, but they do not establish one shared root cause. The LCD/Windows and OLED/SteamOS events +could still be different failures with the same visible symptom. + +## Capture protocol + +The next occurrence needs evidence before the forced shutdown when practical: + +1. Record days/hours since last use, whether the prior action was shutdown/sleep/hibernate, and whether + the Deck was charging while stored. +2. Record power LED, visible LCD illumination or OLED glow, startup chime/audio, fan, haptics, controls, + and whether the screen ever displayed the logo or firmware menu. +3. Test a known-good external display once, without repeated dock/undock cycling. +4. Check whether the device responds to ping/SSH or remote access. This distinguishes a booted OS with + failed local display from a pre-session boot failure. +5. Photograph/video the panel and LEDs, including the time from power press to sound. +6. After recovery, immediately save current and previous boot evidence. + +On SteamOS after the second boot: + +```bash +cd /path/to/deckdoc +sudo ./deckdoc.sh +sudo journalctl -k -b -1 --no-pager > previous-boot-kernel.log +sudo journalctl -b -1 --no-pager > previous-boot-journal.log +``` + +The failed first start may not have written a persistent boot journal. Record that absence; do not treat +the second boot's healthy DRM state as the failed state. + +On Windows, preserve Event Viewer system/display/kernel-power entries and generate a system report only +after reviewing it for private data. Also record whether Windows Fast Startup/hibernation was enabled, +because the user-visible “Shut down” path may not represent the same boot type as a full shutdown. + +## Controlled reproduction matrix + +Change one row at a time and avoid repeatedly forcing power unless necessary: + +| Variable | Values to compare | +|---|---| +| Off interval | immediate, overnight, 2–3 days, longer | +| Prior state | verified full shutdown, sleep, hibernate/hybrid shutdown | +| Storage power | unplugged, official charger connected | +| Battery state | approximate percentage at shutdown and first start | +| Display | handheld only, known-good external display attached before boot | +| Boot layer | firmware menu visible, boot logo visible, OS audio only | +| Software | SteamOS stable clean control; Windows full-shutdown control | + +Do not drain the battery deliberately, change firmware, write panel power nodes, or repeatedly hard-cycle +the device to chase the bug. + +## When to escalate + +Escalate with Valve Support if the issue repeats across clean full shutdowns, the firmware/boot logo is +also invisible, external display behavior points to a panel path, or there are charging/battery/physical +symptoms. File a SteamOS bug when a SteamOS Deck has a repeatable matrix and previous-boot/System Report +evidence. Windows-specific reports should also include the APU/display driver and firmware versions. + +## Related—not equivalent—reports + +- [SteamOS #1324: backlight-on black screen around Desktop Mode](https://github.com/ValveSoftware/SteamOS/issues/1324) +- [SteamOS #2632: intermittent black screen after Desktop Mode wake](https://github.com/ValveSoftware/SteamOS/issues/2632) +- [Valve: Steam Deck basic use and troubleshooting](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28) + +These references show nearby symptom classes, not a confirmed match for the long-off first-start report. diff --git a/docs/wiki/Collecting-and-Sharing-Evidence.md b/docs/wiki/Collecting-and-Sharing-Evidence.md new file mode 100644 index 0000000..36ee884 --- /dev/null +++ b/docs/wiki/Collecting-and-Sharing-Evidence.md @@ -0,0 +1,105 @@ +# Collecting and sharing evidence + +The best report preserves the failure state, the event timeline, and enough system context to reproduce +the problem without exposing personal data. + +## Capture order + +When the Deck is stable enough to use a terminal or SSH: + +1. Record the local time and what is physically visible/audible. +2. Note Game Mode/Desktop Mode, dock state, power state, and the last transition (wake, launch, update, + overlay, dock/undock). +3. Run `sudo ./deckdoc.sh` before restarting. +4. Save Steam's built-in System Report when available: + `Settings -> System -> Advanced -> System Report -> Create Report`. +5. Photograph or record a physical-only symptom when software capture differs from the panel. +6. Copy the timestamped master report somewhere safe before experiments. + +For a physically black LCD where the system remains alive: + +```bash +sudo ./deckdoc.sh --display-black +``` + +## Useful manual context + +```bash +# Versions and kernel +cat /etc/os-release +uname -a + +# Available boot journals +journalctl --list-boots + +# Current and previous boot kernel logs +sudo journalctl -k -b 0 --no-pager +sudo journalctl -k -b -1 --no-pager + +# Crash index; do not upload core files blindly +coredumpctl list --no-pager +``` + +Valve's SteamOS wiki identifies `/tmp/dumps/` and `steam_stdout.txt` as useful Steam-client evidence. +DeckDoc counts only actual minidump/core filename classes; normal bookkeeping files in that directory +are not crashes. + +If the optional [continuous probe](Continuous-Incident-Probe.md) captured the incident, preserve its +whole private directory before rebooting or purging. A normal `sudo ./deckdoc.sh` includes the latest +incident, but an older incident may provide the comparison that matters. + +## Privacy review + +Assume a report can contain: + +- Wi-Fi SSID/BSSID and network interface addresses; +- hostname and local usernames; +- home-directory paths; +- game names, Steam AppIDs, launch arguments, and process command lines; +- mounted device labels and paths; +- plugin names and third-party software; +- timestamps and usage patterns; +- hardware identifiers or serial-like values in manually added logs. + +Search before posting: + +```bash +rg -n -i 'ssid|bssid|serial|hostname|/home/|command line|ipv4|ipv6|mac' \ + logs/deckdoc_master_report_*.log +``` + +Make a redacted copy. Keep the original private. Replace sensitive values consistently so correlations +remain readable, for example `HOME_USER`, `WIFI_NAME`, or `BSSID_1`. + +Never publish passwords, API tokens, session cookies, Steam Guard codes, SSH private keys, browser +profiles, complete environment dumps, or raw core dumps without understanding their contents. A core +dump can contain process memory and secrets. + +## A useful bug report + +Include: + +- one-sentence symptom and expected behavior; +- device model (LCD/OLED and storage involved); +- SteamOS/client version and update channel; +- exact reproduction steps and frequency; +- Game/Desktop Mode, dock/power/sleep context; +- incident timestamp and whether logs are current- or previous-boot; +- minimal relevant DeckDoc sections; +- what changed recently; +- one-at-a-time tests and their outcomes; +- whether third-party plugins/modifications were disabled for a control run. + +Do not upload a huge report without pointing maintainers to the relevant timestamps and signatures. + +## Preserve evidence through reboot + +A forced restart can erase volatile state, but persistent journals and Steam's System Report may still +contain current/previous boot data. After reboot, record that the report is post-reboot and do not claim +that current sysfs state represents the failed state. + +## References + +- [Valve SteamOS: Reviewing log information](https://github.com/ValveSoftware/SteamOS/wiki/Reviewing-log-information) +- [systemd journalctl manual](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html) +- [systemd coredumpctl manual](https://www.freedesktop.org/software/systemd/man/latest/coredumpctl.html) diff --git a/docs/wiki/Continuous-Incident-Probe.md b/docs/wiki/Continuous-Incident-Probe.md new file mode 100644 index 0000000..fc5c487 --- /dev/null +++ b/docs/wiki/Continuous-Incident-Probe.md @@ -0,0 +1,70 @@ +# Continuous incident probe + +The optional DeckDoc probe preserves evidence that is easy to lose between a failure and a later full +report. It does not repeatedly run every diagnostic module. One low-priority process follows the local +system journal and remains blocked while no new record arrives. When a bounded GPU, display, SOF audio, +wireless, storage, OOM, thermal, Gamescope, or resume signature appears, it captures one private incident. + +## What an incident contains + +- the trigger, UTC time, boot ID, category, and requested pre/post window; +- up to two minutes before and five seconds after the event from the journal; +- RAM/swap and pressure-stall state; +- fan, thermal, power-supply, DRM connector/backlight/plane, network-device, USB, storage, Gamescope, + MangoApp, and recent core metadata that was readable at capture time. + +This is correlation evidence, not a verdict. A later core may be downstream of an earlier GPU reset; +a deliberate USB unplug can look like a disconnect; a matching line can still be unrelated to the +user-visible incident. + +## Opt in + +```bash +sudo ./probe/install-probe.sh install +sudo ./probe/install-probe.sh status +``` + +The normal `setup.sh` never installs or starts it. The system service is low-priority, capped at 128 MB, +can write only its private state directory, uses a 60-second per-category cooldown, retains at most 25 +incidents, and caps each journal window at 2 MiB by default. + +Create a manual marker immediately after an odd symptom: + +```bash +sudo /var/lib/deckdoc-probe/bin/deckdoc-probe.sh capture "black panel after wake" +``` + +The next `sudo ./deckdoc.sh` automatically includes the latest incident through +`module_probe.log`. Older incidents remain under `/var/lib/deckdoc-probe/events/`. + +## Stop, uninstall, and retention + +```bash +sudo systemctl stop deckdoc-probe.service +sudo ./probe/install-probe.sh uninstall +``` + +Uninstall preserves existing incidents. `sudo ./probe/install-probe.sh purge` is a separate, +permanent deletion and refuses to run while the service is active. + +## Privacy + +State is mode `0700`; individual files are `0600`. It is still unredacted. Journals can contain +usernames, paths, hostnames, network identifiers, command lines, chat text, tokens, or application +content. Review and redact every incident before sharing. A storage cap limits size, not sensitivity. + +## Performance boundary + +The probe uses Bash pattern matching in-process instead of spawning `grep` for each record. Expensive +commands run only after a trigger or manual capture. Validate real idle CPU, wakeups, memory, and storage +growth on both LCD and OLED releases before calling the overhead universally negligible. + +The watcher depends on the journal. A kernel hard lock, abrupt power loss, volatile journal, or storage +failure may prevent the final records from persisting; software cannot guarantee a last-gasp capture. + +## Why this design + +[`journalctl --follow`](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html) already +provides the append stream and boot/time filtering. Re-running SMART, filesystem, coredump, Steam, and +all hardware modules every few seconds would add unnecessary work and still might miss the transient +DRM/fan/device state that an event-triggered snapshot preserves. diff --git a/docs/wiki/Controls-Bluetooth-and-Input.md b/docs/wiki/Controls-Bluetooth-and-Input.md new file mode 100644 index 0000000..30ea4d5 --- /dev/null +++ b/docs/wiki/Controls-Bluetooth-and-Input.md @@ -0,0 +1,43 @@ +# Controls, Bluetooth, touch, and input + +DeckDoc does not yet automate raw input testing because an always-on recorder could capture typing, +gestures, or private content. Diagnosis should use device inventory, bounded activity counts, Steam's +test UI, and controlled comparisons—not raw event dumps shared publicly. + +## Built-in controls + +Use `Steam -> Settings -> Controller -> Test Controller Inputs`. Record the exact control, whether the +test UI sees it, whether firmware/boot menus see it, whether all titles or one layout fail, and whether +the event followed wake/update. Valve documents the controller test in its +[basic troubleshooting guide](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28). + +- Test UI passes, one title/layout fails: Steam Input or game configuration first. +- Test UI and several titles fail, external controller works: built-in device/firmware/hardware branch. +- Whole controller device disappears only after wake: USB/HID/resume path. +- One physical control fails in firmware/recovery too: stronger hardware suspicion. + +Do not flash controller firmware outside the official flow, write calibration values from another +unit, or publish raw keyboard/event streams. + +## Touchscreen + +Record missing versus phantom touches, charger/dock state, external display mapping, screen condition, +and behavior in firmware/recovery. Desktop-only rotation/mapping points to configuration; absence or +phantom input across recovery on a clean/dry panel raises digitizer/hardware suspicion. Do not press on +the panel or delete calibration/mapping files before capturing them. + +## Bluetooth + +Separate discovery, pairing, connecting, reconnect-after-wake, input, audio profile, and latency. +Capture adapter/rfkill/firmware and `bluetoothd` timestamps before forgetting devices. + +- one peripheral only: its battery/firmware/profile first; +- adapter missing or firmware error: Deck driver/device branch; +- docked-only or 2.4 GHz-dependent: RF placement/coexistence; +- input works but audio profile fails: PipeWire/Bluetooth profile branch. + +After evidence capture, forget only the affected device, update its official firmware, and compare +docked/undocked or line-of-sight placement. Do not erase every bond or blindly reset the USB controller. + +Bluetooth and privacy-preserving input inventory are P1 detector gaps. See +[Coverage and gaps](Coverage-and-Gaps.md) and [Dock/USB-C](Dock-USB-C-and-External-Displays.md). diff --git a/docs/wiki/Coverage-and-Gaps.md b/docs/wiki/Coverage-and-Gaps.md new file mode 100644 index 0000000..39a244f --- /dev/null +++ b/docs/wiki/Coverage-and-Gaps.md @@ -0,0 +1,92 @@ +# Coverage and diagnostic gaps + +DeckDoc is broad, but it is not yet a complete hardware test suite. This page prevents an unsupported +symptom from being mistaken for a clean bill of health. + +## Current automated coverage + +DeckDoc directly inspects: + +- AMD GPU resets/timeouts and selected GPU VM page faults; +- internal eDP/backlight/CRTC/plane state and selected Gamescope failures; +- battery telemetry, hwmon thermals/fan, NVMe SMART, BTRFS/ext4 state; +- SOF audio errors and audio-device presence; +- current/historical crashes, Steam dump files, memory/swap/OOM; +- selected Wi-Fi drivers, interface presence/link state; +- microSD/mmc/ext4/TRIM/read-only signals; +- suspend/resume transitions and selected fan/PCI errors; +- dock USB topology, exported Type-C/PD state, external connectors, USB Ethernet, and selected path errors; +- the latest opt-in continuous-probe incident and its volatile state. + +## Partially covered + +| Area | What DeckDoc sees | What remains uncertain | +|---|---|---| +| LCD/OLED display | Connector, EDID, backlight, CRTC, DRM state | Pixels, cable/TCON/panel electrical health; OLED has no conventional backlight | +| Wi-Fi | `wlan*`, link info, selected firmware patterns | Every LCD/OLED adapter/driver, router/AP faults, DNS, captive portal | +| Audio | SOF/ALSA/PipeWire presence | Speaker/headphone wiring, codec quality, Bluetooth audio | +| Battery/dock | exported voltage/current/energy/Type-C/PD values | Cell/rail electrical diagnosis, fields a driver does not export | +| Storage | fixed primary NVMe path plus mounted filesystem evidence | Every replacement SSD path, destructive surface testing, offline repair | +| Thermals | exported hwmon values and thresholds | Physical airflow/thermal-interface inspection, sensor calibration | +| Games/Proton | dumps, selected Steam errors, prefixes | Per-game compatibility, anti-cheat, launch options, mod interactions | +| Suspend/resume | PM transitions and selected correlated errors | Every firmware/EC/device wake failure | + +## Not automated yet + +### Input and controls + +DeckDoc does not test buttons, sticks, triggers, trackpads, gyro, haptics, touch input, or controller +firmware. Use [Controls, Bluetooth, touch, and input](Controls-Bluetooth-and-Input.md) and Steam's +`Settings -> Controller -> Test Controller Inputs`; record whether the problem +exists in firmware menus and across games, and note whether it began after wake/update. + +### Bluetooth + +There is no Bluetooth module yet. Use [Controls, Bluetooth, touch, and input](Controls-Bluetooth-and-Input.md), +record adapter presence, device type, pairing state, and whether the +fault affects discovery, pairing, reconnect, input, or audio. Avoid deleting every paired device before +capturing logs. + +### Dock, USB-C, external monitors, and power delivery + +The new dock module inventories USB topology and driver-exported Type-C/PD/Alt Mode state but cannot +electrically certify dock firmware, rails, adapters, cables, monitors, or signal integrity. Use +[Dock, USB-C, power delivery, and external displays](Dock-USB-C-and-External-Displays.md) and retain +docked/direct reports. + +### Updates and image health + +DeckDoc does not validate SteamOS image slots, update payloads, recovery media, or immutable-root +integrity. Use [Recovery and escalation](Recovery-and-Escalation.md) and Valve's official recovery +options. + +### Camera, microphone, external peripherals + +Microphone presence may appear indirectly in PipeWire, but DeckDoc has no capture-quality test. There +are no dedicated modules for webcams, printers, USB storage, keyboards, mice, or headsets. + +## Model-aware limitations + +- LCD and OLED Decks use different APUs, panels, wireless devices, audio paths, firmware, power-button + timing, and sensor exports. +- `rem_audio_sof.sh` specifically reloads `snd_sof_amd_vangogh`; it is not a generic audio reset. +- `/dev/nvme0n1` and DRM `card0` are common, not guaranteed after hardware/driver changes. +- A fixed 6.6 V battery threshold and certain frequency paths should be treated as implementation + assumptions until model-aware discovery is added. +- An OLED panel does not expose a conventional LCD backlight, so the LCD-blackout signature and its + remediation precheck may not apply. + +## Proposed expansion backlog + +High-value future modules include: + +- model/APU/panel/storage discovery and capability manifest; +- Bluetooth adapter and reconnect health; +- controller/input event and firmware inventory; +- direct-versus-dock differential summaries and broader PD/Alt Mode validation; +- DNS/gateway/connectivity separation from Wi-Fi firmware state; +- boot slot/update health and report packaging; +- title-scoped Proton/log collection without exposing secrets; +- longer structured trends for thermal, fan, power, memory, and clocks beyond incident snapshots. + +Open a repository issue with a reproducible symptom and primary evidence before adding a broad fix. diff --git a/docs/wiki/Crashes-GPU-and-Memory.md b/docs/wiki/Crashes-GPU-and-Memory.md new file mode 100644 index 0000000..edaf22c --- /dev/null +++ b/docs/wiki/Crashes-GPU-and-Memory.md @@ -0,0 +1,102 @@ +# Crashes, GPU hangs, page faults, and memory pressure + +A game closing, a frozen frame, a black screen, and a complete session restart may look similar but +leave different evidence. Use the incident time to correlate the game/Steam process, Gamescope, kernel +GPU, memory, and core-dump layers. + +## Capture + +```bash +sudo ./deckdoc.sh +``` + +Read these together: + +- `module_coredump.log` +- `module_steam.log` +- `module_gamescope.log` +- `module_gpu.log` +- `module_dxvk.log` +- `module_memory.log` +- `module_thermal.log` + +The core-dump module separates retained history, the current boot, and a rolling last-24-hour window. +It breaks that recent window into steamwebhelper, Gamescope, and Wine/Proton families. A helper +`SIGTRAP` needs UI/incident correlation; a Gamescope `SIGABRT` or `SIGSEGV` is a different, higher-value +session failure signal. + +## Crash branches + +### Process crash without GPU reset + +A current-boot `SIGSEGV`/`SIGABRT` for the game, Wine/Proton, Steam helper, MangoApp, or Gamescope +identifies the process that terminated. It does not alone explain why. Check surrounding logs and +whether only one title reproduces. + +### Gamescope/session crash + +A current Gamescope dump, Vulkan descriptor failure, or repeated session restart can return the user +to Library/login or blank the whole session. A `gamescope-wl` dump recorded only at normal session exit +may be cosmetic; time and restart count matter. + +The MangoApp `FDINFO_PERMISSION_ABORT` is a separate helper signature. Do not call it a GPU or panel +failure without additional evidence. + +### AMD GPU timeout/reset + +`amdgpu_job_timedout`, ring timeout, `VRAM is lost`, and reset messages can explain a frozen game or +session. Classify the outcome: + +- reset succeeded: the driver recovered, but clients may still exit; +- reset failed/skipped: higher risk of a hard lock and orderly reboot may be required; +- no reset: inspect process crashes, VM faults, memory, and display-specific branches. + +DeckDoc does not trigger the kernel's debugfs GPU-reset control because it discards in-flight work and +can destroy the failure state. + +### GPU VM/page fault + +The DXVK/VKD3D module recognizes selected `GCVM_L2_PROTECTION`, `UTCL2`, CB/DB/CPF/CPD, mapping, and +walker strings. These labels classify where the kernel reported a fault; they do not uniquely prove a +specific translation layer or hardware defect. Correlate process attribution, game/API, Mesa/Proton +version, and whether other titles reproduce. + +### Memory pressure/OOM + +Current-boot OOM-killer/page-allocation messages are strong evidence. Below 1 GB `MemAvailable` is a +warning and below 512 MB is critical. Swap over 50% and cumulative swapped pages are historical context; +live `vmstat` swap I/O is stronger evidence of current pressure. None reconstructs a past incident by +itself. + +The Deck's memory is shared with graphics, so pressure can cascade into stutter, allocation failure, +or GPU/client instability. Close the workload and preserve the process/memory context; do not clear +arbitrary caches as a first response. + +## One-title versus system-wide + +| Scope | First hypotheses | +|---|---| +| One title/save/scene | game bug, mod, launch option, Proton/API path, corrupt title data | +| All D3D12 but not Vulkan/D3D11 | VKD3D/driver/API path | +| All titles after an update | SteamOS/Mesa/Gamescope/Proton regression | +| Desktop and games | compositor, kernel/GPU, memory, thermal, hardware | +| Only after wake | device/resume/firmware state | + +Change one variable at a time: remove per-game modifications, select a known-good Proton version, verify +game files, or test a clean boot. Keep the exact result. + +## Escalate when + +- GPU reset fails or the Deck hard-locks repeatedly; +- multiple unrelated titles reproduce on a clean, updated system; +- page faults/timeouts recur at ordinary temperatures and memory use; +- filesystem/SMART errors accompany crashes; +- a core dump and logs identify a reproducible upstream component regression. + +## References + +- [systemd coredumpctl](https://www.freedesktop.org/software/systemd/man/latest/coredumpctl.html) +- [Linux kernel AMDGPU debugfs](https://docs.kernel.org/gpu/amdgpu/debugfs.html) +- [Gamescope repository](https://github.com/ValveSoftware/gamescope) +- [DXVK issue tracker](https://github.com/doitsujin/dxvk/issues) +- [vkd3d-proton issue tracker](https://github.com/HansKristian-Work/vkd3d-proton/issues) diff --git a/docs/wiki/DeckDoc-Rescue.md b/docs/wiki/DeckDoc-Rescue.md new file mode 100644 index 0000000..ea9be92 --- /dev/null +++ b/docs/wiki/DeckDoc-Rescue.md @@ -0,0 +1,75 @@ +# DeckDoc Rescue: bootable outside-OS diagnostics + +DeckDoc Rescue is an alpha design for diagnosing a Deck independently of its installed OS. It boots +from removable media through the firmware boot manager, captures live hardware state, and attempts to +read installed SteamOS journals without mounting or repairing the internal disk. + +It is not a Valve recovery image. It does not repair, re-image, factory-reset, unlock encrypted data, +run `fsck`, mount internal filesystems, change EFI entries, flash firmware, or write the installed disk. + +## Why boot outside the installed OS + +- separate “installed SteamOS is broken” from “hardware also fails in another environment”; +- recover previous-boot logs after a boot loop or black startup; +- inspect NVMe SMART, enumeration, USB-C/dock, display, thermal, power, and input presence even when + Game Mode cannot start; +- compare BIOS/recovery/live behavior with the installed system before destructive recovery. + +Booting another OS changes the kernel and drivers. A device working in Rescue strongly argues against +total hardware absence, but does not prove the installed driver/config root cause. A device missing in +one generic rescue kernel can also mean that kernel lacks Steam Deck support. + +## Current portable collector + +From a compatible Linux rescue environment: + +```bash +sudo ./bootprobe/deckdoc-rescue-collect.sh \ + --installed-disk /dev/nvme0n1 \ + --output-dir /path/to/removable-media +``` + +The collector creates a private archive with live PCI/USB/block/Type-C/PD/DRM/network/thermal/power +state, rescue-boot journal, NVMe health, EFI boot entries, and installed boot indexes/current/previous +journals where systemd can dissect the image. `journalctl --image=` is the documented image reader; +the collector never mounts the disk. See [systemd journalctl](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html). + +If the installed image cannot be dissected, the report says inaccessible. It must not silently remount +or repair it. Encrypted or unusual layouts require a separate, explicit evidence-preserving workflow. + +## Alpha image builder + +`bootprobe/build-rescue-image.sh` builds an ArchISO development image on an Arch Linux build host with +the official `archiso` package. Arch's [`mkarchiso`](https://man.archlinux.org/man/mkarchiso.1.en) +creates UEFI-capable live images and can sign artifacts, but this project does not yet ship a signed +release. + +Release gates: + +1. pin and record every package/build input; +2. make the image reproducible and publish checksums/signatures; +3. boot-test Jupiter LCD and Galileo OLED, internal NVMe untouched; +4. validate Wi-Fi, docked Ethernet, USB storage, display, SMART, journal image reading, and shutdown; +5. threat-model remote access and redaction; +6. document safe image-writing with exact target verification. + +Use Valve's current [SteamOS recovery instructions](https://help.steampowered.com/en/faqs/view/1B71-EDF2-EB6D-2BB3) +for firmware boot and official recovery workflows. Do not confuse or replace those recovery actions +with DeckDoc evidence collection. + +## Remote control and export + +Docked Ethernet is the default remote transport. A release image should accept only an explicitly +supplied SSH public key or one-time authenticated setup; it must not ship a default password or an +unauthenticated web server. Reports remain local unless the user explicitly transfers them. + +Direct USB-C networking is optional future work. Linux USB gadget mode requires a real USB Device +Controller plus configfs/libcomposite support; connector shape alone is insufficient. The image must +verify `/sys/class/udc` and kernel capabilities before offering it. See the +[kernel USB gadget documentation](https://docs.kernel.org/usb/gadget_configfs.html). + +## Hardware boundary + +Persistence in BIOS, Valve recovery, and DeckDoc Rescue with known-good external power/cables raises +hardware suspicion. Only Valve/service diagnosis, qualified electrical testing, or replacement of the +isolated component resolving a controlled reproduction confirms hardware failure. diff --git a/docs/wiki/Display-and-Gamescope-Problems.md b/docs/wiki/Display-and-Gamescope-Problems.md new file mode 100644 index 0000000..20a77a3 --- /dev/null +++ b/docs/wiki/Display-and-Gamescope-Problems.md @@ -0,0 +1,92 @@ +# Display and Gamescope problems + +“Black screen” is a symptom shared by several different failures. First determine whether the Deck, +the game, Gamescope, the GPU, and the physical panel are still alive. + +## Symptom classes + +| Symptom | Leading branches | +|---|---| +| Backlight on, sound/input/rendering continue | physical scanout, plane transition, eDP/TCON/cable | +| Backlight off or brightness actually zero | panel/backlight/power/modeset state | +| Game frozen and sound loops/stops | game, Proton, GPU timeout/reset, OOM | +| Returned to Library/login repeatedly | game or Gamescope crash/session restart | +| External monitor works, internal panel fails | eDP/panel-specific path | +| Internal panel works, docked display fails | dock/cable/monitor/DP Alt Mode/modeset | +| Desktop Mode wake only | KDE/Wayland/resume/display state | +| First power-on after days off; immediate second boot works | cold-start/firmware/panel initialization branch | + +The last row has its own [long-off startup blackout protocol](Black-Screen-After-Long-Shutdown.md). It is +not evidence for the Gamescope forced-composition fix, especially when reported on Windows or OLED. + +## Capture while black + +From SSH or a usable terminal: + +```bash +sudo ./deckdoc.sh --display-black +``` + +Record whether: + +- the backlight is visibly on; +- audio and controls continue; +- a recording/stream shows advancing frames; +- an external display changes the symptom; +- the event followed wake, app transition, overlay toggle, dock/undock, or update. + +Do not change brightness, write `bl_power`, force a GPU reset, or suspend again before capture. + +## Interpret the display section + +### `LIVE_RENDER_TO_PHYSICAL_SCANOUT_GAP` + +DeckDoc found connected eDP, a readable EDID, nonzero LCD backlight, and an active CRTC while the user +declared the panel physically black. When a recording also shows valid frames and there is no matching +GPU/app crash, the failure boundary moves downstream toward plane commit, DCN/eDP scanout, panel timing, +TCON, cable, or panel. + +Multiple planes are not an error by themselves. They justify a reversible forced-composition test only +for this complete signature. Follow [Screen black while sound works](Steam-Deck-Black-Screen-Sound-Working.md). + +### `PANEL_OR_MODESET_STATE_INCOMPLETE` + +At least one panel prerequisite failed. Forced composition is not automatically indicated. Inspect the +missing eDP, EDID, backlight, or CRTC state and correlate kernel modeset/reset errors. + +### GPU or Gamescope evidence + +- `amdgpu_job_timedout`, reset, or VM fault: use [Crashes, GPU and memory](Crashes-GPU-and-Memory.md). +- current-boot Gamescope core dump or repeated restart: inspect the crash/session branch. +- MangoApp `FDINFO_PERMISSION_ABORT`: an overlay helper failed; do not automatically label it a + compositor crash or panel failure. + +## Docked/external display branch + +DeckDoc lists connectors but does not diagnose the whole dock chain. Preserve both docked and undocked +reports, then test one known-good variable at a time: monitor input, cable, adapter/dock, USB-C port, +resolution/refresh setting, and power supply. A successful undocked test narrows the path but does not +identify which dock component failed. + +Use Valve's [Docking the Steam Deck](https://help.steampowered.com/en/faqs/view/4C18-08B5-DEC9-3AF4) +guidance for official first steps. + +## Escalate when + +- the panel is black with missing EDID/eDP or zero backlight; +- forced composition does not help or one plane was already active; +- the issue recurs across clean boots and channels with third-party tools disabled; +- an external/internal split strongly suggests a hardware path; +- there are GPU reset failures, persistent display-engine errors, or physical damage. + +Attach the relevant display, GPU, Gamescope, coredump, ACPI, and exact incident-time excerpts. Linux's +amdgpu documentation recommends dmesg and pre/post-reproduction display debug state for DC problems. + +## References + +- [Linux kernel: AMD Display Core debug tools](https://docs.kernel.org/gpu/amdgpu/display/dc-debug.html) +- [Gamescope repository](https://github.com/ValveSoftware/gamescope) +- [Gamescope direct-scanout/force-composite investigation #1368](https://github.com/ValveSoftware/gamescope/issues/1368) +- [SteamOS backlight-on black screen #1324](https://github.com/ValveSoftware/SteamOS/issues/1324) +- [SteamOS intermittent LCD-black report #2632](https://github.com/ValveSoftware/SteamOS/issues/2632) +- [SteamOS GPU-reset comparison case #1015](https://github.com/ValveSoftware/SteamOS/issues/1015) diff --git a/docs/wiki/Dock-USB-C-and-External-Displays.md b/docs/wiki/Dock-USB-C-and-External-Displays.md new file mode 100644 index 0000000..6a22592 --- /dev/null +++ b/docs/wiki/Dock-USB-C-and-External-Displays.md @@ -0,0 +1,67 @@ +# Dock, USB-C, power delivery, and external displays + +A dock is several paths in one enclosure: USB hub/controller, USB Ethernet/audio/storage, USB-C Power +Delivery, DisplayPort Alt Mode, and an HDMI/DisplayPort converter. One can fail while the others work. +DeckDoc can judge exported behavior and correlation; it cannot certify the dock's power rails or signal +integrity electrically. + +## Capture two controlled states + +Run one report with the symptom present while docked: + +```bash +sudo ./deckdoc.sh +``` + +Then change only the path under test and run a second report—for example official/known-good charger +direct to Deck, direct USB-C display, another cable, or another Ethernet adapter. Record dock model, +power supply, cable, display/input/mode, peripherals, exact failure time, and what remained functional. + +Read `module_dock.log`, `module_display.log`, `module_battery.log`, `module_acpi.log`, and the probe +incident if present. + +## What `module_dock.log` sees + +- USB topology and vendor/product IDs, with serial numbers intentionally omitted; +- Type-C data/power role, orientation, partner presence, PD and Type-C revision when exported; +- DisplayPort Alt Mode configuration/pin/HPD when the driver exports it; +- USB/PD supply voltage, current, power, online state, and a clearly labeled instantaneous calculation; +- external DRM connector, EDID byte count, link state, and modes; +- likely USB Ethernet interface, driver, carrier, speed, and state; +- current-boot xHCI, UCSI, Type-C, USB over-current, reset/disconnect, and display-link errors. + +The kernel Type-C class defines role, partner, `power_operation_mode`, PD revision, and Alternate Mode +exports, but drivers are not required to expose every field. “Not exported” is not a zero-voltage or +bad-dock diagnosis. See the [Linux Type-C class](https://docs.kernel.org/driver-api/usb/typec.html) and +[Type-C sysfs ABI](https://www.kernel.org/doc/html/next/admin-guide/abi-testing-files.html). + +## Decision boundaries + +| Observation | Stronger next branch | +|---|---| +| Direct charger works; docked charging repeatedly renegotiates or drops | dock PSU, cable, PD passthrough, dock controller | +| Direct USB-C display works; same display/cable path through dock fails | dock/adapter conversion path | +| Dock works on another host | Deck port, SteamOS driver, mode, or Deck/dock combination | +| Multiple hosts fail on the same dock/cable/display | shared dock/cable/display path | +| Whole USB hub, Ethernet, and display reset together | controller, upstream cable, or power path before one peripheral | +| One peripheral resets while the rest remain stable | that device, its downstream port/cable, or its driver | +| External display fails but USB/Ethernet/charging remain stable | Alt Mode, converter, EDID/mode/link path | +| Failures reproduce only after suspend | resume/reinitialization path; correlate PM timestamps | + +A `DOCK_SIGNATURE: TOPOLOGY_CHANGE_WITH_DOCK_PATH_ERROR` means a reset/disconnect and a selected +host/PD/display-link error both exist in the boot. It does not prove the dock caused either record; +timestamps and the direct-vs-dock comparison decide whether the signature is relevant. + +## Voltage and power limits + +When voltage/current are exported, DeckDoc reports raw micro-unit values and calculated instantaneous +watts. Battery current may differ from negotiated adapter power because the running Deck consumes power, +conversion has losses, and the battery may charge or discharge. Many docks expose no rail telemetry. +Software output is not an oscilloscope, USB-PD analyzer, or load tester. + +Stop using a dock/cable for sparking, electrical smell, abnormal connector heat, visible damage, +over-current logs, or repeated power loss. Do not stress-test it further. + +Valve's [docking guide](https://help.steampowered.com/en/faqs/view/4C18-08B5-DEC9-3AF4) is the primary +source for supported reset and cable/display cross-tests. Do not flash unofficial dock firmware, force +unsupported timings, unbind the USB host controller remotely, or backfeed through a powered hub. diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md new file mode 100644 index 0000000..6b7fc23 --- /dev/null +++ b/docs/wiki/Getting-Started.md @@ -0,0 +1,96 @@ +# Getting started + +## Before installation + +DeckDoc targets SteamOS 3.x. Use Desktop Mode or SSH and make sure the Deck has enough free space for +text logs. Diagnostics do not need the SteamOS read-only root filesystem to be disabled. + +The normal DeckDoc report is not a daemon: it runs once, writes a report inside its checkout, and +exits. Continuous capture is a separate, explicit opt-in described in the +[continuous incident probe](Continuous-Incident-Probe.md). + +## Install + +```bash +git clone https://github.com/deucebucket/deckdoc.git +cd deckdoc +./setup.sh +``` + +To update an existing clean checkout: + +```bash +git pull --ff-only +``` + +If `git status --short` shows local changes, preserve or commit them before pulling. Do not discard +local evidence or customization just to update the tool. + +## Create a baseline + +Run one report while the Deck is healthy. It gives you model-specific paths and normal values to +compare with a future incident. + +```bash +sudo ./deckdoc.sh +``` + +The report is stored as: + +```text +logs/deckdoc_master_report_.log +``` + +Individual subsystem output is stored in `logs/module_*.log`. A new run replaces those per-module +files but does not delete older timestamped master reports. + +## Capture a failure + +If the system still responds, run DeckDoc before restarting. A restart may clear the exact current +state even when journal history survives. + +```bash +cd /path/to/deckdoc +sudo ./deckdoc.sh +``` + +For a physically black built-in panel where audio/input/rendering continue, declare the symptom so +the display module can classify it: + +```bash +sudo ./deckdoc.sh --display-black +``` + +Do not suspend, change display power/brightness, force a GPU reset, or launch a broad “fix” script +before collecting the failure state. + +## Why root is recommended + +Without root, DeckDoc can still read many `/proc`, `/sys`, user-session, and Steam paths. It may not +be able to read: + +- the full kernel/system journal; +- DRM debugfs state and active hardware planes; +- NVMe SMART data; +- BTRFS device statistics or ext4 superblock state; +- system-wide core dumps; +- some hardware telemetry. + +An empty section in an unprivileged report can therefore mean “permission denied,” not “healthy.” + +If repeatedly entering `sudo` is impractical, the owner can make a one-time approval for DeckDoc's +exact read-only collection operations. This does not disclose the password or authorize a root shell, +arbitrary command, or remediation. See [privileged diagnostic authorization](Privileged-Diagnostic-Authorization.md). + +## Optional tools + +DeckDoc degrades gracefully when commands are missing. Useful commands include `smartctl`, `btrfs`, +`dumpe2fs`, `coredumpctl`, `aplay`, `pw-cli`, `ip`, `iw`, `lspci`, `lsblk`, and `findmnt`. + +SteamOS is an image-based operating system. Avoid disabling its read-only root or installing packages +solely for a first diagnostic run. Record missing commands and use the evidence that is available. + +## Next step + +Use the [triage flow](Triage-Flow.md), then read the relevant symptom page. Before sharing logs, review +[collecting and sharing evidence](Collecting-and-Sharing-Evidence.md). diff --git a/docs/wiki/Hardware-Failure-Decision-Guide.md b/docs/wiki/Hardware-Failure-Decision-Guide.md new file mode 100644 index 0000000..a2359d1 --- /dev/null +++ b/docs/wiki/Hardware-Failure-Decision-Guide.md @@ -0,0 +1,51 @@ +# Hardware failure decision guide + +DeckDoc uses four deliberately conservative outcomes. + +| Outcome | Evidence standard | +|---|---| +| Fixable application/configuration | A title, layout, route, plugin, AP, mode, prefix, cable, or setting follows the symptom and a reversible A/B changes it | +| Driver/firmware/OS | A timestamped subsystem transition fails, a previous build changes the result, or a reproducible upstream signature matches | +| Strong hardware suspicion | The failure persists in firmware or official recovery, across OS slots/stock config and known-good external components, or has new device-local physical/media/electrical evidence | +| Confirmed hardware failure | Valve/service diagnosis, qualified component-level test, or isolated replacement resolves the controlled reproduction | + +Never label an `amdgpu` timeout, one core dump, nonzero historical BTRFS counter, swap use, a hot +chassis, low idle clock, zero idle fan RPM, or “black screen” alone as hardware failure. + +## Three-layer rule + +Require: + +1. the physical/user-visible symptom at an exact time; +2. subsystem evidence from that boot/window; +3. a controlled contrast or independent signal. + +For example, a game freeze plus an AMDGPU timeout plus recurrence across unrelated stock titles is +stronger than any one signal. A physically black LCD plus live audio, live Gamescope, connected eDP, +readable EDID, active CRTC, nonzero backlight, and no reset localizes the display path—but still cannot +inspect panel electronics. + +## High-value contrasts + +- firmware/recovery/Rescue versus installed OS; +- current versus previous SteamOS image; +- stable versus beta/client update; +- clean stock versus plugins/mods/overlays; +- docked versus direct known-good cable/power/display/network; +- internal versus external display; +- cold boot versus resume; +- one title/API/Proton versus several unrelated titles; +- one AP versus a known-good hotspot; +- one SD card/peripheral versus another known-good device. + +Change one variable and record the result. A reboot proves recovery, not root cause. + +## Stop conditions + +Stop software testing for smoke, swelling, liquid, electrical smell, sparking, port/cable damage, +abnormal heat while idle/off, fan stopped while temperature rises, new storage I/O/media errors, +repeated failed GPU resets, or loss of safe backup access. Disconnect power if safe and escalate. + +The full source audit and detector priorities live in the repository's +`docs/research/steamdeck-issue-deep-dive.md`; the [research index](Research-and-Issue-Index.md) maps its +primary sources into this wiki. diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index f102d38..3860099 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -1,19 +1,93 @@ -# DeckDoc engineering wiki +# DeckDoc diagnostic center -This wiki records Steam Deck failure signatures, evidence standards, and safe recovery procedures. -It is intentionally stricter than a collection of tweaks: a remediation must have an observable -trigger, read-only prechecks, a backup, a narrow action, verification, and a rollback. +DeckDoc is a full-system Steam Deck diagnostic and incident-response platform. Its job is to turn “my +Deck is broken” into a narrower, evidence-based question across boot, applications, Steam/Proton, +Gamescope, kernel/GPU, memory, storage, power, thermal, audio, network, suspend/resume, controls, +display, docks/peripherals, or physical hardware. The wiki is the symptom-first entry point to the +collector, continuous probe, outside-OS Rescue environment, and guided DeckMD checker. -## Current investigations +If the Deck is smoking, swollen, wet, unusually hot to touch, smells electrical, or has a damaged +charging port, stop using it, disconnect power if safe, and contact Steam Support. Do not run software +diagnostics on a physically unsafe device. -- [Steam Deck black screen with sound working: diagnosis and fix](Steam-Deck-Black-Screen-Sound-Working.md) -- [Physical LCD blackout with live rendering](LCD-Blackout-Investigation.md) +## Start here + +1. [Getting started](Getting-Started.md) — install DeckDoc and create the first report. +2. [Triage flow](Triage-Flow.md) — choose the branch that matches what is happening now. +3. [Reading DeckDoc reports](Reading-DeckDoc-Reports.md) — separate strong evidence from noise. +4. [Collecting and sharing evidence](Collecting-and-Sharing-Evidence.md) — preserve the incident and + protect private data. +5. [Recovery and escalation](Recovery-and-Escalation.md) — decide when to restart, roll back, repair, + re-image, file a bug, or contact Valve. +6. [Continuous incident probe](Continuous-Incident-Probe.md) — opt in to low-overhead event capture. +7. [DeckDoc Rescue](DeckDoc-Rescue.md) — outside-OS evidence for a Deck that will not boot normally. +8. [Privileged diagnostic authorization](Privileged-Diagnostic-Authorization.md) — approve an exact + read-only command set once without sharing a password. + +## Find your symptom + +### Boot, crashes, and performance + +- [Recovery, boot failure, and escalation](Recovery-and-Escalation.md) +- [Crashes, GPU hangs, page faults, and memory pressure](Crashes-GPU-and-Memory.md) +- [Reading retained core dumps and current-boot crashes](Reading-DeckDoc-Reports.md#crashes-and-retained-history) + +### Audio, network, and sleep + +- [Audio problems and SOF DSP failures](Audio-Problems.md) +- [Network and resume problems](Network-and-Resume-Problems.md) + +### Power and storage + +- [Power, thermal, fan, charging, and battery problems](Power-Thermal-and-Battery-Problems.md) +- [NVMe, filesystem, and microSD problems](Storage-and-MicroSD-Problems.md) + +### Docks, controls, boot, and hardware decisions + +- [Dock, USB-C, power delivery, and external displays](Dock-USB-C-and-External-Displays.md) +- [Controls, Bluetooth, touch, and input](Controls-Bluetooth-and-Input.md) +- [Hardware failure decision guide](Hardware-Failure-Decision-Guide.md) +- [DeckDoc Rescue](DeckDoc-Rescue.md) +- [Coverage and diagnostic gaps](Coverage-and-Gaps.md) +- [Recovery and escalation](Recovery-and-Escalation.md) + +### Display and Game Mode + +- [Display and Gamescope problems](Display-and-Gamescope-Problems.md) +- [Black screen on first start after several days powered off](Black-Screen-After-Long-Shutdown.md) +- [Screen black while sound or input still works](Steam-Deck-Black-Screen-Sound-Working.md) - [Display diagnostic runbook](Display-Diagnostic-Runbook.md) -- [Safe remediation policy](Safe-Remediation-Policy.md) +- [Physical LCD blackout investigation](LCD-Blackout-Investigation.md) + +## Reference desk + +- [Module reference](Module-Reference.md) — every module, log file, dependency, and limitation. +- [Research and issue index](Research-and-Issue-Index.md) — repository issues, upstream evidence, and + implementation status. +- [Safe remediation policy](Safe-Remediation-Policy.md) — mandatory safety boundary for every fix. +- [Continuous incident probe](Continuous-Incident-Probe.md) — capture architecture, limits, privacy, + installation, status, and removal. +- [Privileged diagnostic authorization](Privileged-Diagnostic-Authorization.md) — exact allowlist, + security boundary, use, update, and removal. + +## Evidence standard + +A single log line rarely proves a root cause. A strong DeckDoc diagnosis has: + +- the symptom captured while it is present or immediately afterward; +- timestamps that align with the failure; +- two or more independent signals that agree; +- current-boot evidence separated from retained history; +- explicit alternative explanations and coverage limits; +- a narrow, reversible test and post-test verification; +- human confirmation for physical outcomes such as visible pixels, sound, fan motion, or charging. + +The wiki distinguishes `observed`, `correlated`, `likely`, `ruled out for this incident`, and +`confirmed by remediation`. Those phrases are intentionally different. -## Important distinction +## Project boundary -A captured or streamed frame and the physical LCD are different observation points. If a recording -continues to show valid frames while the built-in panel is black, the application and most of the -compositor path are still working. DeckDoc then inspects the eDP link, EDID, backlight, CRTC, and -hardware-plane commit state instead of blaming the foreground application. +DeckDoc collects evidence and implements only a small number of guarded fixes. It is not a replacement +for Steam's built-in System Report, Steam Support, warranty service, electrical safety judgment, or +offline filesystem repair. When a symptom is not automated, this wiki says so and provides a safe +collection/escalation path instead of inventing a fix. diff --git a/docs/wiki/Module-Reference.md b/docs/wiki/Module-Reference.md new file mode 100644 index 0000000..8104da5 --- /dev/null +++ b/docs/wiki/Module-Reference.md @@ -0,0 +1,168 @@ +# Module reference + +All diagnostic modules run in parallel. Output order in the master report is filename/glob order, not +incident order; use the timestamps inside journal excerpts. + +## Hardware and kernel + +### `gpu_apu.sh` -> `module_gpu.log` + +Reads current-boot amdgpu errors, reset outcomes, historical dmesg matches, CPU frequency, and the +active GPU SCLK state. A low instantaneous frequency can be normal at idle; correlate it with sustained +poor performance and load. A failed/skipped GPU reset is more serious than a successful recovery. + +Limitations: fixed CPU/GPU sysfs paths, no load-normalized trend sample, and current journal access may +require root. + +### `battery_pmic.sh` -> `module_battery.log` + +Reads the first available `BAT1`/`BAT0` status, capacity, voltage, current, charge, and energy nodes. +The raw 6.6 V warning is an implementation threshold, not a model-independent battery diagnosis. + +Limitations: no USB-PD contract, charger, cell-balance, or model-aware threshold logic. + +### `thermal_fan.sh` -> `module_thermal.log` + +Inventories all hwmon temperature/fan inputs. It compares temperature with each sensor's exported +`max`/`crit` threshold. Above 90 C without an exported threshold is reported as a high observation, +not a hardware trip. Exported thresholds above 200 C are treated as invalid sentinel values and ignored. + +Limitations: a 0 RPM export needs context; the fan may legitimately be stopped at low load or the +sensor may not be the system fan. + +### `storage_smart.sh` -> `module_storage.log` + +Runs NVMe SMART health and selected warning/error fields through `smartctl` for `/dev/nvme0n1`. + +Limitations: fixed device path; USB, SD, or additional NVMe devices are not SMART-scanned. + +### `fs_integrity.sh` -> `module_fs.log` + +Reads BTRFS device statistics for mounted BTRFS filesystems and ext4 superblock state for mounted ext4 +devices. Nonzero BTRFS counters are persistent and need device/time context. + +Limitations: read-only mounted-state inspection, not an offline `fsck`, scrub, or repair. + +## Session, display, audio, and network + +### `display_blackout.sh` -> `module_display.log` + +Inventories DRM connectors, identifies a connected eDP panel, reads EDID size, backlight, deduplicated +DRM CRTC/plane state, Gamescope backend data, selected current/recent kernel warnings, and sleep mode. +With `--display-black`, it can emit: + +- `LIVE_RENDER_TO_PHYSICAL_SCANOUT_GAP` when eDP/EDID, backlight, and CRTC remain live; +- `PANEL_OR_MODESET_STATE_INCOMPLETE` when a required state is missing. + +Limitations: root/debugfs required for planes; software cannot observe physical pixels; LCD backlight +logic is not directly portable to OLED. + +### `dock_usb_c.sh` -> `module_dock.log` + +Inventories USB topology, driver-exported Type-C roles/partner/PD/DisplayPort Alt Mode, USB/PD supply +telemetry, external DRM connectors, likely USB Ethernet, and current-boot xHCI/UCSI/Type-C/display-link +errors. It emits a correlation signature when a topology reset/disconnect and selected dock-path error +coexist. + +Limitations: exported voltage/current are instantaneous system telemetry, not electrical certification; +many docks expose no rail or PD fields; deliberate unplug events are normal without aligned errors. + +### `audio_sof.sh` -> `module_audio.log` + +Searches current-boot kernel errors for SOF DSP panic, IPC failures/timeouts, firmware state, and resume +pipeline errors. It also lists ALSA cards/playback devices and the active user's PipeWire nodes. + +Limitations: log matching is broader than one codec; absence may be a user-session/permission issue. + +### `wifi_firmware.sh` -> `module_wifi.log` + +Finds common `wlanN` or `wl*` interfaces, reports link state/info, searches current-boot kernel records +for bounded ath11k/ath12k/iwlwifi/rtw88/b43/brcmfmac driver errors and firmware versions, and lists a +PCI network device. It also flags Wi-Fi and SOF failures retained in the same boot for timestamp review. + +Limitations: a down interface is not proof of a firmware crash; driver pattern coverage is incomplete; +no gateway, DNS, captive-portal, throughput, or Bluetooth test. + +### `gamescope_session.sh` -> `module_gamescope.log` + +Separates Gamescope crashes from normal session-end dumps, counts current-boot starts/restarts using the +active user service's `NRestarts` where available, searches for Vulkan and Wayland errors, and classifies +the MangoApp `/proc//fdinfo` permission abort. + +Limitations: unit names and per-user journal access vary across SteamOS releases. + +### `acpi_pm_state.sh` -> `module_acpi.log` + +Counts current-boot suspend entries and resume exits, lists recent cycles, searches for PM/PCI failures, +fan-controller warnings, wake sources, and the presence of a battery charge-limit interface. + +Limitations: log proximity matters; a fan warning anywhere after any suspend is not automatically a +resume-caused fan failure. + +## Crashes, memory, Steam, and storage media + +### `coredump_analysis.sh` -> `module_coredump.log` + +Aggregates retained dumps by executable; separates current-boot records/signals; classifies all, +steamwebhelper, Gamescope, and Wine/Proton crashes from the last 24 hours; distinguishes +historical/current MangoApp crashes; and reports storage use. A helper `SIGTRAP` is reported separately +from a Gamescope `SIGABRT`/`SIGSEGV` so the two are not treated as equivalent failures. + +Limitations: coredump retention policy and permissions affect visibility; core presence proves a process +terminated by a signal, not the root cause. + +### `memory_swap.sh` -> `module_memory.log` + +Reports RAM/swap totals and usage, current-boot OOM/page-allocation events, swap configuration, and a +short `vmstat` sample. Current implementation warns below 1 GB available, becomes critical below 512 MB, +and flags swap use above 50%. Cumulative pages swapped since boot are history; only the live sample +indicates current swap traffic. + +Limitations: one short sample cannot reconstruct memory pressure at a past incident. + +### `steam_client_logs.sh` -> `module_steam.log` + +Counts actual `.dmp`, `.mdmp`, `.core`, and `.crash` files under `/tmp/dumps`, bounds current-boot and +last-24-hour steamwebhelper crash rates, scans Steam stdout, searches current-boot Steam errors, and +inventories Proton compatibility prefixes/lock files for the active user. + +Limitations: error-word counts contain false positives; a lock file is not automatically corruption. + +### `probe_incidents.sh` -> `module_probe.log` + +Reports whether the optional continuous probe is installed/active and ingests the latest incident's +metadata, trigger, volatile snapshot, and bounded journal tail. No probe means a normal “not installed” +result, not a diagnostic failure. + +Limitations: incidents are unredacted; journal persistence and a functioning kernel/storage path are +required; signature proximity is not causation. See [Continuous incident probe](Continuous-Incident-Probe.md). + +### `mmc_sd_card.sh` -> `module_mmc.log` + +Lists mmc devices and mounts, searches mmc/SDHCI and ext4-on-mmc errors, finds selected TRIM failures, +and reports size/read-only state. + +Limitations: no offline filesystem repair, counterfeit-card capacity test, or flash wear assessment. + +### `dxvk_page_fault.sh` -> `module_dxvk.log` + +Classifies selected AMD VM/UTCL2 page-fault client IDs (CB, DB, CPF, CPD), mapping/walker errors, +possible process attribution, ring timeouts, and GPU-reset outcome. The module title says DXVK/VKD3D +correlation because those are hypotheses to correlate, not conclusions derived from a kernel client ID. + +Limitations: kernel log labels do not uniquely identify DXVK/VKD3D or prove hardware failure. + +## Remediation modules + +### `rem_audio_sof.sh` + +Triggered by a current-boot DSP panic or IPC `-22`; backs up audio state, reloads +`snd_sof_amd_vangogh`, and verifies ALSA cards plus post-cursor errors. It is dispatched by `--fix`. + +### `rem_display_blackout.sh` + +Requires an explicitly declared physical-black symptom, live Gamescope, connected eDP, readable EDID, +and nonzero backlight. It backs up DRM state, applies `gamescopectl composite_force 1`, optionally +installs one user Lua policy, and reports `PARTIAL` until a human confirms the image. + +Read [Safe remediation policy](Safe-Remediation-Policy.md) before extending either module. diff --git a/docs/wiki/Network-and-Resume-Problems.md b/docs/wiki/Network-and-Resume-Problems.md new file mode 100644 index 0000000..8457e5d --- /dev/null +++ b/docs/wiki/Network-and-Resume-Problems.md @@ -0,0 +1,89 @@ +# Network and resume problems + +“Wi-Fi is broken” can mean the PCI/firmware device disappeared, the interface is down, association +failed, DHCP/gateway/DNS failed, a captive portal intervened, or only Steam is offline. DeckDoc currently +covers the first three best. + +## Capture the failed state + +```bash +sudo ./deckdoc.sh +``` + +Record whether the issue started after wake, whether the Wi-Fi icon is misleading, whether other devices +can reach the same network, and whether local IP/gateway traffic works even when DNS/Steam does not. + +Read `module_wifi.log` and `module_acpi.log`. If audio also disappeared after the same wake, inspect +`module_audio.log`; upstream reports show that both failures can share a resume window without proving +they share one root cause. + +`module_wifi.log` searches the current boot for bounded ath11k, ath12k, iwlwifi, rtw88, b43, and +brcmfmac names, reports available firmware-version lines, and emits a coupled Wi-Fi/SOF signature when +both classes of failure exist. That signature is a prompt to compare timestamps with one resume—not a +claim that Wi-Fi caused the audio failure or vice versa. + +## Interpret the signals + +### No `wlanN` interface + +This is stronger than “not connected.” Correlate PCI device presence and driver/firmware errors. A +missing interface after resume can indicate failed reinitialization, but model/driver coverage is not +complete. + +### Interface `DOWN` + +`DOWN` may be administrative (airplane mode, service action) or a failure. Look for a matching firmware +crash, resume failure, or device disappearance before escalating severity. + +Driver names are matched at token boundaries. Text that merely contains the characters `b43`—for +example part of an unrelated identifier—must not be classified as a Broadcom driver error. + +### Interface present and linked + +DeckDoc does not currently separate DHCP, gateway, DNS, captive portal, router, or Steam service faults. +Useful read-only checks include: + +```bash +ip address show +ip route +resolvectl status +``` + +Do not post full addresses, SSIDs, or BSSIDs without redaction. + +## Recovery order + +After preserving evidence: + +1. Toggle Wi-Fi off/on in SteamOS and recheck interface/link state. +2. Test another known-good network if available. +3. Use a normal restart if the device/firmware is missing. +4. Only reload a specific wireless driver after identifying the actual adapter and module. + +DeckDoc does not yet ship Wi-Fi remediation. Never copy a hard-coded `modprobe -r ath11k_pci` command +onto a model using a different driver. Driver removal may disrupt the current SSH connection and can +leave the device unavailable until reboot. + +## Suspend/resume interpretation + +`module_acpi.log` counts `PM: suspend entry` and `PM: suspend exit`/resume messages, then searches for +selected PM/PCI/fan warnings. A completed resume does not guarantee every device resumed. Conversely, +a wireless error somewhere in a boot with a suspend is not automatically caused by that suspend; +timestamps must align. + +## Fan failure after resume + +If the fan reads 0 RPM while APU temperature rises after wake, stop the workload and move to +[Power, thermal and battery](Power-Thermal-and-Battery-Problems.md). SteamOS issue #2475 documents a +charge-limit/sleep case; treat it as a pattern to test, not a universal cause. + +## Escalation evidence + +Include model, adapter/driver/firmware line, interface presence/state, exact wake time, PM transition, +audio co-failure, network scope (one AP/all APs), and recovery outcome. + +## References + +- [SteamOS #2313: audio and rare Wi-Fi failure after resume](https://github.com/ValveSoftware/SteamOS/issues/2313) +- [SteamOS #2475: fan resume failure at charge limit](https://github.com/ValveSoftware/SteamOS/issues/2475) +- [systemd journalctl boot filtering](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html) diff --git a/docs/wiki/Power-Thermal-and-Battery-Problems.md b/docs/wiki/Power-Thermal-and-Battery-Problems.md new file mode 100644 index 0000000..94c2baf --- /dev/null +++ b/docs/wiki/Power-Thermal-and-Battery-Problems.md @@ -0,0 +1,76 @@ +# Power, thermal, fan, charging, and battery problems + +Physical safety outranks diagnostics. Stop using and disconnect the Deck from power if safe when there +is swelling, smoke, liquid, sparking, an electrical smell, damaged charging hardware, or abnormal heat +while idle/off. Contact Steam Support; do not stress-test or open the device. + +## Symptom branches + +- sudden power loss under load; +- will not power on or charge; +- battery percentage jumps or runtime is unexpectedly short; +- charging is slow/intermittent; +- fan stays at 0 RPM while temperature rises; +- high temperatures, throttling, or low CPU/GPU clocks; +- problem begins after sleep/wake or at a charge limit. + +For a safe, booted device run: + +```bash +sudo ./deckdoc.sh +``` + +Read battery, thermal, GPU, ACPI, memory, and storage sections together. + +## Battery telemetry + +DeckDoc records raw exported `capacity`, `voltage_now`, `current_now`, charge, and energy values. Units +come from the kernel power-supply interface and current sign conventions can vary. + +Important limitations: + +- the implemented 6.6 V warning is not model-aware; +- one voltage/current sample does not establish battery health; +- `energy_full` versus design can inform capacity loss but is not a cell-level diagnosis; +- DeckDoc does not inspect the USB-PD contract or charger/cable electrical quality. + +Use Valve's model-specific charging LED and power-button guidance before inferring board failure. + +## Thermal and fan interpretation + +DeckDoc prefers each hwmon sensor's exported `max` and `crit` thresholds. When no critical threshold is +exported, above 90 C is retained as a warning observation, not called a hardware trip. + +A 0 RPM reading is urgent when all of the following align: + +- it is the actual Deck fan input; +- APU temperature is rising; +- the system is under load; +- the state persists rather than being a low-load fan-stop moment; +- ACPI/fan logs align with a resume event. + +Stop the workload and allow the device to cool. DeckDoc does not yet restart the fan controller. + +SteamOS issue #2475 describes a fan-resume failure when sleep occurs while charging at a configured +charge limit. Use it as a testable pattern and compare timestamps; do not assume every stopped fan has +that cause. + +## Low-frequency state + +An instantaneous low CPU/GPU clock can be normal at idle. A suspected 400 MHz CPU or 200 MHz GPU lock +needs sustained sampling under a known workload plus temperature, power, and kernel context. A GPU +reset, thermal limit, or power problem can produce similar poor performance. + +## Recovery and escalation + +- For depleted power/no start, follow Valve's basic use and charging checks. +- For a safe booted system, collect before restarting. +- Do not write TDP, clock, undervolt, charge-current, panel-power, or PMIC controls as a diagnostic + experiment. +- Repeated sudden shutdowns, charging faults with known-good official power, swelling, or persistent fan + failure require Steam Support/hardware service. + +## References + +- [Valve: Steam Deck basic use and troubleshooting](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28) +- [SteamOS #2475: fan fails after sleep at charge limit](https://github.com/ValveSoftware/SteamOS/issues/2475) diff --git a/docs/wiki/Privileged-Diagnostic-Authorization.md b/docs/wiki/Privileged-Diagnostic-Authorization.md new file mode 100644 index 0000000..52bc115 --- /dev/null +++ b/docs/wiki/Privileged-Diagnostic-Authorization.md @@ -0,0 +1,63 @@ +# Privileged diagnostic authorization + +Some DeckDoc evidence is visible only to root. DeckDoc can either ask for `sudo` on every full report, +or the owner can approve a small read-only command set once. This is useful when a helper or diagnostic +agent needs to collect evidence later without learning, storing, or repeatedly requesting the user's +password. + +## What approval installs + +```bash +sudo ./privileged/install-authorized.sh install +``` + +That one interactive approval installs: + +- a root-owned snapshot of `deckdoc.sh` and its diagnostic modules; +- a root-owned broker at `/var/lib/deckdoc-authorized/bin/deckdoc-authorized`; +- a SHA-256 manifest checked before each report; +- exact `sudoers` entries for the approving user. + +The allowlist contains five exact operations: a normal read-only report, a read-only report with the +physical-black symptom declared, a manual incident-probe capture, probe status, and snapshot version. +It does **not** allow arbitrary arguments, output paths, environment variables, programs, shells, or +DeckDoc remediation modes. + +## Use it later without a password prompt + +```bash +./privileged/deckdoc-authorized-client.sh report +./privileged/deckdoc-authorized-client.sh status +./privileged/deckdoc-authorized-client.sh probe-capture +``` + +The client invokes `sudo -n`, receives the report over standard output, and writes it as a private file +under `logs/`. The privileged broker never accepts a user-selected destination. An agent can invoke +these exact client actions, but the authorization is not a general delegation of root access. + +## Security and update boundary + +- The installed application and broker are owned by root and verified against their manifest. +- The repository checkout remains user-writable but is not executed as root by the authorized command. +- A Git pull does not silently change the privileged snapshot. +- Updating that snapshot requires running the interactive install command again. +- Remediation remains outside the passwordless allowlist and requires a separate explicit action. +- Reports are private but unredacted. Review them before sharing. + +If the integrity check fails, the broker refuses to run and asks for reinstallation. This detects a +changed or incomplete installed snapshot; it is not a substitute for package signing or host security. + +## Remove authorization + +```bash +sudo ./privileged/install-authorized.sh uninstall +``` + +This removes the current user's `sudoers` rule while preserving the installed snapshot. After every +authorized user has been removed, delete the snapshot separately: + +```bash +sudo ./privileged/install-authorized.sh purge +``` + +Removal is deliberately split so revoking access does not unexpectedly destroy diagnostic state. diff --git a/docs/wiki/Reading-DeckDoc-Reports.md b/docs/wiki/Reading-DeckDoc-Reports.md new file mode 100644 index 0000000..8ebe6bb --- /dev/null +++ b/docs/wiki/Reading-DeckDoc-Reports.md @@ -0,0 +1,113 @@ +# Reading DeckDoc reports + +DeckDoc reports observations from several time scopes and privilege contexts. Interpretation is about +correlation, not counting the words `WARNING` and `CRITICAL`. + +## Report structure + +The timestamped master report contains all module sections. Per-module logs make it easier to share or +compare one subsystem. + +| Log | Module | +|---|---| +| `module_gpu.log` | GPU/APU | +| `module_battery.log` | Battery/PMIC | +| `module_thermal.log` | Thermal/fan | +| `module_storage.log` | NVMe SMART | +| `module_fs.log` | Filesystem integrity | +| `module_audio.log` | SOF/ALSA/PipeWire audio | +| `module_display.log` | eDP/backlight/CRTC/DRM/Gamescope display path | +| `module_coredump.log` | systemd core dumps | +| `module_wifi.log` | Wi-Fi interface and firmware | +| `module_gamescope.log` | Gamescope and MangoApp session health | +| `module_memory.log` | RAM, swap, and OOM | +| `module_steam.log` | Steam dumps/logs and Proton prefixes | +| `module_mmc.log` | microSD/mmc | +| `module_acpi.log` | suspend/resume | +| `module_dxvk.log` | GPU VM/page-fault classification | + +## Confidence ladder + +- **Observation:** the report read a value or log entry. +- **Time-correlated:** its timestamp aligns with the incident. +- **Cross-correlated:** an independent subsystem agrees. +- **Likely cause:** evidence supports one branch more strongly than alternatives. +- **Confirmed recovery:** a narrow action changed the signature and the user-observed symptom. +- **Root cause:** requires enough evidence to explain why the failure occurred, not just how it was + recovered. + +For example, a game becoming visible after forced composition confirms that the presentation-path +change recovered that occurrence. It does not by itself prove whether the underlying defect was in +Gamescope, DRM/DCN, panel timing, the cable, or the panel. + +## Current boot versus retained history + +`journalctl -b 0` is the current boot. `coredumpctl` can retain crashes from older boots, and the +display module intentionally looks back seven days for selected warnings. A retained crash is useful +history but should not be reported as active instability unless it matches the incident time. + +### Crashes and retained history + +The core-dump module separates: + +- historical counts by executable; +- current-boot dump count; +- current-boot `SIGTRAP`, `SIGABRT`, and `SIGSEGV` counts; +- historical and current-boot MangoApp counts; +- disk space used by retained dumps. + +`SIGTRAP` from `steamwebhelper` is not automatically harmless, but it is not equivalent to a +Gamescope `SIGSEGV`. Read the executable, signal, boot, and surrounding journal together. + +## Severity is contextual + +- `CRITICAL` means the matched condition can represent serious failure, not that every surrounding + symptom has the same cause. +- A stopped fan is more urgent when temperature is rising after resume than when the system is off or + a sensor is not the Deck fan. +- A high temperature without an exported hardware critical threshold is an observation, not proof of + thermal shutdown. +- A Wi-Fi interface marked `DOWN` can be administratively disabled; a firmware crash in the same + window is stronger evidence. +- More than one active DRM plane is normal by itself. It becomes relevant to the validated blackout + only when rendering, eDP, EDID, backlight, and CRTC remain live while the physical panel is black. + +## Empty, missing, and inaccessible + +The following are different: + +- `No ... detected` after a successful read; +- `command not found` because an optional dependency is absent; +- `permission denied` because the report lacked access; +- a path not existing because the model/kernel exports a different interface; +- a per-user service being queried as the wrong user. + +DeckDoc routes several Gamescope, PipeWire, Steam, and user-journal reads through the active session +user during a root run. If session-user resolution fails, treat those sections as incomplete. + +## Strong correlation examples + +### Audio after resume + +SOF IPC error `-22` or `DSP panic` + missing ALSA card + missing PipeWire sink + a nearby resume event +supports an audio-DSP failure. A muted sink with healthy cards does not. + +### GPU or game crash + +`amdgpu_job_timedout` + a reset result + a matching game/Gamescope core dump + the same incident time +supports a GPU-driven crash chain. A historical GPU reset days earlier does not explain today's exit. + +### Storage + +mmc I/O errors + ext4 errors for the same `mmcblk` device + game file corruption strongly supports an +SD path problem. A game validation failure without device/filesystem errors can have other causes. + +## Compare reports + +Keep one healthy baseline and one failure report. Compare exact sections instead of entire files: + +```bash +diff -u healthy-module_audio.log failed-module_audio.log +``` + +Do not publish a raw report until it has been reviewed for private data. diff --git a/docs/wiki/Recovery-and-Escalation.md b/docs/wiki/Recovery-and-Escalation.md new file mode 100644 index 0000000..2179a72 --- /dev/null +++ b/docs/wiki/Recovery-and-Escalation.md @@ -0,0 +1,75 @@ +# Recovery and escalation + +Recovery restores operation. Diagnosis explains the failure. Preserve evidence before recovery when +the device is safe and responsive enough to do so. + +## Recovery ladder + +Use the least destructive step that matches the evidence: + +1. **Capture:** DeckDoc report, Steam System Report, current/previous boot journals, physical symptom. +2. **Narrow reversible action:** reconnect one device, toggle one service/setting, or use a documented + DeckDoc remediation whose precheck matches. +3. **Normal restart:** appropriate for wedged firmware/session state after evidence capture. +4. **Clean control run:** updated stable system, third-party plugins/modifications disabled, one known + title/device/network. +5. **Rollback previous SteamOS:** official recovery option that retains user data where supported. +6. **Repair SteamOS:** official recovery environment; understand the option before selecting it. +7. **Re-image/factory reset:** destructive, clears data; backup first. +8. **Hardware service/Steam Support:** physical safety, repeated cross-software faults, or failed storage, + panel, board, battery, charging, fan, or input evidence. + +Valve's recovery page is authoritative for the current options and their data impact. Do not follow an +old third-party button sequence when the official instructions differ. + +## When not to keep troubleshooting + +Stop and contact support for: + +- smoke, swelling, liquid, electrical smell, sparking, or charging-port damage; +- abnormal heat while idle/off or a fan that remains stopped as temperature rises; +- recurring hard locks/GPU reset failures across unrelated software; +- storage errors that threaten data or prevent safe backup; +- a panel/input/charging fault that persists in firmware/recovery environments; +- any repair that would require opening the device when you are not equipped to do so. + +## Choosing where to report + +### Steam Support + +Use for warranty/hardware, safety, account-specific issues, recovery failure, and guided service. + +### Valve SteamOS issue tracker + +Use for reproducible SteamOS/kernel/session regressions with version, model, steps, timestamps, and +redacted system evidence. Search first and add a high-quality reproduction to an existing matching +issue when appropriate. + +### Gamescope or component tracker + +Use when the evidence isolates a reproducible upstream component and includes its logs/version. A +generic “black screen” without branch evidence is unlikely to be actionable. + +### DeckDoc issue tracker + +Use when DeckDoc misclassifies a state, misses a documented signature, has an unsafe/incorrect command, +or needs a new module. Include a sanitized fixture whenever possible. + +## Minimum escalation packet + +- Steam Deck LCD/OLED model and relevant storage/peripheral; +- SteamOS, client, kernel, update channel; +- exact incident time and boot scope; +- Game/Desktop Mode, dock/power/wake state; +- reproducible steps and frequency; +- relevant report sections, not an unexplained dump; +- what has been ruled out and how; +- recovery/test outcome; +- privacy redaction statement. + +## Official references + +- [Valve: SteamOS Recovery and Troubleshooting](https://help.steampowered.com/en/faqs/view/1B71-EDF2-EB6D-2BB3) +- [Valve: Steam Deck Basic Use and Troubleshooting](https://help.steampowered.com/en/faqs/view/69E3-14AF-9764-4C28) +- [Valve SteamOS issues](https://github.com/ValveSoftware/SteamOS/issues) +- [DeckDoc issues](https://github.com/deucebucket/deckdoc/issues) diff --git a/docs/wiki/Research-and-Issue-Index.md b/docs/wiki/Research-and-Issue-Index.md new file mode 100644 index 0000000..d8f437d --- /dev/null +++ b/docs/wiki/Research-and-Issue-Index.md @@ -0,0 +1,104 @@ +# Research and issue index + +This page maps DeckDoc's repository issues and upstream evidence to shipped behavior. Upstream issue +reports are field evidence, not automatically confirmed root causes. + +## DeckDoc issue-backed modules + +| DeckDoc issue | Topic | Implemented | Wiki route | +|---|---|---|---| +| [#1](https://github.com/deucebucket/deckdoc/issues/1) | SOF DSP panic / IPC `-22` | `audio_sof.sh`, `rem_audio_sof.sh` | [Audio](Audio-Problems.md) | +| [#2](https://github.com/deucebucket/deckdoc/issues/2) | systemd core-dump analysis | `coredump_analysis.sh` | [Crashes/report reading](Reading-DeckDoc-Reports.md#crashes-and-retained-history) | +| [#3](https://github.com/deucebucket/deckdoc/issues/3) | Wi-Fi firmware after resume | `wifi_firmware.sh` | [Network/resume](Network-and-Resume-Problems.md) | +| [#4](https://github.com/deucebucket/deckdoc/issues/4) | Gamescope session health | `gamescope_session.sh` | [Display/Gamescope](Display-and-Gamescope-Problems.md) | +| [#5](https://github.com/deucebucket/deckdoc/issues/5) | memory/swap/OOM | `memory_swap.sh` | [Crashes/GPU/memory](Crashes-GPU-and-Memory.md) | +| [#6](https://github.com/deucebucket/deckdoc/issues/6) | Steam client logs | `steam_client_logs.sh` | [Evidence](Collecting-and-Sharing-Evidence.md) | +| [#7](https://github.com/deucebucket/deckdoc/issues/7) | microSD/mmc errors | `mmc_sd_card.sh` | [Storage/microSD](Storage-and-MicroSD-Problems.md) | +| [#8](https://github.com/deucebucket/deckdoc/issues/8) | suspend/resume state | `acpi_pm_state.sh` | [Network/resume](Network-and-Resume-Problems.md) | +| [#9](https://github.com/deucebucket/deckdoc/issues/9) | DXVK/VKD3D GPU fault differentiation | `dxvk_page_fault.sh` | [Crashes/GPU/memory](Crashes-GPU-and-Memory.md) | + +Issues #2–#9 may still be open in GitHub even though their initial diagnostic modules shipped in PR +[#10](https://github.com/deucebucket/deckdoc/pull/10). Treat them as implementation/follow-up trackers, +not as evidence that the modules are absent. + +The diagnostic-center follow-up closes their remaining acceptance gaps with time-bounded crash-family +classification, broader wireless-driver discovery and coupled resume evidence, user-service restart +counts, live-versus-cumulative memory pressure, bounded Steam helper crash rates, severity-aware mmc/ext4 +signals, live fan/temperature and charge-limit context, and neutral GPU-fault attribution. Each branch +has a healthy/triggered regression fixture; the issues should close when that change merges. + +PR [#14](https://github.com/deucebucket/deckdoc/pull/14) added display-blackout correlation, guarded +forced composition, current-versus-historical crash cleanup, session-user routing, and the original +incident runbooks. + +The [long-off startup blackout](Black-Screen-After-Long-Shutdown.md) is a community-reported research +case added on 2026-07-21. It remains unverified and intentionally separate from PR #14's live-render, +multi-plane LCD signature. + +## Full-platform follow-up issues + +| Issue | Scope | Current branch | +|---|---|---| +| [#15](https://github.com/deucebucket/deckdoc/issues/15) | Model and capability manifest | Priority foundation; not implemented yet | +| [#16](https://github.com/deucebucket/deckdoc/issues/16) | Reproducible, signed DeckDoc Rescue | Alpha collector and builder added | +| [#17](https://github.com/deucebucket/deckdoc/issues/17) | Unified incident timeline and evidence-access ledger | Source data exists; normalization remains | +| [#18](https://github.com/deucebucket/deckdoc/issues/18) | Continuous-probe production hardening | Opt-in bounded prototype added | +| [#19](https://github.com/deucebucket/deckdoc/issues/19) | Privileged authorization security review | Exact-command broker prototype added | +| [#20](https://github.com/deucebucket/deckdoc/issues/20) | Dock/USB-C/PD validation | Read-only module and fixtures added | +| [#21](https://github.com/deucebucket/deckdoc/issues/21) | Network stages, Bluetooth/input, and update health | Wiki routes exist; module gaps remain | +| [#22](https://github.com/deucebucket/deckdoc/issues/22) | Redacted bundle and storage-risk gate | Safety requirements documented; implementation remains | +| [#23](https://github.com/deucebucket/deckdoc/issues/23) | DeckMD GitHub Pages symptom checker | Static checker and schema validation added | + +The [deep research catalog](../research/steamdeck-issue-deep-dive.md) covers 33 failure families, +decision boundaries, detector candidates, and primary sources behind these priorities. + +## Upstream symptom evidence + +### SteamOS and Steam + +- [Reviewing log information](https://github.com/ValveSoftware/SteamOS/wiki/Reviewing-log-information) + documents `/tmp/dumps/`, `steam_stdout.txt`, and journal-based log review. +- [SteamOS #1376](https://github.com/ValveSoftware/SteamOS/issues/1376) reports loss of audio after + sleep. +- [SteamOS #2313](https://github.com/ValveSoftware/SteamOS/issues/2313) reports SOF Vangogh IPC `-22` + and a rarer wireless failure after resume. +- [SteamOS #2475](https://github.com/ValveSoftware/SteamOS/issues/2475) reports a fan-resume problem + when sleeping while charging at a configured charge limit. +- [SteamOS #2037](https://github.com/ValveSoftware/SteamOS/issues/2037) reports SD/ext4 corruption on a + SteamOS handheld; it informs detection patterns but is not Deck-specific proof. +- [SteamOS #1324](https://github.com/ValveSoftware/SteamOS/issues/1324), + [#2632](https://github.com/ValveSoftware/SteamOS/issues/2632), and + [#1015](https://github.com/ValveSoftware/SteamOS/issues/1015) provide comparison black-screen cases + with different scopes and GPU evidence. + +### Gamescope and graphics + +- [Gamescope](https://github.com/ValveSoftware/gamescope) describes the compositor/direct-flip role in + the SteamOS presentation path. +- [Gamescope #1368](https://github.com/ValveSoftware/gamescope/issues/1368) documents direct-scanout/ + plane-transition artifacts improved by forced composition on multiple AMD devices. +- [Linux AMD Display Core debugging](https://docs.kernel.org/gpu/amdgpu/display/dc-debug.html) + emphasizes dmesg and pre/post display state. +- [Linux AMDGPU debugfs](https://docs.kernel.org/gpu/amdgpu/debugfs.html) documents low-level debug + interfaces, including why DeckDoc does not blindly trigger a GPU reset. + +### Linux data sources + +- [journalctl](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html) defines boot, + kernel, unit, and priority filtering. +- [coredumpctl](https://www.freedesktop.org/software/systemd/man/latest/coredumpctl.html) defines crash + matching, metadata, storage, and access limitations. +- [BTRFS device stats](https://btrfs.readthedocs.io/en/latest/btrfs-device.html#device-stats) defines + persistent read/write/flush/corruption/generation counters. + +## Research rules for new signatures + +Before adding a signature or fix: + +1. Link a primary source or attach a sanitized real incident. +2. State affected model/version and whether the issue is open, fixed, or unknown. +3. Separate reporter hypothesis from maintainer-confirmed cause. +4. Define exact time-scoped detection and likely false positives. +5. Add a fixture for healthy, triggered, unavailable, and stale-history states. +6. For remediation, document precheck, backup, action, verify, report, and rollback. +7. Never turn one anecdote into a universal Deck rule. diff --git a/docs/wiki/Storage-and-MicroSD-Problems.md b/docs/wiki/Storage-and-MicroSD-Problems.md new file mode 100644 index 0000000..200d1ec --- /dev/null +++ b/docs/wiki/Storage-and-MicroSD-Problems.md @@ -0,0 +1,70 @@ +# NVMe, filesystem, and microSD problems + +Storage symptoms include corrupt downloads, games failing validation, I/O errors, read-only media, +missing mounts, slow installs, boot failure, and disappearing SD cards. Preserve data before repair. + +## Capture + +```bash +sudo ./deckdoc.sh +``` + +Read `module_storage.log`, `module_fs.log`, `module_mmc.log`, and the kernel/crash sections. Record the +exact device and mount containing the affected game or system path. + +## Internal NVMe + +DeckDoc runs SMART health against `/dev/nvme0n1`. A SMART failure or critical-warning/error field is +important, but the fixed path may miss replacement or differently enumerated storage. Confirm with: + +```bash +lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS,MODEL +``` + +Do not run write/destructive tests on an unbacked-up device. + +## BTRFS and ext4 + +BTRFS device statistics are persistent counters for read/write/flush/corruption/generation errors. +Nonzero values show that errors occurred; compare timestamps and whether counters increase. Official +BTRFS documentation explains that these counters persist and can be updated by normal I/O or scrub. + +DeckDoc reads ext4 superblock state for mounted devices but does not run `fsck`. Never run an offline +repair command against a mounted filesystem, and never guess the partition name. + +## microSD/mmc + +Strong evidence includes: + +- mmc/SDHCI I/O, timeout, signal-verification, or corruption messages; +- `EXT4-fs error` naming the same `mmcblk` device; +- the device switching read-only; +- mount disappearance in the same incident window; +- repeated game corruption only on that card. + +A game file validation failure alone does not prove media corruption. SteamOS issue #2037 documents an +SD corruption report on another SteamOS handheld; it is research context, not proof that every Deck SD +failure has the same cause. + +## Safe response + +1. Stop writes/downloads to the suspected device. +2. Save the report and irreplaceable data if the device remains readable. +3. Resolve the exact block device and mount. +4. Unmount before any filesystem repair. +5. Use a separate trusted system/recovery environment for repair or imaging when needed. + +DeckDoc does not format, trim, scrub, delete, or repair storage automatically. Its roadmap may add +operations only with exact device resolution, backup, explicit confirmation, and verification. + +## Escalation + +Escalate for increasing SMART/BTRFS counters, repeated mmc I/O errors across clean boots, read-only +media, inability to back up, or boot failure. For SteamOS image recovery, follow +[Recovery and escalation](Recovery-and-Escalation.md). + +## References + +- [BTRFS device statistics](https://btrfs.readthedocs.io/en/latest/btrfs-device.html#device-stats) +- [SteamOS #2037: SD card file corruption report](https://github.com/ValveSoftware/SteamOS/issues/2037) +- [Valve SteamOS recovery and troubleshooting](https://help.steampowered.com/en/faqs/view/1B71-EDF2-EB6D-2BB3) diff --git a/docs/wiki/Triage-Flow.md b/docs/wiki/Triage-Flow.md new file mode 100644 index 0000000..40df775 --- /dev/null +++ b/docs/wiki/Triage-Flow.md @@ -0,0 +1,74 @@ +# Steam Deck triage flow + +Use the symptom that is happening now, not the diagnosis you expect. + +## 1. Is the device physically unsafe? + +Stop and disconnect power if safe when there is smoke, swelling, liquid ingress, an electrical smell, +a damaged USB-C port/cable, sparking, or abnormal heat while idle/off. Do not charge, open, stress-test, +or run DeckDoc. Contact Steam Support. + +## 2. Does the Deck boot? + +- **No power or charging response:** use Valve's basic power/charging checks, then Steam Support. +- **Firmware/recovery menu appears but SteamOS will not boot:** go to + [Recovery and escalation](Recovery-and-Escalation.md). +- **SteamOS begins booting but loops, freezes, or enters a black screen:** if SSH works, capture DeckDoc + and the previous/current boot journals before repair or re-image. +- **Only the first start after several days off is black, with sound, and a second boot works:** use the + [long-off startup blackout protocol](Black-Screen-After-Long-Shutdown.md). +- **SteamOS boots:** continue below. + +## 3. Is the whole system dead or only one output? + +Test without changing state: + +- Does audio continue? +- Do controls produce sounds or haptics? +- Does SSH still connect? +- Does Steam Game Recording or streaming show advancing frames? +- Does the fan respond to load? +- Does only the internal panel fail while an external monitor works, or vice versa? + +If sound/rendering/input continue with a black internal LCD, use +[Screen black while sound works](Steam-Deck-Black-Screen-Sound-Working.md). If the game and controls +freeze or the session returns to Library, use [Crashes, GPU and memory](Crashes-GPU-and-Memory.md). + +## 4. Pick the subsystem branch + +| What you observe | First report sections | Guide | +|---|---|---| +| Built-in LCD black, Deck otherwise alive | display, GPU, Gamescope, coredump | [Display and Gamescope](Display-and-Gamescope-Problems.md) | +| First start after days off is black; second boot works | previous-boot journal, power/boot/display state | [Long-off startup blackout](Black-Screen-After-Long-Shutdown.md) | +| Game closes, freezes, or returns to Library | coredump, Steam, Gamescope, GPU, memory | [Crashes, GPU and memory](Crashes-GPU-and-Memory.md) | +| No sound/device after sleep | audio, ACPI, coredump | [Audio problems](Audio-Problems.md) | +| Wi-Fi missing/down after sleep | Wi-Fi, ACPI, audio | [Network and resume](Network-and-Resume-Problems.md) | +| Sudden shutdown, fan/heat, low clocks | thermal, battery, GPU, ACPI | [Power, thermal and battery](Power-Thermal-and-Battery-Problems.md) | +| Corrupt game files, SD disappears/read-only | mmc, filesystem, SMART | [Storage and microSD](Storage-and-MicroSD-Problems.md) | +| Dock, charging, external display, USB Ethernet | dock, display, battery, ACPI | [Dock/USB-C](Dock-USB-C-and-External-Displays.md) | +| Controller, Bluetooth, touch | baseline report plus manual evidence | [Controls/Bluetooth/input](Controls-Bluetooth-and-Input.md) | +| Installed OS will not boot | Rescue/live hardware plus installed journal image | [DeckDoc Rescue](DeckDoc-Rescue.md) | +| Hardware versus software is unclear | controlled A/B evidence | [Hardware decision guide](Hardware-Failure-Decision-Guide.md) | + +## 5. Establish time and scope + +Record: + +- exact local time and current boot; +- LCD or OLED model and storage device involved; +- SteamOS/Steam client channel and version; +- Game Mode or Desktop Mode, docked or handheld; +- whether the event followed boot, wake, dock/undock, overlay toggle, game launch, update, or plugin + change; +- whether it affects one title, all titles, or the whole OS; +- whether it reproduces after a normal restart with third-party tools disabled. + +## 6. Preserve, test, verify + +1. Save the report and relevant logs. +2. Redact private data before upload. +3. Choose one reversible experiment that tests the leading explanation. +4. Reproduce or monitor without stacking unrelated tweaks. +5. Re-run DeckDoc and physically confirm the result. + +Avoid changing several variables at once. A successful reboot proves recovery, not root cause. diff --git a/docs/wiki/_Footer.md b/docs/wiki/_Footer.md new file mode 100644 index 0000000..3734b76 --- /dev/null +++ b/docs/wiki/_Footer.md @@ -0,0 +1,3 @@ +DeckDoc records evidence before action. Preserve the incident, change one variable at a time, verify the +result, and use [Steam Support](https://help.steampowered.com/en/wizard/HelpWithSteamDeck) for safety, +warranty, and hardware concerns. diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md new file mode 100644 index 0000000..1ee548b --- /dev/null +++ b/docs/wiki/_Sidebar.md @@ -0,0 +1,34 @@ +## DeckDoc diagnostic center + +- [Home](Home) +- [Getting started](Getting-Started) +- [Triage flow](Triage-Flow) +- [Reading reports](Reading-DeckDoc-Reports) +- [Collect and share evidence](Collecting-and-Sharing-Evidence) +- [Continuous incident probe](Continuous-Incident-Probe) +- [Privileged authorization](Privileged-Diagnostic-Authorization) + +### Problems + +- [Crashes, GPU and memory](Crashes-GPU-and-Memory) +- [Audio](Audio-Problems) +- [Network and resume](Network-and-Resume-Problems) +- [Power, thermal and battery](Power-Thermal-and-Battery-Problems) +- [Storage and microSD](Storage-and-MicroSD-Problems) +- [Dock, USB-C and displays](Dock-USB-C-and-External-Displays) +- [Controls, Bluetooth and input](Controls-Bluetooth-and-Input) +- [Display and Gamescope](Display-and-Gamescope-Problems) +- [Long-off startup blackout](Black-Screen-After-Long-Shutdown) +- [Black screen with sound](Steam-Deck-Black-Screen-Sound-Working) +- [Coverage and gaps](Coverage-and-Gaps) + +### Recovery and reference + +- [Recovery and escalation](Recovery-and-Escalation) +- [Hardware failure decisions](Hardware-Failure-Decision-Guide) +- [DeckDoc Rescue](DeckDoc-Rescue) +- [Module reference](Module-Reference) +- [Safe remediation policy](Safe-Remediation-Policy) +- [Research and issue index](Research-and-Issue-Index) +- [Display runbook](Display-Diagnostic-Runbook) +- [LCD investigation](LCD-Blackout-Investigation) diff --git a/modules/acpi_pm_state.sh b/modules/acpi_pm_state.sh index 9159f41..9bf8f84 100755 --- a/modules/acpi_pm_state.sh +++ b/modules/acpi_pm_state.sh @@ -4,6 +4,9 @@ set -uo pipefail echo "[MODULE: ACPI Sleep/Wake (PM State)]" sync +HWMON_DIR="${DECKDOC_HWMON_DIR:-/sys/class/hwmon}" +POWER_SUPPLY_ROOT="${DECKDOC_POWER_SUPPLY_ROOT:-/sys/class/power_supply}" + echo "--- Suspend/resume transitions (current boot) ---" SUSPEND_COUNT=0 RESUME_COUNT=0 @@ -62,13 +65,47 @@ if command -v journalctl >/dev/null 2>&1; then fi sync +echo "--- Live fan/temperature cross-check ---" +LIVE_FAN_ZERO=false +MAX_TEMP_RAW=0 +FAN_INPUTS=0 +for fan in "${HWMON_DIR}"/hwmon*/fan*_input; do + [ -r "$fan" ] || continue + FAN_INPUTS=$((FAN_INPUTS + 1)) + rpm=$(cat "$fan" 2>/dev/null || echo unknown) + echo " ${fan}: ${rpm} RPM" + if [ "$rpm" = "0" ]; then LIVE_FAN_ZERO=true; fi +done +for temp in "${HWMON_DIR}"/hwmon*/temp*_input; do + [ -r "$temp" ] || continue + raw=$(cat "$temp" 2>/dev/null || echo 0) + case "$raw" in ''|*[!0-9]*) raw=0 ;; esac + if [ "$raw" -gt "$MAX_TEMP_RAW" ]; then MAX_TEMP_RAW="$raw"; fi +done +if [ "$FAN_INPUTS" -eq 0 ]; then + echo " No live fan RPM input exported." +elif [ "$LIVE_FAN_ZERO" = "true" ] && [ "$SUSPEND_COUNT" -gt 0 ] && [ "$MAX_TEMP_RAW" -ge 70000 ]; then + echo " RESUME_SIGNATURE: LIVE_ZERO_RPM_WITH_HOT_SENSOR_AFTER_SUSPEND" + echo " HIGH: A fan input is 0 RPM while a sensor is at least 70 C after a suspend in this boot. Stop load and verify fan-controller state." +elif [ "$LIVE_FAN_ZERO" = "true" ] && [ "$SUSPEND_COUNT" -gt 0 ]; then + echo " NOTE: A fan input is 0 RPM after a suspend in this boot, but no sensor is currently at least 70 C. Timing and fan-stop policy require correlation." +else + echo " No live hot-sensor/zero-RPM resume signature." +fi +sync + echo "--- Battery charge limit interaction ---" -BAT_DIR="/sys/class/power_supply/BAT1" +BAT_DIR="${POWER_SUPPLY_ROOT}/BAT1" if [ ! -d "$BAT_DIR" ]; then - BAT_DIR="/sys/class/power_supply/BAT0" + BAT_DIR="${POWER_SUPPLY_ROOT}/BAT0" fi -if [ -f "${BAT_DIR}/charge_control_limit" ]; then - echo " Battery charge limit is set (may interact with PM resume bug #2475)." +CHARGE_LIMIT_FILE="" +for candidate in "${BAT_DIR}/charge_control_limit" "${BAT_DIR}/charge_control_end_threshold"; do + if [ -r "$candidate" ]; then CHARGE_LIMIT_FILE="$candidate"; break; fi +done +if [ -n "$CHARGE_LIMIT_FILE" ]; then + echo " Battery charge-limit control: ${CHARGE_LIMIT_FILE}=$(cat "$CHARGE_LIMIT_FILE" 2>/dev/null || echo unreadable)" + echo " NOTE: Compare the configured limit and charging state with the suspend window before applying issue #2475." else echo " No battery charge limit configured." fi diff --git a/modules/coredump_analysis.sh b/modules/coredump_analysis.sh index e448563..09430bd 100755 --- a/modules/coredump_analysis.sh +++ b/modules/coredump_analysis.sh @@ -23,6 +23,7 @@ fi BOOT_START=$(uptime -s 2>/dev/null || true) ALL_DUMPS=$(coredumpctl list --no-legend --no-pager 2>/dev/null || true) +LAST_24H_DUMPS=$(coredumpctl list --no-legend --no-pager --since "24 hours ago" 2>/dev/null || true) if [ -n "$BOOT_START" ]; then BOOT_DUMPS=$(coredumpctl list --no-legend --no-pager --since "$BOOT_START" 2>/dev/null || true) else @@ -49,6 +50,18 @@ count_executable() { ' } +count_signal_for_executable() { + local records="$1" pattern="$2" signal="$3" + printf '%s\n' "$records" | awk -v wanted="$pattern" -v wanted_signal="$signal" ' + { + for (i=1; i<=NF; i++) { + if ($i == wanted_signal && $(i+2) ~ wanted) count++ + } + } + END { print count+0 } + ' +} + count_records() { local records="$1" printf '%s\n' "$records" | awk ' @@ -82,6 +95,29 @@ else fi sync +echo "--- Crash activity in the last 24 hours ---" +LAST_24H_COUNT=$(count_records "$LAST_24H_DUMPS") +STEAMWEBHELPER_24H=$(count_executable "$LAST_24H_DUMPS" '/steamwebhelper$') +GAMESCOPE_24H=$(count_executable "$LAST_24H_DUMPS" '/gamescope(-wl)?$') +WINE_PROTON_24H=$(count_executable "$LAST_24H_DUMPS" '/(wine[^/]*|proton[^/]*)$') +STEAMWEBHELPER_TRAPS_24H=$(count_signal_for_executable "$LAST_24H_DUMPS" '/steamwebhelper$' SIGTRAP) +GAMESCOPE_ABORTS_24H=$(count_signal_for_executable "$LAST_24H_DUMPS" '/gamescope(-wl)?$' SIGABRT) +GAMESCOPE_SEGV_24H=$(count_signal_for_executable "$LAST_24H_DUMPS" '/gamescope(-wl)?$' SIGSEGV) +echo " All crashes: ${LAST_24H_COUNT}" +echo " steamwebhelper crashes: ${STEAMWEBHELPER_24H} (${STEAMWEBHELPER_TRAPS_24H} SIGTRAP)" +echo " Gamescope crashes: ${GAMESCOPE_24H} (${GAMESCOPE_ABORTS_24H} SIGABRT, ${GAMESCOPE_SEGV_24H} SIGSEGV)" +echo " Wine/Proton crashes: ${WINE_PROTON_24H}" +if [ "$LAST_24H_COUNT" -gt 10 ]; then + echo " HIGH: More than 10 crashes were recorded in the last 24 hours." +fi +if [ "$STEAMWEBHELPER_TRAPS_24H" -gt 0 ]; then + echo " NOTE: steamwebhelper SIGTRAP records need incident-time correlation; they are not equivalent to a compositor crash." +fi +if [ $((GAMESCOPE_ABORTS_24H + GAMESCOPE_SEGV_24H)) -gt 0 ]; then + echo " HIGH: Gamescope SIGABRT/SIGSEGV records occurred in the last 24 hours." +fi +sync + echo "--- Steam Deck overlay crash signature ---" MANGOAPP_DUMPS=$(count_executable "$ALL_DUMPS" '/mangoapp$') MANGOAPP_BOOT_DUMPS=$(count_executable "$BOOT_DUMPS" '/mangoapp$') diff --git a/modules/dock_usb_c.sh b/modules/dock_usb_c.sh new file mode 100755 index 0000000..27b7289 --- /dev/null +++ b/modules/dock_usb_c.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +set -uo pipefail + +echo "[MODULE: Dock / USB-C / Power Delivery]" +sync + +SYS_ROOT="${DECKDOC_SYS_ROOT:-/sys}" +TYPEC_ROOT="${DECKDOC_TYPEC_ROOT:-${SYS_ROOT}/class/typec}" +POWER_ROOT="${DECKDOC_POWER_SUPPLY_ROOT:-${SYS_ROOT}/class/power_supply}" +DRM_ROOT="${DECKDOC_DRM_ROOT:-${SYS_ROOT}/class/drm}" + +read_field() { + local file="$1" + if [ -r "$file" ]; then cat "$file" 2>/dev/null || echo inaccessible; else echo not-exported; fi +} + +echo "--- USB topology ---" +if command -v lsusb >/dev/null 2>&1; then + lsusb -t 2>/dev/null || echo " USB topology inaccessible." + echo " USB device IDs (serial numbers intentionally omitted):" + lsusb 2>/dev/null | sed -E 's/(ID [0-9a-fA-F]{4}:[0-9a-fA-F]{4}).*/\1 [description omitted]/' || true +else + echo " lsusb not available." +fi +sync + +echo "--- USB Type-C roles and partner ---" +TYPEC_PORTS=0 +for port in "$TYPEC_ROOT"/port[0-9]*; do + [ -d "$port" ] || continue + case "$(basename "$port")" in *-*) continue ;; esac + TYPEC_PORTS=$((TYPEC_PORTS + 1)) + name=$(basename "$port") + echo " Port: ${name}" + for field in data_role power_role port_type power_operation_mode orientation usb_power_delivery_revision usb_typec_revision; do + echo " ${field}=$(read_field "${port}/${field}")" + done + if [ -e "${TYPEC_ROOT}/${name}-partner" ] || [ -e "${port}/${name}-partner" ]; then + echo " partner=present" + else + echo " partner=not-exported-or-absent" + fi +done +if [ "$TYPEC_PORTS" -eq 0 ]; then + echo " INACCESSIBLE: No Type-C class port is exported by this kernel/driver." +fi + +echo "--- DisplayPort Alternate Mode ---" +ALT_MODE_FOUND=false +for field in "$SYS_ROOT"/bus/typec/devices/*/displayport/configuration \ + "$SYS_ROOT"/bus/typec/devices/*/displayport/pin_assignment \ + "$SYS_ROOT"/bus/typec/devices/*/displayport/hpd; do + [ -r "$field" ] || continue + ALT_MODE_FOUND=true + echo " ${field#${SYS_ROOT}/}=$(cat "$field" 2>/dev/null || echo inaccessible)" +done +if [ "$ALT_MODE_FOUND" = false ]; then echo " DisplayPort Alt Mode state not exported."; fi +sync + +echo "--- Power Delivery and charger telemetry ---" +PD_TELEMETRY=false +for supply in "$POWER_ROOT"/*; do + [ -d "$supply" ] || continue + supply_type=$(read_field "${supply}/type") + case "${supply_type,,}" in usb*|mains*) ;; + *) continue ;; + esac + PD_TELEMETRY=true + echo " Supply: $(basename "$supply") type=${supply_type}" + for field in online usb_type voltage_now current_now current_max power_now input_current_limit; do + value=$(read_field "${supply}/${field}") + echo " ${field}=${value}" + done + voltage=$(read_field "${supply}/voltage_now") + current=$(read_field "${supply}/current_now") + case "$voltage:$current" in + *[!0-9:-]*|not-exported:*|*:not-exported) ;; + *) + watts=$(awk -v v="$voltage" -v c="$current" 'BEGIN {printf "%.2f", (v*c)/1000000000000}') + echo " calculated_instantaneous_power=${watts} W (telemetry, not dock certification)" + ;; + esac +done +if [ "$PD_TELEMETRY" = false ]; then + echo " INACCESSIBLE: Charger voltage/current/PD telemetry is not exported. DeckDoc will not infer it." +fi +sync + +echo "--- External display connectors ---" +EXTERNAL_CONNECTORS=0 +for connector in "$DRM_ROOT"/card*-*; do + [ -d "$connector" ] || continue + name=$(basename "$connector") + case "$name" in *-eDP-*|*-DSI-*) continue ;; esac + EXTERNAL_CONNECTORS=$((EXTERNAL_CONNECTORS + 1)) + status=$(read_field "${connector}/status") + edid_bytes=$(wc -c < "${connector}/edid" 2>/dev/null || echo inaccessible) + echo " ${name}: status=${status} edid_bytes=${edid_bytes} link_status=$(read_field "${connector}/link_status")" + if [ "$status" = "connected" ] && [ -r "${connector}/modes" ]; then + echo " modes=$(head -5 "${connector}/modes" 2>/dev/null | tr '\n' ' ')" + fi +done +if [ "$EXTERNAL_CONNECTORS" -eq 0 ]; then echo " No external DRM connector is exported."; fi +sync + +echo "--- Dock Ethernet candidates ---" +DOCK_NET=0 +for iface_path in "$SYS_ROOT"/class/net/*; do + [ -d "$iface_path" ] || continue + iface=$(basename "$iface_path") + [ "$iface" = "lo" ] && continue + driver=$(basename "$(readlink -f "${iface_path}/device/driver" 2>/dev/null || echo unknown)") + device_path=$(readlink -f "${iface_path}/device" 2>/dev/null || true) + if [[ "$device_path" == *"/usb"* ]] || [[ "$driver" =~ ^(r8152|cdc_|asix|ax88179) ]]; then + DOCK_NET=$((DOCK_NET + 1)) + echo " ${iface}: driver=${driver} state=$(read_field "${iface_path}/operstate") carrier=$(read_field "${iface_path}/carrier") speed=$(read_field "${iface_path}/speed")" + fi +done +if [ "$DOCK_NET" -eq 0 ]; then echo " No USB/dock Ethernet interface identified."; fi +sync + +echo "--- Dock/USB-C errors (current boot) ---" +KERNEL_LOG="" +if command -v journalctl >/dev/null 2>&1; then + KERNEL_LOG=$(journalctl -k -b 0 -o short-monotonic --no-pager 2>/dev/null || true) +fi +if [ -z "$KERNEL_LOG" ] && command -v dmesg >/dev/null 2>&1; then KERNEL_LOG=$(dmesg 2>/dev/null || true); fi +if [ -n "$KERNEL_LOG" ]; then + HARD_ERRORS=$(printf '%s\n' "$KERNEL_LOG" | grep -iE 'xhci.*(host.*(halt|reset)|error|timeout)|usb.*over-current|ucsi.*(error|fail|timeout)|type-?c.*(error|fail|timeout)|displayport.*(error|fail)|drm.*(link.*fail|edid.*fail)' | tail -20 || true) + TOPOLOGY_EVENTS=$(printf '%s\n' "$KERNEL_LOG" | grep -iE 'usb [0-9-]+: (reset|disconnect)|xhci.*reset' | tail -20 || true) + if [ -n "$HARD_ERRORS" ]; then + echo "$HARD_ERRORS" + echo " HIGH: USB-C/host/PD/display-link errors occurred. Correlate exact time and direct-vs-dock behavior." + else + echo " No selected USB-C host, PD, or display-link errors in the current boot." + fi + if [ -n "$TOPOLOGY_EVENTS" ]; then + echo " USB reset/disconnect observations (a deliberate unplug can be normal):" + echo "$TOPOLOGY_EVENTS" + if [ -n "$HARD_ERRORS" ]; then + echo " DOCK_SIGNATURE: TOPOLOGY_CHANGE_WITH_DOCK_PATH_ERROR" + fi + fi +else + echo " INACCESSIBLE: Current-boot kernel log unavailable." +fi +sync + +echo " INTERPRETATION: Software can observe negotiation and failures but cannot certify dock rail quality. Compare one docked report with a direct known-good charger/display/network path." diff --git a/modules/dxvk_page_fault.sh b/modules/dxvk_page_fault.sh index 712f2b8..16d13a8 100755 --- a/modules/dxvk_page_fault.sh +++ b/modules/dxvk_page_fault.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -uo pipefail -echo "[MODULE: DXVK/VKD3D GPU Page Fault Analysis]" +echo "[MODULE: GPU VM Page Fault Analysis (DXVK/VKD3D correlation)]" sync echo "--- GPU page fault analysis (current boot) ---" @@ -18,18 +18,18 @@ if command -v journalctl >/dev/null 2>&1; then CPF_FAULTS=$(echo "$PAGE_FAULTS" | grep -c 'CPF (0x5)' || true) CPD_FAULTS=$(echo "$PAGE_FAULTS" | grep -c 'CPD (0x6)' || true) - [ "$CB_FAULTS" -gt 0 ] && echo " Color Buffer (CB) faults: ${CB_FAULTS} — likely game/DXVK memory access issue" - [ "$DB_FAULTS" -gt 0 ] && echo " Depth Buffer (DB) faults: ${DB_FAULTS} — likely game/DXVK depth-stencil issue" - [ "$CPF_FAULTS" -gt 0 ] && echo " Command Processor (CPF) faults: ${CPF_FAULTS} — CRITICAL: may indicate driver or hardware fault" - [ "$CPD_FAULTS" -gt 0 ] && echo " Command Processor Data (CPD) faults: ${CPD_FAULTS} — CRITICAL: may indicate driver or hardware fault" + [ "$CB_FAULTS" -gt 0 ] && echo " Color Buffer (CB) faults: ${CB_FAULTS} — GPU client classification; correlate process/API and reset state" + [ "$DB_FAULTS" -gt 0 ] && echo " Depth Buffer (DB) faults: ${DB_FAULTS} — GPU client classification; correlate process/API and reset state" + [ "$CPF_FAULTS" -gt 0 ] && echo " Command Processor (CPF) faults: ${CPF_FAULTS} — CRITICAL: command-path fault; driver, workload, and hardware remain candidates" + [ "$CPD_FAULTS" -gt 0 ] && echo " Command Processor Data (CPD) faults: ${CPD_FAULTS} — CRITICAL: command-path fault; driver, workload, and hardware remain candidates" echo "--- Page fault severity ---" MAPPING_ERRORS=$(echo "$PAGE_FAULTS" | grep -c 'MAPPING_ERROR' || true) WALKER_ERRORS=$(echo "$PAGE_FAULTS" | grep -c 'WALKER_ERROR' || true) PERM_FAULTS=$(echo "$PAGE_FAULTS" | grep -c 'PERMISSION_FAULTS' || true) - [ "$MAPPING_ERRORS" -gt 0 ] && echo " MAPPING_ERROR detected — VRAM mapping lost. GPU state compromised." - [ "$WALKER_ERRORS" -gt 0 ] && echo " WALKER_ERROR detected — page table walk failed. Likely hardware or driver bug." + [ "$MAPPING_ERRORS" -gt 0 ] && echo " MAPPING_ERROR detected — the VM mapping failed; root cause is not identified by this bit alone." + [ "$WALKER_ERRORS" -gt 0 ] && echo " WALKER_ERROR detected — page-table walk failed; correlate driver, workload, memory pressure, and recurrence." [ "$PERM_FAULTS" -gt 0 ] && echo " PERMISSION_FAULTS detected — attempted access to protected GPU memory region." else echo " No GPU page faults in current boot." @@ -39,13 +39,20 @@ sync echo "--- Process attribution ---" if command -v journalctl >/dev/null 2>&1; then - journalctl -k -b 0 2>/dev/null | grep -E 'page fault|amdgpu_job_timedout' | grep -oP '(?<=process )\S+(?= pid|$)' | sort | uniq -c | sort -rn | head -5 || echo " No process attribution available for GPU faults." + ATTRIBUTION=$(journalctl -k -b 0 2>/dev/null | grep -E 'page fault|amdgpu_job_timedout' | grep -oP '(?<=process )\S+(?= pid|$)' | sort | uniq -c | sort -rn | head -5 || true) + if [ -n "$ATTRIBUTION" ]; then echo "$ATTRIBUTION"; else echo " No process attribution available for GPU faults."; fi fi sync echo "--- Ring timeout correlation ---" if command -v journalctl >/dev/null 2>&1; then - journalctl -k -b 0 2>/dev/null | grep -iE 'ring gfx.*timeout|ring sdma0.*timeout|ring comp.*timeout' | head -10 || echo " No ring timeouts. Page faults are standalone (likely DXVK/VKD3D bug, not driver crash)." + RING_TIMEOUTS=$(journalctl -k -b 0 2>/dev/null | grep -iE 'ring gfx.*timeout|ring sdma0.*timeout|ring comp.*timeout' | head -10 || true) + if [ -n "$RING_TIMEOUTS" ]; then + echo "$RING_TIMEOUTS" + echo " HIGH: GPU page-fault and ring-timeout evidence must be correlated by timestamp." + else + echo " No ring timeouts. A standalone page fault still requires process/API/driver correlation." + fi fi sync @@ -57,6 +64,8 @@ if command -v journalctl >/dev/null 2>&1; then echo "$RESETS" if echo "$RESETS" | grep -q 'failed'; then echo " CRITICAL: GPU reset failed. System-level instability or hardware fault." + elif echo "$RESETS" | grep -qi 'succeeded'; then + echo " GPU reset succeeded; affected clients may still have terminated." fi else echo " No GPU resets in current boot." diff --git a/modules/gamescope_session.sh b/modules/gamescope_session.sh index 371d794..5a5dca8 100755 --- a/modules/gamescope_session.sh +++ b/modules/gamescope_session.sh @@ -49,21 +49,33 @@ if command -v journalctl >/dev/null 2>&1; then if [ -z "$SESSION_LOG" ]; then SESSION_LOG=$(run_user journalctl --user -b 0 -u gamescope-session.service --priority=err -n 30 2>/dev/null) fi - if [ -n "$SESSION_LOG" ]; then - echo "$SESSION_LOG" | grep -iE 'error|warn|fail|core dump|terminate|abort' | head -10 || true + SESSION_MATCHES=$(printf '%s\n' "$SESSION_LOG" | grep -iE 'error|warn|fail|core dump|terminate|abort' | head -10 || true) + if [ -n "$SESSION_MATCHES" ]; then + echo "$SESSION_MATCHES" else echo " No gamescope session errors in current boot." fi sync echo "--- Session restart count ---" - SESSION_STARTS=$(journalctl -b 0 -u gamescope-session 2>/dev/null | grep -c 'Started Gamescope' || true) - if [ -z "$SESSION_STARTS" ] || [ "$SESSION_STARTS" -eq 0 ]; then - SESSION_STARTS=$(run_user journalctl --user -b 0 -u gamescope-session.service 2>/dev/null | grep -c 'Started Gamescope Session' || true) + ACTIVE_STATE=$(run_user systemctl --user show gamescope-session.service --property=ActiveState --value 2>/dev/null || true) + SYSTEMD_RESTARTS=$(run_user systemctl --user show gamescope-session.service --property=NRestarts --value 2>/dev/null || true) + SESSION_SOURCE="journal" + case "$SYSTEMD_RESTARTS" in ''|*[!0-9]*) SYSTEMD_RESTARTS="" ;; esac + if [ -n "$SYSTEMD_RESTARTS" ] && [ "$ACTIVE_STATE" = "active" ]; then + SESSION_STARTS=$((SYSTEMD_RESTARTS + 1)) + SESSION_SOURCE="systemd NRestarts + active invocation" + else + SESSION_STARTS=$(journalctl -b 0 -u gamescope-session 2>/dev/null | grep -c 'Started Gamescope' || true) + if [ -z "$SESSION_STARTS" ] || [ "$SESSION_STARTS" -eq 0 ]; then + SESSION_STARTS=$(run_user journalctl --user -b 0 -u gamescope-session.service 2>/dev/null | grep -c 'Started Gamescope Session' || true) + fi fi RESTART_COUNT="${SESSION_STARTS:-0}" - echo " Gamescope session starts: ${RESTART_COUNT} (1 = normal, >1 indicates restarts)" - if [ "${RESTART_COUNT:-0}" -gt 1 ]; then + echo " Gamescope session starts: ${RESTART_COUNT} (source: ${SESSION_SOURCE}; 1 = normal)" + if [ "${RESTART_COUNT:-0}" -gt 3 ]; then + echo " CRITICAL: Gamescope session started ${RESTART_COUNT} times in one boot. Repeated compositor instability." + elif [ "${RESTART_COUNT:-0}" -gt 1 ]; then echo " WARNING: Gamescope session restarted ${RESTART_COUNT} times. Possible compositor instability." fi fi diff --git a/modules/memory_swap.sh b/modules/memory_swap.sh index 922468c..2600de9 100755 --- a/modules/memory_swap.sh +++ b/modules/memory_swap.sh @@ -4,13 +4,15 @@ set -uo pipefail echo "[MODULE: Memory & Swap Pressure]" sync +PROC_ROOT="${DECKDOC_PROC_ROOT:-/proc}" + echo "--- Memory status ---" -if [ -f /proc/meminfo ]; then - MEM_TOTAL=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo) - MEM_AVAIL=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo) - MEM_FREE=$(awk '/^MemFree:/ {print $2}' /proc/meminfo) - SWAP_TOTAL=$(awk '/^SwapTotal:/ {print $2}' /proc/meminfo) - SWAP_FREE=$(awk '/^SwapFree:/ {print $2}' /proc/meminfo) +if [ -f "${PROC_ROOT}/meminfo" ]; then + MEM_TOTAL=$(awk '/^MemTotal:/ {print $2}' "${PROC_ROOT}/meminfo") + MEM_AVAIL=$(awk '/^MemAvailable:/ {print $2}' "${PROC_ROOT}/meminfo") + MEM_FREE=$(awk '/^MemFree:/ {print $2}' "${PROC_ROOT}/meminfo") + SWAP_TOTAL=$(awk '/^SwapTotal:/ {print $2}' "${PROC_ROOT}/meminfo") + SWAP_FREE=$(awk '/^SwapFree:/ {print $2}' "${PROC_ROOT}/meminfo") MEM_TOTAL_GB=$(awk "BEGIN {printf \"%.1f\", $MEM_TOTAL/1024/1024}") MEM_AVAIL_GB=$(awk "BEGIN {printf \"%.1f\", $MEM_AVAIL/1024/1024}") @@ -23,10 +25,10 @@ if [ -f /proc/meminfo ]; then echo " Available RAM: ${MEM_AVAIL_GB} GB" echo " Memory used: ${MEM_USED_PCT}%" - if [ "$MEM_AVAIL" -lt 1048576 ]; then - echo " CRITICAL: Available memory below 1 GB. System under severe memory pressure." - elif [ "$MEM_AVAIL" -lt 2097152 ]; then - echo " WARNING: Available memory below 2 GB." + if [ "$MEM_AVAIL" -lt 524288 ]; then + echo " CRITICAL: Available memory below 512 MB. System under severe memory pressure." + elif [ "$MEM_AVAIL" -lt 1048576 ]; then + echo " WARNING: Available memory below 1 GB." fi if [ -n "$SWAP_TOTAL" ] && [ "$SWAP_TOTAL" -gt 0 ]; then @@ -36,13 +38,13 @@ if [ -f /proc/meminfo ]; then echo " Swap total: ${SWAP_TOTAL_GB} GB" echo " Swap used: ${SWAP_PCT}%" if [ "$SWAP_PCT" -gt 50 ]; then - echo " CRITICAL: Swap usage exceeds 50%. Memory exhaustion likely." + echo " HIGH: Swap usage exceeds 50%. Check live swap I/O and OOM history." fi else echo " Swap: none configured" fi else - echo " /proc/meminfo not found." + echo " ${PROC_ROOT}/meminfo not found." fi sync @@ -59,17 +61,38 @@ fi sync echo "--- Page swap activity ---" -if [ -f /proc/vmstat ]; then - SI=$(awk '/^pswpin/ {print $2}' /proc/vmstat) - SO=$(awk '/^pswpout/ {print $2}' /proc/vmstat) +if [ -f "${PROC_ROOT}/vmstat" ]; then + SI=$(awk '/^pswpin/ {print $2}' "${PROC_ROOT}/vmstat") + SO=$(awk '/^pswpout/ {print $2}' "${PROC_ROOT}/vmstat") echo " Swap in (pswpin): ${SI:-0} pages (total since boot)" echo " Swap out (pswpout): ${SO:-0} pages (total since boot)" if [ "${SI:-0}" -gt 50000 ] || [ "${SO:-0}" -gt 50000 ]; then - echo " WARNING: High cumulative swap I/O. System frequently memory-constrained." + echo " NOTE: Cumulative swap I/O is high, but it does not establish current pressure; use the live sample and incident time." fi fi sync + +echo "--- Live vmstat sample ---" +if command -v vmstat >/dev/null 2>&1; then + VMSTAT_SAMPLE=$(vmstat 1 "${DECKDOC_VMSTAT_SAMPLES:-3}" 2>/dev/null | tail -1 || true) + if [ -n "$VMSTAT_SAMPLE" ]; then + echo " ${VMSTAT_SAMPLE}" + LIVE_SI=$(printf '%s\n' "$VMSTAT_SAMPLE" | awk '{print $7+0}') + LIVE_SO=$(printf '%s\n' "$VMSTAT_SAMPLE" | awk '{print $8+0}') + if [ "$LIVE_SI" -gt 0 ] || [ "$LIVE_SO" -gt 0 ]; then + echo " WARNING: Live swap I/O observed (si=${LIVE_SI}, so=${LIVE_SO} KiB/s)." + else + echo " No live swap I/O in this short sample." + fi + else + echo " vmstat returned no sample." + fi +else + echo " vmstat not available." +fi +sync + echo "--- Top memory consumers ---" if command -v ps >/dev/null 2>&1; then ps -eo pid,comm,%mem,rss --sort=-%mem 2>/dev/null | head -8 diff --git a/modules/mmc_sd_card.sh b/modules/mmc_sd_card.sh index b3a6413..537b022 100755 --- a/modules/mmc_sd_card.sh +++ b/modules/mmc_sd_card.sh @@ -4,6 +4,8 @@ set -uo pipefail echo "[MODULE: SD Card / mmc Storage]" sync +SYS_ROOT="${DECKDOC_SYS_ROOT:-/sys}" + echo "--- mmc device detection ---" if command -v lsblk >/dev/null 2>&1; then MMC_DEVICES=$(lsblk -dno NAME,TYPE,TRAN 2>/dev/null | grep -i mmc || true) @@ -14,10 +16,12 @@ if command -v lsblk >/dev/null 2>&1; then echo " No mmc devices detected." fi - MMC_MOUNTS=$(findmnt -lo SOURCE,TARGET,FSTYPE,SIZE 2>/dev/null | grep mmc || true) - if [ -n "$MMC_MOUNTS" ]; then - echo " Mounted mmc partitions:" - echo "$MMC_MOUNTS" + if command -v findmnt >/dev/null 2>&1; then + MMC_MOUNTS=$(findmnt -lo SOURCE,TARGET,FSTYPE,SIZE 2>/dev/null | grep mmc || true) + if [ -n "$MMC_MOUNTS" ]; then + echo " Mounted mmc partitions:" + echo "$MMC_MOUNTS" + fi fi else echo " lsblk not available." @@ -28,7 +32,7 @@ echo "--- mmc driver errors (dmesg) ---" if command -v dmesg >/dev/null 2>&1; then MMC_ERRORS=$(dmesg 2>/dev/null | grep -iE 'mmc[0-9]:|sdhci|card reader' | grep -iE 'error|fail|timeout|cannot verify|corrupt' | head -10 || true) if [ -n "$MMC_ERRORS" ]; then - echo " CRITICAL: mmc driver errors detected:" + echo " HIGH: mmc driver errors detected:" echo "$MMC_ERRORS" else echo " No mmc driver errors in dmesg." @@ -46,7 +50,7 @@ echo "--- SD card TRIM status ---" if command -v journalctl >/dev/null 2>&1; then TRIM_ERRORS=$(journalctl -b 0 2>/dev/null | grep -iE 'fstrim|trim.*mmc|mmc.*trim|safe_trim' | grep -iE 'error|fail|unsupported' | head -5 || true) if [ -n "$TRIM_ERRORS" ]; then - echo " TRIM errors on mmc/SD:" + echo " MEDIUM: TRIM errors on mmc/SD:" echo "$TRIM_ERRORS" else echo " No TRIM errors for mmc devices in current boot." @@ -55,8 +59,8 @@ fi sync echo "--- SD card health indicators ---" -if [ -d /sys/block ] && command -v ls >/dev/null 2>&1; then - for mmc in /sys/block/mmcblk*; do +if [ -d "${SYS_ROOT}/block" ] && command -v ls >/dev/null 2>&1; then + for mmc in "${SYS_ROOT}"/block/mmcblk*; do if [ -d "$mmc" ]; then DEV=$(basename "$mmc") SIZE=$(cat "${mmc}/size" 2>/dev/null || echo 0) diff --git a/modules/probe_incidents.sh b/modules/probe_incidents.sh new file mode 100755 index 0000000..27ce43d --- /dev/null +++ b/modules/probe_incidents.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -uo pipefail + +echo "[MODULE: Continuous Incident Probe]" +sync + +SESSION_USER="${DECKDOC_SESSION_USER:-${SUDO_USER:-$(id -un)}}" +SESSION_HOME=$(getent passwd "$SESSION_USER" 2>/dev/null | cut -d: -f6) +USER_STATE="${XDG_STATE_HOME:-${SESSION_HOME}/.local/state}/deckdoc-probe" +JOURNAL_LINES="${DECKDOC_PROBE_JOURNAL_LINES:-250}" +case "$JOURNAL_LINES" in ''|*[!0-9]*) JOURNAL_LINES=250 ;; esac + +if [ -n "${DECKDOC_PROBE_STATE_DIR:-}" ]; then + STATE_DIR="$DECKDOC_PROBE_STATE_DIR" +elif [ -d /var/lib/deckdoc-probe ]; then + STATE_DIR=/var/lib/deckdoc-probe +else + STATE_DIR="$USER_STATE" +fi +EVENTS_DIR="${STATE_DIR}/events" + +echo "--- Probe availability ---" +echo " State directory: ${STATE_DIR}" +if [ ! -d "$EVENTS_DIR" ]; then + echo " Probe not installed or no probe state is available." + sync + exit 0 +fi +if [ ! -r "$EVENTS_DIR" ]; then + echo " INACCESSIBLE: Probe events exist but this report cannot read them. Run the report with sudo." + sync + exit 0 +fi + +EVENT_COUNT=$(find "$EVENTS_DIR" -mindepth 1 -maxdepth 1 -type d -name '20*_*' 2>/dev/null | wc -l) +echo " Stored incidents: ${EVENT_COUNT}" +if command -v systemctl >/dev/null 2>&1; then + ACTIVE=$(systemctl is-active deckdoc-probe.service 2>/dev/null || true) + echo " System probe service: ${ACTIVE:-unknown}" +fi + +LATEST="" +if [ -L "${STATE_DIR}/latest" ]; then + LATEST=$(readlink -f "${STATE_DIR}/latest" 2>/dev/null || true) +fi +if [ -z "$LATEST" ]; then + LATEST=$(find "$EVENTS_DIR" -mindepth 1 -maxdepth 1 -type d -name '20*_*' -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -1 | cut -d' ' -f2-) +fi +case "$LATEST" in + "${EVENTS_DIR}"/20*_*) ;; + '') echo " No captured incidents."; sync; exit 0 ;; + *) echo " INACCESSIBLE: Latest incident path failed validation."; sync; exit 0 ;; +esac +if [ ! -d "$LATEST" ]; then + echo " Latest incident directory is missing." + sync + exit 0 +fi + +echo "--- Latest incident metadata ---" +if [ -r "${LATEST}/metadata.txt" ]; then cat "${LATEST}/metadata.txt"; else echo " Metadata inaccessible."; fi +echo "--- Triggering record ---" +if [ -r "${LATEST}/trigger.log" ]; then cat "${LATEST}/trigger.log"; else echo " Trigger inaccessible."; fi +echo "--- Volatile state captured at incident ---" +if [ -r "${LATEST}/state.log" ]; then cat "${LATEST}/state.log"; else echo " State snapshot inaccessible."; fi +echo "--- Incident journal tail (up to ${JOURNAL_LINES} lines) ---" +if [ -r "${LATEST}/journal.log" ]; then + tail -n "$JOURNAL_LINES" "${LATEST}/journal.log" +else + echo " Journal window inaccessible." +fi +echo " PRIVACY: Probe captures are unredacted. Review network, username, path, serial, and command-line data before sharing." +sync diff --git a/modules/steam_client_logs.sh b/modules/steam_client_logs.sh index 507ca22..9a1e6d1 100755 --- a/modules/steam_client_logs.sh +++ b/modules/steam_client_logs.sh @@ -33,6 +33,27 @@ else fi sync + +echo "--- steamwebhelper crash frequency ---" +if command -v coredumpctl >/dev/null 2>&1; then + BOOT_START=$(uptime -s 2>/dev/null || true) + STEAMWEBHELPER_24H=$(coredumpctl list --no-legend --no-pager --since "24 hours ago" 2>/dev/null | grep -ic '/steamwebhelper' || true) + STEAMWEBHELPER_BOOT=0 + if [ -n "$BOOT_START" ]; then + STEAMWEBHELPER_BOOT=$(coredumpctl list --no-legend --no-pager --since "$BOOT_START" 2>/dev/null | grep -ic '/steamwebhelper' || true) + fi + echo " Current-boot steamwebhelper dumps: ${STEAMWEBHELPER_BOOT}" + echo " Last-24h steamwebhelper dumps: ${STEAMWEBHELPER_24H}" + if [ "$STEAMWEBHELPER_BOOT" -gt 5 ] || [ "$STEAMWEBHELPER_24H" -gt 10 ]; then + echo " HIGH: Repeated steamwebhelper crashes exceed the diagnostic threshold." + elif [ "$STEAMWEBHELPER_BOOT" -gt 0 ]; then + echo " NOTE: Correlate steamwebhelper dumps with visible Steam UI/session symptoms." + fi +else + echo " coredumpctl not available." +fi +sync + echo "--- Steam stdout log ---" STDOUT_LOG="${DUMP_DIR}/steam_stdout.txt" if [ -f "$STDOUT_LOG" ]; then @@ -66,7 +87,7 @@ if [ "${DECKDOC_SKIP_JOURNAL:-0}" != "1" ] && command -v journalctl >/dev/null 2 fi sync -echo "--- Proton prefix corruption check ---" +echo "--- Proton prefix inventory ---" if command -v find >/dev/null 2>&1; then # A root DeckDoc run must still inspect the active Deck user's Steam tree, # not /root, or a healthy install is falsely reported as missing. @@ -74,7 +95,7 @@ if command -v find >/dev/null 2>&1; then if [ -d "$STEAM_DIR" ]; then BROKEN_PREFIXES=$(find "$STEAM_DIR" -name 'drive_c' -prune -o -name '*.lock' -print 2>/dev/null | wc -l) echo " Proton prefixes: $(ls -d "$STEAM_DIR"/*/ 2>/dev/null | wc -l) total" - echo " Lock/stall files: ${BROKEN_PREFIXES}" + echo " Lock files: ${BROKEN_PREFIXES} (inventory only; presence does not prove a stalled or corrupt prefix)" else echo " Proton compatdata directory not found at ${STEAM_DIR}." fi diff --git a/modules/thermal_fan.sh b/modules/thermal_fan.sh index 0a05fb0..f07befa 100755 --- a/modules/thermal_fan.sh +++ b/modules/thermal_fan.sh @@ -25,6 +25,16 @@ if [ -d "$HWMON_DIR" ]; then max_raw=""; crit_raw="" [ -r "$max_file" ] && max_raw=$(cat "$max_file") [ -r "$crit_file" ] && crit_raw=$(cat "$crit_file") + case "$max_raw" in ''|*[!0-9]*) max_raw="" ;; esac + case "$crit_raw" in ''|*[!0-9]*) crit_raw="" ;; esac + if [ -n "$max_raw" ] && [ "$max_raw" -gt 200000 ]; then + echo " NOTE: Ignoring implausible exported high threshold ${max_raw} millidegrees C." + max_raw="" + fi + if [ -n "$crit_raw" ] && [ "$crit_raw" -gt 200000 ]; then + echo " NOTE: Ignoring implausible exported critical threshold ${crit_raw} millidegrees C." + crit_raw="" + fi if [ -n "$max_raw" ]; then echo " High threshold: $(awk "BEGIN {print $max_raw/1000}") C"; fi if [ -n "$crit_raw" ]; then echo " Critical threshold: $(awk "BEGIN {print $crit_raw/1000}") C"; fi # A hard-coded 90 C is not a hardware trip point. Compare to diff --git a/modules/wifi_firmware.sh b/modules/wifi_firmware.sh index 387c448..37e38a9 100755 --- a/modules/wifi_firmware.sh +++ b/modules/wifi_firmware.sh @@ -4,9 +4,17 @@ set -uo pipefail echo "[MODULE: WiFi / Wireless Firmware]" sync +KERNEL_LOG="" +if command -v journalctl >/dev/null 2>&1; then + KERNEL_LOG=$(journalctl -k -b 0 -o short-monotonic --no-pager 2>/dev/null || true) +fi +if [ -z "$KERNEL_LOG" ] && command -v dmesg >/dev/null 2>&1; then + KERNEL_LOG=$(dmesg 2>/dev/null || true) +fi + echo "--- Network interface status ---" if command -v ip >/dev/null 2>&1; then - WLAN_IFACE=$(ip link show 2>/dev/null | grep -oE 'wlan[0-9]' | head -1 || true) + WLAN_IFACE=$(ip -o link show 2>/dev/null | awk -F': ' '$2 ~ /^(wlan[0-9]+|wl[^:]+)$/ {print $2; exit}' || true) if [ -n "$WLAN_IFACE" ]; then WLAN_STATE=$(ip link show "$WLAN_IFACE" 2>/dev/null | grep -oE 'state (UP|DOWN|UNKNOWN)' || echo "state UNKNOWN") echo " Interface: ${WLAN_IFACE} ${WLAN_STATE}" @@ -28,8 +36,8 @@ fi sync echo "--- Firmware errors (current boot) ---" -if command -v dmesg >/dev/null 2>&1; then - FW_ERRORS=$(dmesg 2>/dev/null | grep -iE 'ath11k|ath12k|iwlwifi|b43|brcmfmac' | grep -iE 'fail|error|crash|firmware|crashed' | head -10 || true) +if [ -n "$KERNEL_LOG" ]; then + FW_ERRORS=$(printf '%s\n' "$KERNEL_LOG" | grep -iE '(^|[^[:alnum:]_])(ath11k(_pci)?|ath12k(_pci)?|iwlwifi|rtw_88|rtw88|b43|brcmfmac)([^[:alnum:]_]|$)' | grep -iE 'fail|error|crash|crashed|timeout' | head -10 || true) if [ -n "$FW_ERRORS" ]; then echo "$FW_ERRORS" sync @@ -41,9 +49,26 @@ if command -v dmesg >/dev/null 2>&1; then fi echo "--- Firmware version ---" - dmesg 2>/dev/null | grep -iE 'ath11k.*firmware|iwlwifi.*loaded firmware' | tail -3 || echo " Firmware version info not available." + FW_VERSION=$(printf '%s\n' "$KERNEL_LOG" | grep -iE '(ath11k|ath12k).*(fw_version|fw_build|firmware)|iwlwifi.*loaded firmware|rtw_88.*firmware|rtw88.*firmware' | tail -3 || true) + if [ -n "$FW_VERSION" ]; then echo "$FW_VERSION"; else echo " Firmware version info not available."; fi +else + echo " Kernel log unavailable. Run as root for firmware evidence." +fi +sync + +echo "--- Coupled resume-device failure check ---" +SOF_FATAL="" +if [ -n "$KERNEL_LOG" ]; then + SOF_FATAL=$(printf '%s\n' "$KERNEL_LOG" | grep -iE 'snd_sof.*ipc.*(-22|timed out)|DSP panic|Failed to restore pipeline after resume|Failed to acquire HW lock' | tail -5 || true) +fi +if [ -n "${FW_ERRORS:-}" ] && [ -n "$SOF_FATAL" ]; then + echo " RESUME_SIGNATURE: WIFI_AND_SOF_FAILURES_IN_CURRENT_BOOT" + echo " Wireless firmware and SOF audio failures both appear in this boot; compare their timestamps with the same resume window." +elif [ -z "${WLAN_IFACE:-}" ] && [ -n "$SOF_FATAL" ]; then + echo " RESUME_SIGNATURE: WIFI_MISSING_WITH_SOF_FAILURE_IN_CURRENT_BOOT" + echo " No wireless interface is visible and a SOF failure is retained; timestamp correlation is still required." else - echo " dmesg not available." + echo " No coupled Wi-Fi/SOF failure signature in the current boot." fi sync diff --git a/privileged/deckdoc-authorized b/privileged/deckdoc-authorized new file mode 100755 index 0000000..4c0a345 --- /dev/null +++ b/privileged/deckdoc-authorized @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This file is installed root-owned. Keep every accepted operation argument-free +# and read-only; sudoers grants access to the exact command lines below. +readonly INSTALL_ROOT="/var/lib/deckdoc-authorized" +readonly APP_DIR="${INSTALL_ROOT}/app" +readonly PROBE_BIN="/var/lib/deckdoc-probe/bin/deckdoc-probe.sh" + +export PATH="/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin" +unset BASH_ENV ENV CDPATH GLOBIGNORE DECKDOC_DIR DECKDOC_LOG_DIR +umask 077 + +usage() { + echo "Usage: deckdoc-authorized {report|report-display-black|probe-capture|probe-status|version}" >&2 + exit 2 +} + +verify_snapshot() { + if [ ! -r "${INSTALL_ROOT}/MANIFEST.sha256" ]; then + echo "DeckDoc authorized snapshot has no integrity manifest; reinstall authorization." >&2 + exit 1 + fi + if ! (cd "$INSTALL_ROOT" && sha256sum -c --status MANIFEST.sha256); then + echo "DeckDoc authorized snapshot failed its integrity check; reinstall authorization." >&2 + exit 1 + fi +} + +emit_report() { + local symptom_arg="${1:-}" + local run_dir + run_dir=$(mktemp -d "${INSTALL_ROOT}/run.XXXXXXXX") + cleanup_run() { + case "$run_dir" in + "${INSTALL_ROOT}"/run.*) rm -rf -- "$run_dir" ;; + esac + } + trap cleanup_run EXIT HUP INT TERM + + if [ -n "$symptom_arg" ]; then + env -i PATH="$PATH" DECKDOC_LOG_DIR="$run_dir" \ + /usr/bin/bash "${APP_DIR}/deckdoc.sh" "$symptom_arg" >/dev/null + else + env -i PATH="$PATH" DECKDOC_LOG_DIR="$run_dir" \ + /usr/bin/bash "${APP_DIR}/deckdoc.sh" >/dev/null + fi + + local report + report=$(find "$run_dir" -maxdepth 1 -type f -name 'deckdoc_master_report_*.log' -print -quit) + if [ -z "$report" ] || [ ! -r "$report" ]; then + echo "DeckDoc did not produce a report." >&2 + exit 1 + fi + cat -- "$report" +} + +case "${1:-}" in + report) + [ "$#" -eq 1 ] || usage + verify_snapshot + emit_report + ;; + report-display-black) + [ "$#" -eq 1 ] || usage + verify_snapshot + emit_report --display-black + ;; + probe-capture) + [ "$#" -eq 1 ] || usage + if [ ! -x "$PROBE_BIN" ]; then + echo "The opt-in DeckDoc probe is not installed." >&2 + exit 1 + fi + "$PROBE_BIN" capture authorized-manual + ;; + probe-status) + [ "$#" -eq 1 ] || usage + systemctl is-active deckdoc-probe.service + ;; + version) + [ "$#" -eq 1 ] || usage + cat "${INSTALL_ROOT}/VERSION" + ;; + *) usage ;; +esac diff --git a/privileged/deckdoc-authorized-client.sh b/privileged/deckdoc-authorized-client.sh new file mode 100755 index 0000000..8547077 --- /dev/null +++ b/privileged/deckdoc-authorized-client.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly BROKER="/var/lib/deckdoc-authorized/bin/deckdoc-authorized" +readonly PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly LOG_DIR="${PROJECT_DIR}/logs" +ACTION="${1:-status}" + +authorized() { + if ! sudo -n "$BROKER" version >/dev/null 2>&1; then + echo "DeckDoc diagnostics are not authorized for this user." >&2 + echo "Approve them once with: sudo ./privileged/install-authorized.sh install" >&2 + exit 1 + fi +} + +save_report() { + local broker_action="$1" + local report_path temporary + mkdir -p "$LOG_DIR" + chmod 700 "$LOG_DIR" 2>/dev/null || true + report_path="${LOG_DIR}/deckdoc_authorized_report_$(date +%s).log" + temporary=$(mktemp "${LOG_DIR}/.authorized-report.XXXXXXXX") + trap 'rm -f -- "$temporary"' EXIT HUP INT TERM + if ! sudo -n "$BROKER" "$broker_action" > "$temporary"; then + echo "Authorized diagnostic collection failed." >&2 + exit 1 + fi + chmod 600 "$temporary" + mv -- "$temporary" "$report_path" + trap - EXIT HUP INT TERM + echo "$report_path" +} + +case "$ACTION" in + report) + [ "$#" -eq 1 ] || exit 2 + authorized + save_report report + ;; + report-display-black) + [ "$#" -eq 1 ] || exit 2 + authorized + save_report report-display-black + ;; + probe-capture) + [ "$#" -eq 1 ] || exit 2 + authorized + sudo -n "$BROKER" probe-capture + ;; + status) + [ "$#" -eq 1 ] || exit 2 + authorized + echo "Authorized snapshot: $(sudo -n "$BROKER" version)" + if sudo -n "$BROKER" probe-status >/dev/null 2>&1; then + echo "Continuous probe: active" + else + echo "Continuous probe: not active" + fi + ;; + *) + echo "Usage: ./privileged/deckdoc-authorized-client.sh {report|report-display-black|probe-capture|status}" >&2 + exit 2 + ;; +esac diff --git a/privileged/install-authorized.sh b/privileged/install-authorized.sh new file mode 100755 index 0000000..338c261 --- /dev/null +++ b/privileged/install-authorized.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ACTION="${1:-status}" +readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly INSTALL_ROOT="/var/lib/deckdoc-authorized" +readonly APP_DIR="${INSTALL_ROOT}/app" +readonly BIN_DIR="${INSTALL_ROOT}/bin" +readonly BROKER="${BIN_DIR}/deckdoc-authorized" +readonly VERSION="3.2.0" + +require_root() { + if [ "$(id -u)" -ne 0 ]; then + echo "This action needs one interactive sudo approval." >&2 + echo "Run: sudo ./privileged/install-authorized.sh ${ACTION}" >&2 + exit 1 + fi +} + +resolve_user() { + local candidate="${SUDO_USER:-}" + if [ "$candidate" = "root" ] || [ -z "$candidate" ]; then + candidate="${2:-}" + fi + case "$candidate" in + ''|*[!a-zA-Z0-9_.-]*) + echo "Could not safely identify the user to authorize." >&2 + exit 1 + ;; + esac + if ! id "$candidate" >/dev/null 2>&1; then + echo "Unknown authorization user: $candidate" >&2 + exit 1 + fi + printf '%s\n' "$candidate" +} + +sudoers_path_for() { + printf '/etc/sudoers.d/deckdoc-authorized-%s\n' "$1" +} + +install_authorization() { + require_root + local auth_user sudoers_path sudoers_tmp + auth_user=$(resolve_user "$@") + sudoers_path=$(sudoers_path_for "$auth_user") + sudoers_tmp=$(mktemp /tmp/deckdoc-sudoers.XXXXXXXX) + trap 'rm -f -- "$sudoers_tmp"' EXIT HUP INT TERM + + install -d -o root -g root -m 755 "$INSTALL_ROOT" "$APP_DIR" "$BIN_DIR" + install -o root -g root -m 755 "${SOURCE_DIR}/deckdoc.sh" "${APP_DIR}/deckdoc.sh" + install -d -o root -g root -m 755 "${APP_DIR}/modules" + find "${APP_DIR}/modules" -maxdepth 1 -type f -name '*.sh' -delete + local module + for module in "${SOURCE_DIR}"/modules/*.sh; do + install -o root -g root -m 755 "$module" "${APP_DIR}/modules/$(basename "$module")" + done + install -o root -g root -m 755 "${SOURCE_DIR}/privileged/deckdoc-authorized" "$BROKER" + printf '%s\n' "$VERSION" > "${INSTALL_ROOT}/VERSION" + chown root:root "${INSTALL_ROOT}/VERSION" + chmod 644 "${INSTALL_ROOT}/VERSION" + + ( + cd "$INSTALL_ROOT" + find app bin -type f -print0 | sort -z | xargs -0 sha256sum + sha256sum VERSION + ) > "${INSTALL_ROOT}/MANIFEST.sha256" + chown root:root "${INSTALL_ROOT}/MANIFEST.sha256" + chmod 644 "${INSTALL_ROOT}/MANIFEST.sha256" + + { + echo "# DeckDoc read-only diagnostic authorization for ${auth_user}" + echo "${auth_user} ALL=(root) NOPASSWD: NOSETENV: ${BROKER} report" + echo "${auth_user} ALL=(root) NOPASSWD: NOSETENV: ${BROKER} report-display-black" + echo "${auth_user} ALL=(root) NOPASSWD: NOSETENV: ${BROKER} probe-capture" + echo "${auth_user} ALL=(root) NOPASSWD: NOSETENV: ${BROKER} probe-status" + echo "${auth_user} ALL=(root) NOPASSWD: NOSETENV: ${BROKER} version" + } > "$sudoers_tmp" + chmod 440 "$sudoers_tmp" + visudo -cf "$sudoers_tmp" >/dev/null + install -o root -g root -m 440 "$sudoers_tmp" "$sudoers_path" + + echo "DeckDoc read-only diagnostics authorized for ${auth_user}." + echo "Installed root-owned snapshot: ${VERSION}" + echo "No password, root shell, arbitrary command, remediation, or arbitrary output path was authorized." +} + +show_status() { + local auth_user sudoers_path + auth_user="${SUDO_USER:-${USER:-}}" + case "$auth_user" in ''|*[!a-zA-Z0-9_.-]*) auth_user="" ;; esac + if [ -r "${INSTALL_ROOT}/VERSION" ]; then + echo "Installed snapshot: $(cat "${INSTALL_ROOT}/VERSION")" + else + echo "Installed snapshot: none" + fi + if [ -n "$auth_user" ]; then + sudoers_path=$(sudoers_path_for "$auth_user") + if [ -r "$sudoers_path" ]; then + echo "Authorization for ${auth_user}: installed" + else + echo "Authorization for ${auth_user}: not installed" + fi + fi +} + +uninstall_authorization() { + require_root + local auth_user sudoers_path + auth_user=$(resolve_user "$@") + sudoers_path=$(sudoers_path_for "$auth_user") + case "$sudoers_path" in + /etc/sudoers.d/deckdoc-authorized-*) rm -f -- "$sudoers_path" ;; + *) exit 1 ;; + esac + echo "DeckDoc passwordless diagnostic authorization removed for ${auth_user}." + echo "The root-owned snapshot remains until explicitly purged." +} + +purge_snapshot() { + require_root + if find /etc/sudoers.d -maxdepth 1 -type f -name 'deckdoc-authorized-*' -print -quit 2>/dev/null | grep -q .; then + echo "Remove every DeckDoc authorization before purging the snapshot." >&2 + exit 1 + fi + case "$INSTALL_ROOT" in + /var/lib/deckdoc-authorized) rm -rf -- "$INSTALL_ROOT" ;; + *) exit 1 ;; + esac + echo "DeckDoc authorized snapshot removed." +} + +case "$ACTION" in + install) install_authorization "$@" ;; + status) show_status ;; + uninstall) uninstall_authorization "$@" ;; + purge) purge_snapshot ;; + *) + echo "Usage: sudo ./privileged/install-authorized.sh {install|status|uninstall|purge}" >&2 + exit 2 + ;; +esac diff --git a/probe/deckdoc-probe.service b/probe/deckdoc-probe.service new file mode 100644 index 0000000..479394b --- /dev/null +++ b/probe/deckdoc-probe.service @@ -0,0 +1,29 @@ +[Unit] +Description=DeckDoc lightweight incident probe +Documentation=https://github.com/deucebucket/deckdoc/wiki/Continuous-Incident-Probe +After=systemd-journald.service + +[Service] +Type=simple +ExecStart=/var/lib/deckdoc-probe/bin/deckdoc-probe.sh watch +EnvironmentFile=-/etc/deckdoc-probe.conf +Restart=on-failure +RestartSec=5s +Nice=10 +IOSchedulingClass=idle +CPUWeight=10 +IOWeight=10 +MemoryMax=128M +UMask=0077 +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=read-only +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +ReadWritePaths=/var/lib/deckdoc-probe +RestrictAddressFamilies=AF_UNIX AF_NETLINK +LockPersonality=yes + +[Install] +WantedBy=multi-user.target diff --git a/probe/deckdoc-probe.sh b/probe/deckdoc-probe.sh new file mode 100755 index 0000000..99c7001 --- /dev/null +++ b/probe/deckdoc-probe.sh @@ -0,0 +1,279 @@ +#!/usr/bin/env bash +set -uo pipefail + +umask 077 + +if [ "$(id -u)" -eq 0 ]; then + DEFAULT_STATE_DIR="/var/lib/deckdoc-probe" +else + DEFAULT_STATE_DIR="${XDG_STATE_HOME:-${HOME}/.local/state}/deckdoc-probe" +fi + +STATE_DIR="${DECKDOC_PROBE_STATE_DIR:-$DEFAULT_STATE_DIR}" +EVENTS_DIR="${STATE_DIR}/events" +COOLDOWN_SECONDS="${DECKDOC_PROBE_COOLDOWN_SECONDS:-60}" +PRE_SECONDS="${DECKDOC_PROBE_PRE_SECONDS:-120}" +POST_SECONDS="${DECKDOC_PROBE_POST_SECONDS:-5}" +MAX_EVENTS="${DECKDOC_PROBE_MAX_EVENTS:-25}" +MAX_EVENT_KIB="${DECKDOC_PROBE_MAX_EVENT_KIB:-2048}" +SESSION_USER="${DECKDOC_SESSION_USER:-${SUDO_USER:-}}" +CATEGORY="" + +valid_uint() { + case "${1:-}" in ''|*[!0-9]*) return 1 ;; *) return 0 ;; esac +} + +for value in "$COOLDOWN_SECONDS" "$PRE_SECONDS" "$POST_SECONDS" "$MAX_EVENTS" "$MAX_EVENT_KIB"; do + if ! valid_uint "$value"; then + echo "Invalid non-negative probe limit: ${value}" >&2 + exit 2 + fi +done +if [ "$MAX_EVENTS" -lt 1 ] || [ "$MAX_EVENT_KIB" -lt 64 ]; then + echo "DECKDOC_PROBE_MAX_EVENTS must be at least 1 and MAX_EVENT_KIB at least 64." >&2 + exit 2 +fi + +ensure_state() { + mkdir -p "$EVENTS_DIR" "${STATE_DIR}/cooldowns" + chmod 700 "$STATE_DIR" "$EVENTS_DIR" "${STATE_DIR}/cooldowns" 2>/dev/null || true +} + +# Sets CATEGORY without spawning grep/awk for every journal line. The watcher is +# otherwise blocked in journalctl and consumes no polling CPU while the system is quiet. +classify_line() { + local line="${1,,}" + CATEGORY="" + if [[ "$line" =~ amdgpu.*(job.*timed|ring.*timeout|page[[:space:]]fault|gpu[[:space:]]reset|vram[[:space:]]is[[:space:]]lost) ]]; then + CATEGORY="gpu" + elif [[ "$line" =~ (drm|amdgpu).*(flip_done.*timed|link.*fail|atomic.*fail|edid.*fail) ]]; then + CATEGORY="display" + elif [[ "$line" =~ (snd_sof|sof-audio|dsp).*(panic|ipc.*-22|ipc.*timed|restore.*fail|hw[[:space:]]lock.*fail) ]]; then + CATEGORY="audio" + elif [[ "$line" =~ (ath11k|ath12k|iwlwifi|rtw_?88|b43|brcmfmac).*(firmware.*crash|fail|error|timeout) ]]; then + CATEGORY="wireless" + elif [[ "$line" =~ (nvme|mmc|sdhci|ext4-fs|btrfs).*(i/o[[:space:]]error|critical[[:space:]]warning|reset.*fail|timeout|corrupt|read-only|error) ]]; then + CATEGORY="storage" + elif [[ "$line" =~ (oom-killer|out[[:space:]]of[[:space:]]memory|page[[:space:]]allocation[[:space:]]failure) ]]; then + CATEGORY="memory" + elif [[ "$line" =~ (critical[[:space:]]temperature|thermal.*trip|fan.*(fail|error|0[[:space:]]rpm)) ]]; then + CATEGORY="thermal" + elif [[ "$line" =~ (gamescope|steamwebhelper|mangoapp).*(segfault|sigsegv|sigabrt|core[[:space:]]dump|aborted|failed[[:space:]]with[[:space:]]result) ]]; then + CATEGORY="session" + elif [[ "$line" =~ (pm:.*resume.*fail|pci_pm_resume.*fail|failed[[:space:]]to[[:space:]]resume) ]]; then + CATEGORY="resume" + fi +} + +run_session_user() { + if [ -z "$SESSION_USER" ] || ! id "$SESSION_USER" >/dev/null 2>&1; then + return 1 + fi + local session_uid + session_uid=$(id -u "$SESSION_USER" 2>/dev/null) || return 1 + if [ "$(id -un)" = "$SESSION_USER" ]; then + XDG_RUNTIME_DIR="/run/user/${session_uid}" "$@" + elif command -v runuser >/dev/null 2>&1; then + runuser -u "$SESSION_USER" -- env XDG_RUNTIME_DIR="/run/user/${session_uid}" "$@" + else + return 1 + fi +} + +capture_state() { + local output="$1" + { + echo "[DeckDoc probe volatile state]" + echo "captured_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null || echo inaccessible)" + echo "kernel=$(uname -r 2>/dev/null || echo inaccessible)" + echo "uptime_seconds=$(cut -d' ' -f1 /proc/uptime 2>/dev/null || echo inaccessible)" + if [ -r /etc/os-release ]; then + grep -E '^(NAME|VERSION_ID|BUILD_ID|VARIANT_ID)=' /etc/os-release || true + fi + + echo "--- memory and pressure ---" + grep -E '^(MemTotal|MemAvailable|SwapTotal|SwapFree):' /proc/meminfo 2>/dev/null || true + for pressure in /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io; do + if [ -r "$pressure" ]; then echo "${pressure}:"; cat "$pressure"; fi + done + + echo "--- thermal, fan, and power ---" + for input in /sys/class/hwmon/hwmon*/temp*_input /sys/class/hwmon/hwmon*/fan*_input; do + [ -r "$input" ] || continue + echo "${input}=$(cat "$input" 2>/dev/null || echo inaccessible)" + done + for supply in /sys/class/power_supply/*; do + [ -d "$supply" ] || continue + echo "supply=${supply}" + for field in status online capacity voltage_now current_now power_now charge_control_limit charge_control_end_threshold; do + [ -r "${supply}/${field}" ] && echo " ${field}=$(cat "${supply}/${field}" 2>/dev/null || echo inaccessible)" + done + done + + echo "--- DRM display state ---" + for connector in /sys/class/drm/card*-*; do + [ -d "$connector" ] || continue + echo "connector=$(basename "$connector") status=$(cat "${connector}/status" 2>/dev/null || echo inaccessible) edid_bytes=$(wc -c < "${connector}/edid" 2>/dev/null || echo inaccessible)" + done + for backlight in /sys/class/backlight/*; do + [ -d "$backlight" ] || continue + echo "backlight=$(basename "$backlight") actual=$(cat "${backlight}/actual_brightness" 2>/dev/null || echo inaccessible) max=$(cat "${backlight}/max_brightness" 2>/dev/null || echo inaccessible)" + done + for drm_state in /sys/kernel/debug/dri/*/state; do + [ -r "$drm_state" ] || continue + echo "drm_state=${drm_state}" + sed -n '1,500p' "$drm_state" 2>/dev/null || true + done + + echo "--- device presence ---" + for iface in /sys/class/net/*; do + [ -d "$iface" ] || continue + echo "net=$(basename "$iface") state=$(cat "${iface}/operstate" 2>/dev/null || echo inaccessible)" + done + command -v lsusb >/dev/null 2>&1 && lsusb -t 2>/dev/null || true + command -v lsblk >/dev/null 2>&1 && lsblk -o NAME,TYPE,TRAN,SIZE,RO,FSTYPE,MOUNTPOINTS 2>/dev/null || true + + echo "--- active session services ---" + run_session_user systemctl --user show gamescope-session.service gamescope-mangoapp.service \ + --property=Id,ActiveState,SubState,NRestarts 2>/dev/null || echo "session service state inaccessible" + + echo "--- recent core metadata ---" + if command -v coredumpctl >/dev/null 2>&1; then + coredumpctl list --no-legend --no-pager --since "${PRE_SECONDS} seconds ago" 2>/dev/null | tail -30 || true + fi + } > "$output" 2>&1 +} + +prune_events() { + local -a records + local excess victim record + mapfile -t records < <(find "$EVENTS_DIR" -mindepth 1 -maxdepth 1 -type d -name '20*_*' -printf '%T@ %p\n' 2>/dev/null | sort -n) + excess=$((${#records[@]} - MAX_EVENTS)) + while [ "$excess" -gt 0 ]; do + record="${records[0]}" + records=("${records[@]:1}") + victim="${record#* }" + case "$victim" in + "${EVENTS_DIR}"/20*_*) rm -rf -- "$victim" ;; + *) echo "Refusing to prune unexpected path: ${victim}" >&2; return 1 ;; + esac + excess=$((excess - 1)) + done +} + +capture_event() { + local category="${1:-manual}" trigger="${2:-manual capture}" event_epoch="${3:-$(date +%s)}" + local event_id temp_dir final_dir start_epoch end_epoch journal_limit + case "$category" in *[!a-z0-9_-]*|'') category="manual" ;; esac + ensure_state + event_id="$(date -u -d "@${event_epoch}" +%Y%m%dT%H%M%SZ 2>/dev/null || date -u +%Y%m%dT%H%M%SZ)_${category}_$$_${RANDOM}" + temp_dir="${EVENTS_DIR}/.${event_id}.tmp" + final_dir="${EVENTS_DIR}/${event_id}" + mkdir -m 700 "$temp_dir" || return 1 + + { + echo "event_id=${event_id}" + echo "category=${category}" + echo "event_epoch=${event_epoch}" + echo "event_utc=$(date -u -d "@${event_epoch}" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null || echo inaccessible)" + echo "window_seconds_before=${PRE_SECONDS}" + echo "window_seconds_after=${POST_SECONDS}" + echo "capture_scope=local-private-unredacted" + } > "${temp_dir}/metadata.txt" + printf '%s\n' "$trigger" > "${temp_dir}/trigger.log" + capture_state "${temp_dir}/state.log" + + if [ "$POST_SECONDS" -gt 0 ]; then sleep "$POST_SECONDS"; fi + start_epoch=$((event_epoch - PRE_SECONDS)) + end_epoch=$((event_epoch + POST_SECONDS)) + journal_limit=$((MAX_EVENT_KIB * 1024)) + if command -v journalctl >/dev/null 2>&1; then + journalctl --since "@${start_epoch}" --until "@${end_epoch}" -o short-iso-precise --no-pager 2>/dev/null \ + | tail -c "$journal_limit" > "${temp_dir}/journal.log" || true + else + echo "journalctl unavailable" > "${temp_dir}/journal.log" + fi + + chmod 600 "${temp_dir}"/* 2>/dev/null || true + mv "$temp_dir" "$final_dir" + ln -sfn "events/${event_id}" "${STATE_DIR}/latest" + printf '%s %s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$category" "$event_id" >> "${STATE_DIR}/events.index" + chmod 600 "${STATE_DIR}/events.index" 2>/dev/null || true + prune_events + echo "$final_dir" +} + +watch_journal() { + ensure_state + if ! command -v journalctl >/dev/null 2>&1; then + echo "journalctl is required for watch mode." >&2 + exit 1 + fi + exec 9>"${STATE_DIR}/probe.lock" + if ! flock -n 9; then + echo "Another DeckDoc probe is already watching ${STATE_DIR}." >&2 + exit 1 + fi + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) probe_started boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null || echo inaccessible)" >> "${STATE_DIR}/probe.log" + chmod 600 "${STATE_DIR}/probe.log" 2>/dev/null || true + + journalctl --follow --since now -o short-iso-precise --no-pager 2>/dev/null | while IFS= read -r line; do + classify_line "$line" + [ -n "$CATEGORY" ] || continue + now=$(date +%s) + last=0 + cooldown_file="${STATE_DIR}/cooldowns/${CATEGORY}" + if [ -r "$cooldown_file" ]; then read -r last < "$cooldown_file" || last=0; fi + valid_uint "$last" || last=0 + if [ $((now - last)) -lt "$COOLDOWN_SECONDS" ]; then continue; fi + printf '%s\n' "$now" > "$cooldown_file" + capture_event "$CATEGORY" "$line" "$now" >> "${STATE_DIR}/probe.log" 2>&1 || true + done +} + +show_status() { + if [ ! -d "$EVENTS_DIR" ]; then + echo "Probe state not found at ${STATE_DIR}." + return 1 + fi + count=$(find "$EVENTS_DIR" -mindepth 1 -maxdepth 1 -type d -name '20*_*' 2>/dev/null | wc -l) + echo "State directory: ${STATE_DIR}" + echo "Stored incidents: ${count}/${MAX_EVENTS}" + if [ -L "${STATE_DIR}/latest" ]; then + echo "Latest incident: $(readlink -f "${STATE_DIR}/latest" 2>/dev/null || readlink "${STATE_DIR}/latest")" + else + echo "Latest incident: none" + fi +} + +usage() { + cat <<'EOF' +Usage: deckdoc-probe.sh COMMAND + +Commands: + watch Follow journald and capture only on matched high-value errors + capture [reason] Create a manual incident snapshot immediately + classify LINE Print the category DeckDoc would assign to one log line + status Show private state location and retained incident count + latest Print the latest incident directory + +The probe is read-only toward system state. Captures are private but unredacted; review before sharing. +EOF +} + +case "${1:-}" in + watch) watch_journal ;; + capture) capture_event manual "${2:-manual capture}" "$(date +%s)" ;; + classify) + classify_line "${2:-}" + if [ -n "$CATEGORY" ]; then echo "$CATEGORY"; else echo "unmatched"; fi + ;; + status) show_status ;; + latest) + if [ -L "${STATE_DIR}/latest" ]; then readlink -f "${STATE_DIR}/latest"; else exit 1; fi + ;; + -h|--help|help|'') usage ;; + *) usage >&2; exit 2 ;; +esac diff --git a/probe/install-probe.sh b/probe/install-probe.sh new file mode 100755 index 0000000..d53be94 --- /dev/null +++ b/probe/install-probe.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +ACTION="${1:-status}" +SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STATE_DIR="/var/lib/deckdoc-probe" +BIN_DIR="${STATE_DIR}/bin" +UNIT_PATH="/etc/systemd/system/deckdoc-probe.service" +CONFIG_PATH="/etc/deckdoc-probe.conf" + +require_root() { + if [ "$(id -u)" -ne 0 ]; then + echo "Run this action with sudo." >&2 + exit 1 + fi +} + +install_probe() { + require_root + local session_user="${SUDO_USER:-}" + if [ "$session_user" = "root" ] || ! id "$session_user" >/dev/null 2>&1; then + session_user=$(loginctl list-users --no-legend 2>/dev/null | awk '$2 != "root" {print $2; exit}') + fi + case "$session_user" in ''|*[!a-zA-Z0-9_.-]*) session_user="" ;; esac + + install -d -o root -g root -m 700 "$STATE_DIR" "$BIN_DIR" + install -o root -g root -m 755 "${SOURCE_DIR}/deckdoc-probe.sh" "${BIN_DIR}/deckdoc-probe.sh" + install -o root -g root -m 644 "${SOURCE_DIR}/deckdoc-probe.service" "$UNIT_PATH" + { + echo "DECKDOC_PROBE_STATE_DIR=${STATE_DIR}" + echo "DECKDOC_PROBE_COOLDOWN_SECONDS=60" + echo "DECKDOC_PROBE_PRE_SECONDS=120" + echo "DECKDOC_PROBE_POST_SECONDS=5" + echo "DECKDOC_PROBE_MAX_EVENTS=25" + echo "DECKDOC_PROBE_MAX_EVENT_KIB=2048" + if [ -n "$session_user" ]; then echo "DECKDOC_SESSION_USER=${session_user}"; fi + } > "$CONFIG_PATH" + chown root:root "$CONFIG_PATH" + chmod 600 "$CONFIG_PATH" + systemctl daemon-reload + systemctl enable --now deckdoc-probe.service + echo "DeckDoc probe installed and started. Captures remain private in ${STATE_DIR}/events." +} + +uninstall_probe() { + require_root + systemctl disable --now deckdoc-probe.service 2>/dev/null || true + if [ "$UNIT_PATH" = "/etc/systemd/system/deckdoc-probe.service" ]; then rm -f -- "$UNIT_PATH"; fi + if [ -f "${BIN_DIR}/deckdoc-probe.sh" ]; then rm -f -- "${BIN_DIR}/deckdoc-probe.sh"; fi + if [ -d "$BIN_DIR" ]; then rmdir "$BIN_DIR" 2>/dev/null || true; fi + if [ "$CONFIG_PATH" = "/etc/deckdoc-probe.conf" ]; then rm -f -- "$CONFIG_PATH"; fi + systemctl daemon-reload + echo "DeckDoc probe stopped and uninstalled. Existing incident captures were preserved in ${STATE_DIR}." +} + +purge_captures() { + require_root + if systemctl is-active --quiet deckdoc-probe.service; then + echo "Uninstall or stop the probe before purging captures." >&2 + exit 1 + fi + case "$STATE_DIR" in /var/lib/deckdoc-probe) rm -rf -- "$STATE_DIR" ;; *) exit 1 ;; esac + echo "DeckDoc probe captures permanently removed from ${STATE_DIR}." +} + +case "$ACTION" in + install) install_probe ;; + uninstall) uninstall_probe ;; + purge) purge_captures ;; + status) systemctl status deckdoc-probe.service --no-pager ;; + *) echo "Usage: sudo ./probe/install-probe.sh {install|status|uninstall|purge}" >&2; exit 2 ;; +esac diff --git a/setup.sh b/setup.sh index c6e661c..b714635 100755 --- a/setup.sh +++ b/setup.sh @@ -8,6 +8,9 @@ mkdir -p "${LOG_DIR}" chmod 755 "${DECKDOC_DIR}/deckdoc.sh" chmod 755 "${DECKDOC_DIR}"/modules/*.sh chmod 755 "${DECKDOC_DIR}"/tests/*.sh +chmod 755 "${DECKDOC_DIR}"/probe/*.sh +chmod 755 "${DECKDOC_DIR}"/bootprobe/*.sh +chmod 755 "${DECKDOC_DIR}"/privileged/* -echo "[*] DeckDoc v1.0.0 environment scaffolded successfully at ${DECKDOC_DIR}" +echo "[*] DeckDoc v3.2.0 environment scaffolded successfully at ${DECKDOC_DIR}" sync diff --git a/tests/fixtures/deckdoc-authorized.sudoers b/tests/fixtures/deckdoc-authorized.sudoers new file mode 100644 index 0000000..862e5ac --- /dev/null +++ b/tests/fixtures/deckdoc-authorized.sudoers @@ -0,0 +1,6 @@ +# Syntax fixture only. The installer replaces the user with the approving account. +deckdoc_test ALL=(root) NOPASSWD: NOSETENV: /var/lib/deckdoc-authorized/bin/deckdoc-authorized report +deckdoc_test ALL=(root) NOPASSWD: NOSETENV: /var/lib/deckdoc-authorized/bin/deckdoc-authorized report-display-black +deckdoc_test ALL=(root) NOPASSWD: NOSETENV: /var/lib/deckdoc-authorized/bin/deckdoc-authorized probe-capture +deckdoc_test ALL=(root) NOPASSWD: NOSETENV: /var/lib/deckdoc-authorized/bin/deckdoc-authorized probe-status +deckdoc_test ALL=(root) NOPASSWD: NOSETENV: /var/lib/deckdoc-authorized/bin/deckdoc-authorized version diff --git a/tests/test_runner.sh b/tests/test_runner.sh index 5a9e209..f52a2d2 100755 --- a/tests/test_runner.sh +++ b/tests/test_runner.sh @@ -7,7 +7,7 @@ cleanup() { rm -rf -- "${TEST_ENV}"; } trap cleanup EXIT echo "=========================================" -echo "DeckDoc v3.1.0 — Test Runner" +echo "DeckDoc v3.2.0 — Test Runner" echo "=========================================" # === Test 1: Mock sysfs structure === @@ -74,10 +74,10 @@ echo "" echo "--- Test 5: Module count ---" MODULE_COUNT=$(ls -1 "${DECKDOC_DIR}"/modules/*.sh | wc -l) echo " Total modules: ${MODULE_COUNT}" -if [ "$MODULE_COUNT" -eq 17 ]; then - echo " PASS: Expected 17 modules present (15 diagnostic + 2 remediation)." +if [ "$MODULE_COUNT" -eq 19 ]; then + echo " PASS: Expected 19 modules present (17 diagnostic + 2 remediation)." else - echo " FAIL: Expected 17 modules, found ${MODULE_COUNT}." + echo " FAIL: Expected 19 modules, found ${MODULE_COUNT}." exit 1 fi @@ -86,10 +86,10 @@ echo "" echo "--- Test 6: Parallel module launch check ---" LAUNCH_COUNT=$(grep -c '"${MODULES_DIR}/.*\.sh".*&[[:space:]]*$' "${DECKDOC_DIR}/deckdoc.sh" 2>/dev/null || echo 0) echo " Module launches in deckdoc.sh: ${LAUNCH_COUNT}" -if [ "$LAUNCH_COUNT" -eq 15 ]; then - echo " PASS: All 15 diagnostic modules launched in parallel." +if [ "$LAUNCH_COUNT" -eq 17 ]; then + echo " PASS: All 17 diagnostic modules launched in parallel." else - echo " FAIL: Expected 15 module launches, found ${LAUNCH_COUNT}." + echo " FAIL: Expected 17 module launches, found ${LAUNCH_COUNT}." exit 1 fi @@ -258,17 +258,22 @@ fi # === Test 13: Sensor-native thermal thresholds === echo "" echo "--- Test 13: Sensor-native thermal thresholds ---" -mkdir -p "${TEST_ENV}/hwmon/hwmon0" "${TEST_ENV}/hwmon/hwmon1" +mkdir -p "${TEST_ENV}/hwmon/hwmon0" "${TEST_ENV}/hwmon/hwmon1" "${TEST_ENV}/hwmon/hwmon2" printf 'acpitz\n' > "${TEST_ENV}/hwmon/hwmon0/name" printf '91000\n' > "${TEST_ENV}/hwmon/hwmon0/temp1_input" printf 'nvme\n' > "${TEST_ENV}/hwmon/hwmon1/name" printf '85000\n' > "${TEST_ENV}/hwmon/hwmon1/temp1_input" printf '82850\n' > "${TEST_ENV}/hwmon/hwmon1/temp1_max" printf '84850\n' > "${TEST_ENV}/hwmon/hwmon1/temp1_crit" +printf 'nvme-sentinel\n' > "${TEST_ENV}/hwmon/hwmon2/name" +printf '42000\n' > "${TEST_ENV}/hwmon/hwmon2/temp1_input" +printf '65261850\n' > "${TEST_ENV}/hwmon/hwmon2/temp1_max" THERMAL_REPORT="${TEST_ENV}/thermal-report.txt" DECKDOC_HWMON_DIR="${TEST_ENV}/hwmon" "${DECKDOC_DIR}/modules/thermal_fan.sh" > "$THERMAL_REPORT" if grep -q 'above 90 C; this sensor exposes no hardware critical threshold' "$THERMAL_REPORT" && \ grep -q 'at or above its exported critical threshold' "$THERMAL_REPORT" && \ + grep -q 'Ignoring implausible exported high threshold 65261850' "$THERMAL_REPORT" && \ + ! grep -q '65261.8 C' "$THERMAL_REPORT" && \ ! grep -q 'Thermal Trip Point Exceeded (>90C)' "$THERMAL_REPORT"; then echo " PASS: Thermal severity follows exported hardware thresholds without inventing a 90 C trip." else @@ -290,6 +295,311 @@ else exit 1 fi +# === Test 15: 24-hour crash-family classification === +echo "" +echo "--- Test 15: 24-hour crash-family classification ---" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'if [[ " $* " == *" 24 hours ago "* ]]; then' \ + ' for pid in $(seq 1 11); do echo "Tue 2026-07-21 09:10:00 CDT $pid 1000 1000 SIGTRAP present /usr/bin/steamwebhelper 3M"; done' \ + ' echo "Tue 2026-07-21 09:11:00 CDT 20 1000 1000 SIGSEGV present /usr/bin/gamescope-wl 3M"' \ + 'elif [[ " $* " == *" --since "* ]]; then' \ + ' for pid in $(seq 30 35); do echo "Tue 2026-07-21 09:12:00 CDT $pid 1000 1000 SIGTRAP present /usr/bin/steamwebhelper 3M"; done' \ + 'else' \ + ' echo "Tue 2026-07-21 09:11:00 CDT 20 1000 1000 SIGSEGV present /usr/bin/gamescope-wl 3M"' \ + 'fi' > "${TEST_ENV}/bin/coredumpctl" +chmod +x "${TEST_ENV}/bin/coredumpctl" +CRASH_RATE_REPORT="${TEST_ENV}/crash-rate-report.txt" +PATH="${TEST_ENV}/bin:${PATH}" DECKDOC_SKIP_JOURNAL=1 \ + "${DECKDOC_DIR}/modules/coredump_analysis.sh" > "$CRASH_RATE_REPORT" +if grep -q 'More than 10 crashes were recorded in the last 24 hours' "$CRASH_RATE_REPORT" && \ + grep -q 'Gamescope SIGABRT/SIGSEGV records occurred' "$CRASH_RATE_REPORT" && \ + grep -q 'steamwebhelper crashes: 11 (11 SIGTRAP)' "$CRASH_RATE_REPORT"; then + echo " PASS: 24-hour volume and executable/signal families are classified." +else + echo " FAIL: 24-hour crash classification is incomplete." + cat "$CRASH_RATE_REPORT" + exit 1 +fi + +# === Test 16: Issue-aligned memory thresholds and live swap I/O === +echo "" +echo "--- Test 16: Memory pressure thresholds ---" +mkdir -p "${TEST_ENV}/proc-memory" +printf '%s\n' \ + 'MemTotal: 16384000 kB' \ + 'MemFree: 100000 kB' \ + 'MemAvailable: 500000 kB' \ + 'SwapTotal: 1048576 kB' \ + 'SwapFree: 400000 kB' > "${TEST_ENV}/proc-memory/meminfo" +printf '%s\n' 'pswpin 60000' 'pswpout 70000' > "${TEST_ENV}/proc-memory/vmstat" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'echo "procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----"' \ + 'echo " r b swpd free buff cache si so bi bo in cs us sy id wa st"' \ + 'echo " 1 0 100000 500000 10000 200000 12 34 0 0 100 200 10 5 85 0 0"' > "${TEST_ENV}/bin/vmstat" +printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "${TEST_ENV}/bin/journalctl" +chmod +x "${TEST_ENV}/bin/vmstat" "${TEST_ENV}/bin/journalctl" +MEMORY_REPORT="${TEST_ENV}/memory-report.txt" +PATH="${TEST_ENV}/bin:${PATH}" DECKDOC_PROC_ROOT="${TEST_ENV}/proc-memory" DECKDOC_VMSTAT_SAMPLES=1 \ + "${DECKDOC_DIR}/modules/memory_swap.sh" > "$MEMORY_REPORT" +if grep -q 'CRITICAL: Available memory below 512 MB' "$MEMORY_REPORT" && \ + grep -q 'HIGH: Swap usage exceeds 50%' "$MEMORY_REPORT" && \ + grep -q 'Live swap I/O observed (si=12, so=34' "$MEMORY_REPORT"; then + echo " PASS: Memory, swap-consumption, and live-I/O thresholds match issue #5." +else + echo " FAIL: Memory pressure thresholds are inaccurate." + cat "$MEMORY_REPORT" + exit 1 +fi + +# === Test 17: Coupled Wi-Fi/SOF resume failure signature === +echo "" +echo "--- Test 17: Coupled Wi-Fi/SOF failure signature ---" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'echo "[10.000] ath11k_pci firmware crashed"' \ + 'echo "[10.250] traps: qrenderdoc error:0 in librenderdoc.so[1b437]"' \ + 'echo "[10.500] snd_sof_amd_vangogh ipc tx failed -22"' > "${TEST_ENV}/bin/journalctl" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'if [ "${1:-}" = "-o" ]; then echo "2: wlan0: mtu 1500 state DOWN mode DEFAULT"; else echo "2: wlan0: mtu 1500 state DOWN mode DEFAULT"; fi' > "${TEST_ENV}/bin/ip" +printf '%s\n' '#!/usr/bin/env bash' 'echo "Not connected."' > "${TEST_ENV}/bin/iw" +printf '%s\n' '#!/usr/bin/env bash' 'echo "01:00.0 Network controller: Qualcomm device"' > "${TEST_ENV}/bin/lspci" +chmod +x "${TEST_ENV}/bin/journalctl" "${TEST_ENV}/bin/ip" "${TEST_ENV}/bin/iw" "${TEST_ENV}/bin/lspci" +WIFI_REPORT="${TEST_ENV}/wifi-report.txt" +PATH="${TEST_ENV}/bin:${PATH}" "${DECKDOC_DIR}/modules/wifi_firmware.sh" > "$WIFI_REPORT" +if grep -q 'RESUME_SIGNATURE: WIFI_AND_SOF_FAILURES_IN_CURRENT_BOOT' "$WIFI_REPORT" && \ + grep -q 'CRITICAL: Wireless firmware crashed' "$WIFI_REPORT" && \ + ! grep -q 'qrenderdoc' "$WIFI_REPORT"; then + echo " PASS: Same-boot wireless and SOF failures produce an explicit correlation signature." +else + echo " FAIL: Coupled resume-device failures were not classified." + cat "$WIFI_REPORT" + exit 1 +fi + +# === Test 18: Gamescope restart severity threshold === +echo "" +echo "--- Test 18: Gamescope restart severity threshold ---" +if grep -q 'RESTART_COUNT:-0.*-gt 3' "${DECKDOC_DIR}/modules/gamescope_session.sh" && \ + grep -q 'CRITICAL: Gamescope session started' "${DECKDOC_DIR}/modules/gamescope_session.sh" && \ + grep -q 'systemd NRestarts + active invocation' "${DECKDOC_DIR}/modules/gamescope_session.sh"; then + echo " PASS: More than three Gamescope starts in one boot is critical." +else + echo " FAIL: Gamescope restart threshold does not match issue #4." + exit 1 +fi + +# === Test 19: Live fan-after-resume correlation === +echo "" +echo "--- Test 19: Live fan-after-resume correlation ---" +mkdir -p "${TEST_ENV}/acpi-hwmon/hwmon0" "${TEST_ENV}/acpi-power/BAT1" +printf '0\n' > "${TEST_ENV}/acpi-hwmon/hwmon0/fan1_input" +printf '75000\n' > "${TEST_ENV}/acpi-hwmon/hwmon0/temp1_input" +printf '80\n' > "${TEST_ENV}/acpi-power/BAT1/charge_control_end_threshold" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'echo "PM: suspend entry (s2idle)"' \ + 'echo "PM: suspend exit"' > "${TEST_ENV}/bin/journalctl" +chmod +x "${TEST_ENV}/bin/journalctl" +ACPI_REPORT="${TEST_ENV}/acpi-report.txt" +PATH="${TEST_ENV}/bin:${PATH}" DECKDOC_HWMON_DIR="${TEST_ENV}/acpi-hwmon" \ +DECKDOC_POWER_SUPPLY_ROOT="${TEST_ENV}/acpi-power" \ + "${DECKDOC_DIR}/modules/acpi_pm_state.sh" > "$ACPI_REPORT" +if grep -q 'RESUME_SIGNATURE: LIVE_ZERO_RPM_WITH_HOT_SENSOR_AFTER_SUSPEND' "$ACPI_REPORT" && \ + grep -q 'charge_control_end_threshold=80' "$ACPI_REPORT"; then + echo " PASS: Hot zero-RPM state is cross-correlated with suspend and charge-limit context." +else + echo " FAIL: Fan/resume cross-correlation is incomplete." + cat "$ACPI_REPORT" + exit 1 +fi + +# === Test 20: GPU page-fault correlation boundaries === +echo "" +echo "--- Test 20: GPU page-fault correlation boundaries ---" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'echo "amdgpu: page fault process game pid 44 Faulty UTCL2 client ID: CPF (0x5) MAPPING_ERROR"' \ + 'echo "amdgpu: ring gfx timeout"' \ + 'echo "amdgpu: GPU reset succeeded"' > "${TEST_ENV}/bin/journalctl" +chmod +x "${TEST_ENV}/bin/journalctl" +DXVK_REPORT="${TEST_ENV}/dxvk-report.txt" +PATH="${TEST_ENV}/bin:${PATH}" "${DECKDOC_DIR}/modules/dxvk_page_fault.sh" > "$DXVK_REPORT" +if grep -q 'command-path fault; driver, workload, and hardware remain candidates' "$DXVK_REPORT" && \ + grep -q 'GPU page-fault and ring-timeout evidence must be correlated' "$DXVK_REPORT" && \ + grep -q 'GPU reset succeeded; affected clients may still have terminated' "$DXVK_REPORT"; then + echo " PASS: Page faults are classified without over-claiming DXVK or hardware causality." +else + echo " FAIL: GPU page-fault causality boundaries are inaccurate." + cat "$DXVK_REPORT" + exit 1 +fi + +# === Test 21: Steam helper crash-frequency threshold === +echo "" +echo "--- Test 21: Steam helper crash-frequency threshold ---" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'for pid in $(seq 1 12); do echo "Tue 2026-07-21 09:10:00 CDT $pid 1000 1000 SIGTRAP present /usr/bin/steamwebhelper 3M"; done' > "${TEST_ENV}/bin/coredumpctl" +chmod +x "${TEST_ENV}/bin/coredumpctl" +STEAM_RATE_REPORT="${TEST_ENV}/steam-rate-report.txt" +PATH="${TEST_ENV}/bin:${PATH}" DECKDOC_DUMP_DIR="${TEST_ENV}/dumps" \ +DECKDOC_SESSION_HOME="${TEST_ENV}/session-home" DECKDOC_SKIP_JOURNAL=1 \ + "${DECKDOC_DIR}/modules/steam_client_logs.sh" > "$STEAM_RATE_REPORT" +if grep -q 'Current-boot steamwebhelper dumps: 12' "$STEAM_RATE_REPORT" && \ + grep -q 'HIGH: Repeated steamwebhelper crashes exceed' "$STEAM_RATE_REPORT"; then + echo " PASS: Repeated Steam helper crashes are surfaced with a bounded threshold." +else + echo " FAIL: Steam helper crash frequency is not surfaced." + cat "$STEAM_RATE_REPORT" + exit 1 +fi + +# === Test 22: Lightweight incident-probe classification and capture === +echo "" +echo "--- Test 22: Lightweight incident probe ---" +if [ "$("${DECKDOC_DIR}/probe/deckdoc-probe.sh" classify 'amdgpu: ring gfx timeout')" != "gpu" ] || \ + [ "$("${DECKDOC_DIR}/probe/deckdoc-probe.sh" classify 'snd_sof_amd_vangogh ipc tx failed -22')" != "audio" ] || \ + [ "$("${DECKDOC_DIR}/probe/deckdoc-probe.sh" classify 'normal informational line')" != "unmatched" ]; then + echo " FAIL: Probe signature classification is inaccurate." + exit 1 +fi +PROBE_STATE="${TEST_ENV}/probe-state" +DECKDOC_PROBE_STATE_DIR="$PROBE_STATE" DECKDOC_PROBE_POST_SECONDS=0 \ +DECKDOC_PROBE_PRE_SECONDS=1 DECKDOC_PROBE_MAX_EVENTS=2 DECKDOC_PROBE_MAX_EVENT_KIB=64 \ + "${DECKDOC_DIR}/probe/deckdoc-probe.sh" capture test >/dev/null +PROBE_LATEST=$(readlink -f "${PROBE_STATE}/latest") +PROBE_REPORT="${TEST_ENV}/probe-report.txt" +DECKDOC_PROBE_STATE_DIR="$PROBE_STATE" "${DECKDOC_DIR}/modules/probe_incidents.sh" > "$PROBE_REPORT" +if [ -f "${PROBE_LATEST}/metadata.txt" ] && [ -f "${PROBE_LATEST}/state.log" ] && \ + [ -f "${PROBE_LATEST}/journal.log" ] && grep -q 'category=manual' "$PROBE_REPORT"; then + echo " PASS: Probe captures a bounded private incident and the main report can ingest it." +else + echo " FAIL: Probe capture/report integration is incomplete." + exit 1 +fi + +# === Test 23: Probe opt-in and safety contract === +echo "" +echo "--- Test 23: Probe service safety contract ---" +if grep -q 'systemctl enable --now deckdoc-probe.service' "${DECKDOC_DIR}/probe/install-probe.sh" && \ + grep -q 'ProtectSystem=strict' "${DECKDOC_DIR}/probe/deckdoc-probe.service" && \ + grep -q 'ReadWritePaths=/var/lib/deckdoc-probe' "${DECKDOC_DIR}/probe/deckdoc-probe.service" && \ + grep -q 'Captures are private but unredacted' "${DECKDOC_DIR}/probe/deckdoc-probe.sh" && \ + ! grep -q 'install_probe' "${DECKDOC_DIR}/setup.sh"; then + echo " PASS: Continuous monitoring remains opt-in, resource-limited, private, and read-only." +else + echo " FAIL: Probe opt-in or service safety boundary is incomplete." + exit 1 +fi + +# === Test 24: Dock/USB-C/PD evidence boundaries === +echo "" +echo "--- Test 24: Dock and USB-C diagnostics ---" +DOCK_SYS="${TEST_ENV}/dock-sys" +mkdir -p "${DOCK_SYS}/class/typec/port0" "${DOCK_SYS}/class/typec/port0-partner" \ + "${DOCK_SYS}/class/power_supply/ucsi-source" "${DOCK_SYS}/class/drm/card0-DP-1" \ + "${DOCK_SYS}/class/net/eth0" "${DOCK_SYS}/devices/usb1/1-1/net/eth0" \ + "${DOCK_SYS}/bus/usb/drivers/r8152" +printf 'host\n' > "${DOCK_SYS}/class/typec/port0/data_role" +printf 'sink\n' > "${DOCK_SYS}/class/typec/port0/power_role" +printf 'USB Power Delivery\n' > "${DOCK_SYS}/class/typec/port0/power_operation_mode" +printf 'USB\n' > "${DOCK_SYS}/class/power_supply/ucsi-source/type" +printf '1\n' > "${DOCK_SYS}/class/power_supply/ucsi-source/online" +printf '15000000\n' > "${DOCK_SYS}/class/power_supply/ucsi-source/voltage_now" +printf '3000000\n' > "${DOCK_SYS}/class/power_supply/ucsi-source/current_now" +printf 'connected\n' > "${DOCK_SYS}/class/drm/card0-DP-1/status" +printf '1920x1080\n' > "${DOCK_SYS}/class/drm/card0-DP-1/modes" +head -c 128 /dev/zero > "${DOCK_SYS}/class/drm/card0-DP-1/edid" +printf 'up\n' > "${DOCK_SYS}/class/net/eth0/operstate" +printf '1\n' > "${DOCK_SYS}/class/net/eth0/carrier" +printf '1000\n' > "${DOCK_SYS}/class/net/eth0/speed" +ln -s "${DOCK_SYS}/devices/usb1/1-1/net/eth0" "${DOCK_SYS}/class/net/eth0/device" +ln -s "${DOCK_SYS}/bus/usb/drivers/r8152" "${DOCK_SYS}/devices/usb1/1-1/net/eth0/driver" +printf '%s\n' '#!/usr/bin/env bash' 'echo "xhci host controller reset error"' 'echo "usb 1-1: disconnect"' > "${TEST_ENV}/bin/journalctl" +printf '%s\n' '#!/usr/bin/env bash' 'if [ "${1:-}" = "-t" ]; then echo "/: Bus 001.Port 001: Dev 1, Driver=xhci_hcd"; else echo "Bus 001 Device 002: ID 0bda:8153 Realtek"; fi' > "${TEST_ENV}/bin/lsusb" +chmod +x "${TEST_ENV}/bin/journalctl" "${TEST_ENV}/bin/lsusb" +DOCK_REPORT="${TEST_ENV}/dock-report.txt" +PATH="${TEST_ENV}/bin:${PATH}" DECKDOC_SYS_ROOT="$DOCK_SYS" \ + "${DECKDOC_DIR}/modules/dock_usb_c.sh" > "$DOCK_REPORT" +if grep -q 'power_operation_mode=USB Power Delivery' "$DOCK_REPORT" && \ + grep -q 'calculated_instantaneous_power=45.00 W' "$DOCK_REPORT" && \ + grep -q 'card0-DP-1: status=connected edid_bytes=128' "$DOCK_REPORT" && \ + grep -q 'eth0: driver=r8152' "$DOCK_REPORT" && \ + grep -q 'DOCK_SIGNATURE: TOPOLOGY_CHANGE_WITH_DOCK_PATH_ERROR' "$DOCK_REPORT" && \ + grep -q 'not dock certification' "$DOCK_REPORT"; then + echo " PASS: Dock topology, exported PD telemetry, and correlated path errors remain evidence—not certification." +else + echo " FAIL: Dock/USB-C evidence collection is incomplete." + cat "$DOCK_REPORT" + exit 1 +fi + +# === Test 25: Rescue collection remains outside-OS and read-only === +echo "" +echo "--- Test 25: DeckDoc Rescue safety contract ---" +RESCUE_HELP=$("${DECKDOC_DIR}/bootprobe/deckdoc-rescue-collect.sh" --help) +if grep -q 'journalctl --image' <<< "$RESCUE_HELP" && \ + grep -q 'never mounts, repairs, unlocks, or writes' <<< "$RESCUE_HELP" && \ + grep -q 'journalctl --image=' "${DECKDOC_DIR}/bootprobe/deckdoc-rescue-collect.sh" && \ + ! grep -Eq '^[[:space:]]*(sudo[[:space:]]+)?(mount|fsck|btrfs[[:space:]]+check)([[:space:]]|$)' "${DECKDOC_DIR}/bootprobe/deckdoc-rescue-collect.sh" && \ + grep -q 'unsigned DeckDoc Rescue alpha' "${DECKDOC_DIR}/bootprobe/build-rescue-image.sh" && \ + grep -q 'not a Valve recovery image' "${DECKDOC_DIR}/bootprobe/build-rescue-image.sh"; then + echo " PASS: Rescue collection uses the image reader and the builder states its alpha trust boundary." +else + echo " FAIL: Rescue collection or image trust boundary is incomplete." + exit 1 +fi + +# === Test 26: Exact-command privileged authorization boundary === +echo "" +echo "--- Test 26: Privileged diagnostic authorization ---" +set +e +"${DECKDOC_DIR}/privileged/deckdoc-authorized" unexpected > "${TEST_ENV}/broker.out" 2> "${TEST_ENV}/broker.err" +BROKER_EXIT=$? +set -e +if [ "$BROKER_EXIT" -eq 2 ] && \ + grep -Fq 'NOPASSWD: NOSETENV: ${BROKER} report' "${DECKDOC_DIR}/privileged/install-authorized.sh" && \ + grep -q 'sha256sum -c --status' "${DECKDOC_DIR}/privileged/deckdoc-authorized" && \ + grep -q 'sudo -n "$BROKER"' "${DECKDOC_DIR}/privileged/deckdoc-authorized-client.sh" && \ + ! grep -Eq -- '--fix|bash[[:space:]]+-c|sh[[:space:]]+-c' "${DECKDOC_DIR}/privileged/deckdoc-authorized"; then + echo " PASS: Authorization exposes exact read-only operations and rejects unknown commands." +else + echo " FAIL: Privileged broker boundary is too broad or incomplete." + cat "${TEST_ENV}/broker.err" + exit 1 +fi +if command -v visudo >/dev/null 2>&1; then + if visudo -cf "${DECKDOC_DIR}/tests/fixtures/deckdoc-authorized.sudoers" >/dev/null 2>&1; then + echo " PASS: Exact-command sudoers fixture passes visudo syntax validation." + else + echo " FAIL: Exact-command sudoers fixture is invalid." + exit 1 + fi +fi + +# === Test 27: DeckMD questionnaire and knowledge integrity === +echo "" +echo "--- Test 27: DeckMD symptom checker schema ---" +if command -v node >/dev/null 2>&1; then + node --check "${DECKDOC_DIR}/docs/assets/questionnaire.js" + node --check "${DECKDOC_DIR}/docs/assets/knowledge.js" + node --check "${DECKDOC_DIR}/docs/assets/app.js" + node "${DECKDOC_DIR}/tests/validate_deckmd.js" + echo " PASS: DeckMD facts, progressive suggestions, rules, and wiki routes are internally consistent." +elif grep -Fq 'display: ["sound-works", "screen-backlight", "screen-no-light", "input-works", "ssh-works", "stream-works", "external-works", "during-game", "after-wake"' \ + "${DECKDOC_DIR}/docs/assets/questionnaire.js" && \ + grep -q 'const related = window.DECKDOC_RELATED_CHECKS' "${DECKDOC_DIR}/docs/assets/app.js" && \ + grep -q 'No safe pattern match yet' "${DECKDOC_DIR}/docs/assets/app.js"; then + echo " PASS: Required progressive-display and safe-unknown contracts are present." + echo " NOTE: Full JavaScript/schema validation skipped because Node.js is not installed." +else + echo " FAIL: DeckMD structural fallback contract is incomplete." + exit 1 +fi + echo "" echo "=========================================" echo "All scaffold tests completed successfully." diff --git a/tests/validate_deckmd.js b/tests/validate_deckmd.js new file mode 100644 index 0000000..4ca330b --- /dev/null +++ b/tests/validate_deckmd.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const root = path.resolve(__dirname, ".."); +const context = { window: {} }; +vm.createContext(context); +for (const file of ["docs/assets/questionnaire.js", "docs/assets/knowledge.js"]) { + vm.runInContext(fs.readFileSync(path.join(root, file), "utf8"), context, { filename: file }); +} + +const questions = context.window.DECKDOC_QUESTIONNAIRE; +const knowledge = context.window.DECKDOC_KNOWLEDGE; +const related = context.window.DECKDOC_RELATED_CHECKS; +const failures = []; + +if (!Array.isArray(questions) || questions.length < 8) failures.push("questionnaire must contain at least 8 sections"); +if (!Array.isArray(knowledge) || knowledge.length < 12) failures.push("knowledge base must contain at least 12 diagnostic branches"); + +const primary = new Set((questions.find((group) => group.id === "symptom") || { options: [] }).options.map(([id]) => id)); +const allIds = questions.flatMap((group) => group.options.map(([id]) => id)); +const uniqueIds = new Set(allIds); +const duplicates = [...new Set(allIds.filter((id, index) => allIds.indexOf(id) !== index))]; +if (duplicates.length) failures.push(`duplicate option IDs: ${duplicates.join(", ")}`); + +for (const rule of knowledge) { + for (const key of ["symptoms", "requiresAll", "contextsAny"]) { + for (const id of rule[key] || []) if (!uniqueIds.has(id)) failures.push(`${rule.id}.${key} references missing ${id}`); + } + if (!rule.link || !fs.existsSync(path.join(root, "docs", `${rule.link}.md`))) failures.push(`${rule.id} has a missing wiki route`); +} + +for (const [symptom, suggestions] of Object.entries(related || {})) { + if (!primary.has(symptom)) failures.push(`related-check map uses non-primary symptom ${symptom}`); + for (const id of suggestions) if (!uniqueIds.has(id)) failures.push(`${symptom} suggests missing ${id}`); +} + +const displayChecks = related?.display || []; +for (const required of ["sound-works", "screen-backlight", "screen-no-light", "input-works", "ssh-works", "during-game", "after-wake"]) { + if (!displayChecks.includes(required)) failures.push(`display follow-ups omit ${required}`); +} + +if (allIds.length < 100) failures.push(`expected a massive checklist; found only ${allIds.length} facts`); + +if (failures.length) { + console.error(failures.join("\n")); + process.exit(1); +} +console.log(`DeckMD schema valid: ${questions.length} sections, ${allIds.length} unique facts, ${knowledge.length} ranked branches.`); diff --git a/tests/validate_links.js b/tests/validate_links.js new file mode 100644 index 0000000..a0144eb --- /dev/null +++ b/tests/validate_links.js @@ -0,0 +1,44 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const root = path.resolve(__dirname, ".."); +const markdown = [path.join(root, "README.md"), path.join(root, "ROADMAP.md")]; + +function walk(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const file = path.join(directory, entry.name); + if (entry.isDirectory()) walk(file); + else if (entry.name.endsWith(".md")) markdown.push(file); + } +} +walk(path.join(root, "docs")); + +const failures = []; +for (const file of markdown) { + const contents = fs.readFileSync(file, "utf8"); + for (const match of contents.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) { + const raw = match[1].trim(); + if (/^(?:[a-z]+:|#)/i.test(raw)) continue; + const target = raw.split("#")[0]; + if (!target) continue; + const plain = path.resolve(path.dirname(file), target); + const candidates = path.extname(target) ? [plain] : [plain, `${plain}.md`]; + if (!candidates.some(fs.existsSync)) failures.push(`${path.relative(root, file)} -> ${raw}`); + } +} + +const html = fs.readFileSync(path.join(root, "docs/index.html"), "utf8"); +for (const match of html.matchAll(/(?:href|src)="([^"]+)"/g)) { + const raw = match[1]; + if (/^(?:https?:|#)/.test(raw)) continue; + const target = path.resolve(root, "docs", raw.replace(/^\.\//, "")); + if (!fs.existsSync(target)) failures.push(`docs/index.html -> ${raw}`); +} + +if (failures.length) { + console.error(`Broken local links:\n${failures.join("\n")}`); + process.exit(1); +} +console.log(`Local links valid across ${markdown.length} Markdown files and docs/index.html.`);