Skip to content

Wave 9 #122.d.ii — Phase 6 Tailscale wizard QR code (mobile pairing) - #119

Open
bernardc6 wants to merge 1 commit into
mainfrom
subagent/phase6-tailscale-qr-code
Open

Wave 9 #122.d.ii — Phase 6 Tailscale wizard QR code (mobile pairing)#119
bernardc6 wants to merge 1 commit into
mainfrom
subagent/phase6-tailscale-qr-code

Conversation

@bernardc6

Copy link
Copy Markdown
Collaborator

What this slice does

User-facing: When RoamCore is ready for me to pair my phone with the van, the dashboard shows a small black-and-white square I can scan with the Tailscale app on my phone — so I don't have to type a long secret code by hand.

Context

Phase 6 of the 2026-08-03 Product Build Directive calls for 'Guided Tailscale onboarding — wizard, QR, connectivity test, remote PWA access, troubleshooting.' The Phase 6 wizard package (roamcore_remote_access_setup.yaml, sub-slice #122.a) already wires Path A (Tailscale) end-to-end + Path B/C/D honest stubs; this slice adds the QR step that the directive §'Phase 6 delivery' calls out ('QR codes, one-click links, clear explanations, no RoamCore-operated VPN control plane').

The QR tile surfaces a tailscale login URL of the form https://login.tailscale.com/a/<device_key> that the operator scans with the Tailscale app on their phone instead of typing the long auth key by hand. If the QR ever fails (no qrcode dep, display too small, etc.), a plain-English copy-paste block with the same URL surfaces automatically — fail-softly recovery per Bernard's 'must not fail + super intuitive + critical infrastructure' doctrine.

Files

  • NEW homeassistant/packages/roamcore_tailscale_qr.yaml — the new wizard-extension package (167 LOC): 4 input_text + 1 input_button + 1 binary_sensor + 1 template sensor + 1 shell_command + 2 §8 MANDATORY AUTOMATIONS.
  • NEW homeassistant/packages/scripts/qr_generator.py — pure-Python stdlib-only QR generator (~470 LOC) with byte-mode + Reed-Solomon + mask pattern 0 + SVG emission.
  • NEW homeassistant/packages/tests/test_tailscale_qr.py — pytest rig (30 contract tests, all PASS).
  • NEW scripts/checks/tailscale-qr-smoke.sh — bash smoke wrapper (16 assertions, all PASS).
  • NEW docs/setup/tailscale-qr.md — IKEA 5-step user-facing guide.
  • MOD homeassistant/packages/roamcore_setup_wizard.yaml (+24 lines) — adds 1 helper automation that copies rc_tailscale_auth_keyrc_tailscale_qr_device_key on stage transition.
  • MOD scripts/check.sh (+11 lines) — wires the new smoke into the core-only chain.

Verification

  • bash scripts/check.sh --core-only → exit 0, GREEN.
  • bash scripts/checks/tailscale-qr-smoke.sh → 16/16 PASS.
  • python3 -m pytest homeassistant/packages/tests/test_tailscale_qr.py -v → 30/30 PASS.
  • Visual sanity: python3 qr_generator.py 'https://login.tailscale.com/a/test-abc123' 256 /tmp/test-qr.svg → valid SVG with viewBox='0 0 256 256' + 210 dark modules + 211 total rects.

GOLDEN.md alignment (re-read 2026-08-06)

Product principles served: P1 (Novice-first UX — IKEA 5-step guide, plain-English status, no operator jargon in §1-§4), P2 (Mission-critical connectivity — auto-recover on QR failure, copy-paste fallback), P6 (OpenClaw first-class — the new rc_tailscale_qr_* entities are readable from OpenClaw).

Engineering principles respected: E1 (Customer-facing repo), E2 (Documentation-driven), E3 (Backup + rollback discipline — no Proxmox/HA/OpenWrt/vmbr0 touched), E4 (Git fast mode — direct-to-branch push), E5 (no vmbr0), E7 (Naming follows rc-entity-naming.md — every entity starts with rc_tailscale_qr_).

Anti-patterns avoided: no secrets committed (mode: password for the device-key), no vmbr0 touch, no wide PR (pure Phase 6 wizard extension), no Cron-handoff references in user copy, no SUPERSEDED banners in user tree, no tier-letter exposition in user copy.

Tier discipline

Tier-b recipe over the upstream Tailscale login URL schema (https://login.tailscale.com/a/<key>). The QR generator is pure-Python stdlib (no qrcode / segno / qrencode dep). No RoamCore-operated VPN control plane.

Scope discipline

Strictly additive, no overlap with in-flight Phase 6 slices (#122.a / #122.b / #122.c / #122.d.i). All slices-before-mine live in roamcore_remote_access_setup.yaml; this slice lives in its own dedicated package roamcore_tailscale_qr.yaml to keep file-scope conflicts out.

Rollback

git revert <sha> in a single commit; no state to recover (pure repo-local code + data).

Diff: +1472 / -0 across 7 files (1 commit on subagent/phase6-tailscale-qr-code, branched off 5f94405 = current origin/main tip).

User-facing: When RoamCore is ready for me to pair my phone with the
van, the dashboard shows a small black-and-white square I can scan
with the Tailscale app on my phone — so I don't have to type a long
secret code by hand.

Context

  Phase 6 of the 2026-08-03 Product Build Directive calls for
  'Guided Tailscale onboarding — wizard, QR, connectivity test,
  remote PWA access, troubleshooting.' The Phase 6 wizard package
  (roamcore_remote_access_setup.yaml, sub-slice #122.a) already wires
  Path A (Tailscale) end-to-end + Path B/C/D honest stubs; this slice
  adds the QR step that the directive §'Phase 6 delivery' calls out
  ('QR codes, one-click links, clear explanations, no RoamCore-operated
  VPN control plane').

  The QR tile surfaces a 'tailscale login' URL of the form
  'https://login.tailscale.com/a/<device_key>' that the operator
  scans with the Tailscale app on their phone instead of typing
  the long auth key by hand. If the QR ever fails (no qrcode dep,
  display too small, etc.), a plain-English copy-paste block with
  the same URL surfaces automatically — fail-softly recovery per
  Bernard's 'must not fail + super intuitive + critical
  infrastructure' doctrine.

  Scope (additive + isolated — no overlap with in-flight slices
  #122.a/#122.b/#122.c/#122.d.i which all live in
  roamcore_remote_access_setup.yaml):

    NEW homeassistant/packages/roamcore_tailscale_qr.yaml
        The new wizard-extension package. Adds the QR tile with:
        - 4 input_text helpers (device_key [mode: password],
          login_url, svg_path, nonce).
        - 1 input_button (qr_regenerate).
        - 1 binary_sensor (rc_tailscale_qr_visible — true when
          stage in [tailscale_paste_key, tailscale_done] AND key set).
        - 1 template sensor (rc_tailscale_qr_status — plain-English
          status string for the dashboard tile).
        - 1 shell_command (qr_render — invokes the stdlib generator).
        - 2 §8 MANDATORY AUTOMATIONS:
            §8.Q.1 qr_compute_login_url — stage transition →
              URL build + persist.
            §8.Q.2 qr_regenerate_on_request — button press →
              nonce increment + shell_command invocation.
        Tier-b recipe over a self-contained Python QR generator
        (pure stdlib, no qrcode / segno / qrencode dep, no
        RoamCore-operated VPN control plane).

    NEW homeassistant/packages/scripts/qr_generator.py
        Self-contained pure-Python QR encoder (stdlib-only). Implements
        the byte-mode + Reed-Solomon EC + mask pattern 0 + SVG
        emission. Writes a valid SVG with the right viewBox +
        deterministic per-pixel modules. Tested via '--self-test'
        (deterministic + valid for 3 sample payloads).
        Verifies: viewBox='0 0 256 256' + ≥1 dark module +
        idempotent (same input → identical SVG) +
        gracefully rejects payloads beyond version 10 capacity.
        No third-party deps (paranoid import check in the smoke).

    NEW homeassistant/packages/tests/test_tailscale_qr.py
        pytest rig (30 contract tests, all PASS):
        - (a) YAML parse + 4 input_text + input_button + binary_sensor +
          sensor + shell_command presence.
        - rc-entity-naming compliance (every entity_id starts with
          'rc_tailscale_qr_').
        - Device key is 'mode: password' (never logged).
        - YAML is idempotent (re-parsing produces the same dict).
        - (b) §8 Q.1 + §8 Q.2 automation contracts (stage transition
          + button press triggers, URL build + shell_command call
          actions).
        - (c) Secrets-leak grep (no tskey- / ts-auth- / hardcoded
          device-key patterns in any file).
        - (d) QR generator is stdlib-only (AST import scan).
        - QR generator produces valid SVG with correct viewBox +
          ≥1 dark module.
        - QR generator is idempotent.
        - QR generator handles over-long payloads gracefully
          (ValueError, NOT crash).
        - parse_svg returns structured info (viewBox, dark_modules,
          size_modules).
        - (e) IKEA doc has 5 sections (§1..§5) + no operator jargon
          in §1..§4 (no 'integration', 'entity', 'shell_command',
          'automation', 'input_text', 'binary_sensor',
          'template sensor').
        - No internal-speak references (no 'cron', 'sub-agent',
          'Wave 9', etc.).
        - (f) Tile visibility contract (only on the right stages).
        - (g) Kitchen-sink (every contract marker present).

    NEW scripts/checks/tailscale-qr-smoke.sh
        bash smoke wrapper (16 assertions, all PASS):
        - 4 file-presence checks (package + pytest + generator + doc).
        - YAML parse.
        - rc-entity-naming pre-check (every helper + automation +
          template binary_sensor + template sensor + automation id
          prefixed with rc_tailscale_qr_).
        - Helper/automation contract (4 input_text + button +
          2 automations + shell_command, device_key mode:password).
        - Secrets-leak check (no tskey- / ts-auth- patterns).
        - YAML idempotency probe.
        - QR generator: stdlib-only check (AST import scan) +
          self-test (exits 0) + canonical-Tailscale-URL render
          (valid SVG with right viewBox + ≥1 dark module).
        - IKEA doc shape check (5 sections + no jargon in §1..§4
          + no internal-speak references).
        - Pytest rig (test_tailscale_qr.py).

    NEW docs/setup/tailscale-qr.md
        IKEA 5-step user-facing guide:
        §1 What this is (one sentence plain English).
        §2 What you see.
        §3 What you do (5 numbered steps: open Tailscale app →
          point phone at square → approve → done).
        §4 What to do if it goes wrong (copy-paste URL fallback,
          regenerate button, etc.).
        §5 Useful links (developer plumbing references — internal
        to the doc).
        Operator→vanlifer translation: integration → connection;
        entity → device; shell_command → action; automation → rule;
        input_text → settings box; binary_sensor → on/off indicator;
        template sensor → what your dashboard shows.

    MOD homeassistant/packages/roamcore_setup_wizard.yaml (+24 lines)
        Adds 1 §8 helper automation:
        - rc_setup_register_tailscale_qr_device_key: copies
          input_text.rc_tailscale_auth_key (in the remote-access-setup
          wizard) into input_text.rc_tailscale_qr_device_key (in the
          new QR package) on stage transition to tailscale_paste_key.
          One automation, idempotent across re-fires. The remote-access
          wizard owns the secret; the QR package owns the public
          URL — this glues them without rewiring either package.

    MOD scripts/check.sh (+11 lines)
        Wires the new tailscale-qr-smoke.sh + test_tailscale_qr.py
        into the core-only chain via the run_if_present pattern.
        No other chain entries touched.

Verification

  - bash scripts/check.sh --core-only → exit 0, GREEN.
  - bash scripts/checks/tailscale-qr-smoke.sh → 16/16 PASS.
  - cd homeassistant/packages && python3 -m pytest tests/test_tailscale_qr.py -v
    → 30/30 PASS.
  - python3 homeassistant/packages/scripts/qr_generator.py
    'https://login.tailscale.com/a/test-abc123' 256 /tmp/test-qr.svg
    → produces valid SVG with viewBox='0 0 256 256' + 210 dark
    modules + 211 total rects (xml.etree.ElementTree parses cleanly).

  Visual sanity check (the render-on-host path):
    $ python3 homeassistant/packages/scripts/qr_generator.py \
        'https://login.tailscale.com/a/test-abc123' 256 /tmp/test-qr.svg
    OK: /tmp/test-qr.svg
    $ head -c 200 /tmp/test-qr.svg
    <?xml version='1.0' encoding='UTF-8'?>\n<svg xmlns='http://www.w3.org/2000/svg'
    viewBox='0 0 256 256' width='256' height='256' ...
    $ grep -c '<rect' /tmp/test-qr.svg
    211    # 210 dark modules + 1 white background rect

Doctrine applied

  Bernard's 2026-08-04 doctrine ('must not fail + super intuitive +
  critical infrastructure'), expanded 2026-08-04 21:18 UTC chat
  #7580 — applied:

  1. Verification is mandatory (real pytest rig + smoke). The
     QR payload parses as a valid 'tailscale login' URL; the rendered
     SVG has the correct size (viewBox='0 0 256 256') + dark modules
     (210 cells on the 21-57 module grid); the wizard surfaces the
     QR tile only when stage == tailscale_paste_key OR
     stage == tailscale_done (verified by test_qr_visible_only_at_correct_stages).

  2. Auto-recover (graceful fallback). If QR generation fails
     (no qrcode dep, qr_generator.py returns exit 2 on overflow, etc.),
     sensor.rc_tailscale_qr_status surfaces 'QR unavailable — use
     the URL below' AND the dashboard falls back to the plain-English
     copy-paste block with the same URL. No locked state.

  3. Plain-English errors ('Your phone can't see this QR code —
     copy this URL instead') — no raw Python stack trace surfaces
     on the dashboard tile. Status template strings are all
     one-sentence plain English.

  4. Idempotent installers (pytest asserts uniqueness). Every helper
     has a unique_id; the YAML is idempotent on re-parse (smoke
     asserts this); the QR generator is bit-deterministic (same
     input → byte-for-byte same output).

  5. Backup-before-mutate. This slice touches YAML + pytest only —
     no Proxmox / HA / OpenWrt / networking. Pure repo-local code.
     No state to back up; rollback is a single 'git revert'.

  6. Tier discipline. Tier-b recipe. The QR generator is pure
     stdlib (no extra dep), no RoamCore-operated VPN control plane
     (the directive §'Phase 6 delivery' is explicit on that).

  7. User-facing repo hygiene. IKEA 5-step user doc lives in
     docs/setup/ (not memory/ or Cron-handoff/). Developer plumbing
     in homeassistant/packages/ + tests/ + scripts/checks/. No
     SUPERSEDED banners in user tree. No Cron-handoff references
     in user copy. No tier-letter exposition in user copy.

GOLDEN.md alignment (re-read 2026-08-06 21:22 UTC)

  Product principles served:
    P1 Novice-first UX — the wizard surfaces a one-tap QR tile +
        a fallback copy-paste block. No YAML editing, no entity
        IDs in any user-facing string, no jargon in the docs/runbooks.
        IKEA 5-step user guide with one plain-English opener.
    P2 Mission-critical connectivity — the QR tile is the operator-
        friendly shortcut to remote access. The copy-paste fallback
        is the operational twin of fail-softly UI when the camera
        won't scan (e.g. cracked screen, dirty fingerprint sensor).
    P6 OpenClaw as a first-class citizen — binary_sensor
        rc_tailscale_qr_visible + sensor rc_tailscale_qr_status are
        readable from OpenClaw ('Is the QR ready?' / 'Show me the
        QR status').

  Engineering principles respected:
    E1 Customer-facing repo — no internal logs, no sensitive infra.
        The user doc lives in docs/setup/; the runbook-style
        §5 in the doc references the internal locations only.
    E2 Documentation-driven — the IKEA doc is the user-facing source
        of truth; the pytest rig is the source of truth for the
        YAML contract; the smoke is the source of truth for the
        chain.
    E3 Backup + rollback discipline — no Proxmox / HA / OpenWrt /
        networking change. Pure repo-local code + data. Rollback
        is a single 'git revert' with no state to recover.
    E4 Git fast mode — direct-to-branch push on
        subagent/phase6-tailscale-qr-code. One PR opened via
        'gh pr create'.
    E5 Safety rule: do not touch vmbr0 — no networking config
        touched.
    E7 Naming follows rc-entity-naming.md — every new entity_id
        starts with 'rc_tailscale_qr_'. The new subsystem is
        OWNED by 'homeassistant/packages/roamcore_tailscale_qr.yaml'.

  Anti-patterns explicitly avoided:
    - ❌ Hand-configuring Victron instead of using capability discovery
      → N/A: this slice is about a wizard step, not device discovery.
    - ❌ Touching vmbr0 without explicit Bernard override
      → no networking config touched.
    - ❌ Committing secrets or HA tokens to the repo
      → input_text.rc_tailscale_qr_device_key is 'mode: password'
        (operator-entered in HA, never committed to YAML).
        Smoke + pytest assert no tskey- / ts-auth- patterns in
        any committed file.
    - ❌ Adding 'advanced' features before the novice path is solid
      → the QR step IS the novice path; advanced operators can
        skip it (the URL is also exposed).
    - ❌ Wide PRs that mix networking + UI + infra changes
      → pure Phase 6 / wizard extension scope; 7 files +1/-0 in a
        single additive commit.
    - ❌ Pulling in unrelated project context
      → strictly Wave 9 #122.d.ii scope; no overlap with
        #122.a/#122.b/#122.c/#122.d.i (they all live in
        roamcore_remote_access_setup.yaml).
    - ❌ Putting internal engineering logs on the public GitHub
      → user-facing doc has no SUPERSEDED banners, no
        Cron-handoff references, no Wave 9 mention.

User-facing: When RoamCore is ready for me to pair my phone with the
van, the dashboard shows a small black-and-white square I can scan
with the Tailscale app on my phone — so I don't have to type a long
secret code by hand.

Rollback

  git revert <this sha> undoes the QR package + tests + smoke + doc
  + setup wizard helper + check.sh wiring in a single commit. No
  state to recover; nothing touches Proxmox / HA / OpenWrt / vmbr0
  / openclaw / sensitive infra. The pre-revert state is just the
  Phase 6 wizard package without the QR tile; everything else
  (the auth-key, the wizard stages, the recovery automation) is
  untouched.
if col_left < 0:
break
for col in (col_right, col_left):
if 0 <= row < size and 0 <= col < size:
from __future__ import annotations

import argparse
import math
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant