Skip to content

Wave 9 #122.d.iii — Phase 6 Tailscale self-test (Gate E 'tunnel works both ways') - #121

Open
bernardc6 wants to merge 1 commit into
mainfrom
subagent/phase6-tailscale-self-test
Open

Wave 9 #122.d.iii — Phase 6 Tailscale self-test (Gate E 'tunnel works both ways')#121
bernardc6 wants to merge 1 commit into
mainfrom
subagent/phase6-tailscale-self-test

Conversation

@bernardc6

Copy link
Copy Markdown
Collaborator

Phase 6 Tailscale wizard — sub-slice #122.d.iii

One-tap self-test that proves the Tailscale tunnel works in BOTH directions — not just that the dashboard loads (a stale one-way tunnel can still serve a cached page, which is the failure mode this slice catches).

What's in this slice

  • homeassistant/packages/roamcore_tailscale_self_test.yaml — the package
    • 4 inputs (tunnel URL password-mode + running flag + Run now button + last_run stamp)
    • 2 shell_command probes (10s timeout each) — outbound HTTPS via magicdns + inbound nonce POST
    • 3 command_line sensors (HTTP code + expected/received nonce files)
    • 4 template binary_sensors (outbound_ok / inbound_ok / ok / recovery)
    • 1 template sensor (status — 6 plain-English branches, zero jargon)
    • 3 §8 MANDATORY automations: §8.T.1 run / §8.T.2 recovery / §8.T.3 wizard advance
  • homeassistant/packages/tests/test_tailscale_self_test.py — 52 pytest tests (all green)
  • scripts/checks/tailscale-self-test-smoke.sh — 10 bash assertions + pytest wrapper
  • docs/setup/tailscale-self-test.md — IKEA 5-step user guide + operator→vanlifer translation table

Plus 3 additive edits to the existing files (connection.yml manifest entry + setup_wizard bridge automation + check.sh wire).

Doctrine

  • Verification is real (live HTTP probe via magicdns + nonce round-trip, not manifest-honesty)
  • Auto-recover (60s timeout → plain-English persistent notification)
  • Plain-English errors ("Hub can't reach itself through the tunnel — check your Tailscale ACL", not "curl exit 28")
  • Idempotent (re-tap Run now without re-typing tunnel URL; wizard advance no-ops if stage already past networking)
  • 10-second timeouts so a stalled tunnel never blocks the dashboard

GOLDEN.md alignment

Product principles served (verbatim):

  • P1 (Novice-first UX, fail-softly UI): one plain-English status phrase at a time; IKEA 5-step doc
  • P2 (Mission-critical connectivity — Tailscale-like remote access; failures must not lose data or lock the user out): two-probe round-trip catches the cached-page failure mode

Engineering principles respected: E1 (customer-facing repo), E2 (documentation-driven), E4 (git fast mode — direct-to-main on subagent branch), E7 (rc-entity-naming compliance — every entity_id starts with rc_tailscale_self_test_ / rc_tailscale_run_self_test).

Anti-patterns avoided: no Victron hand-config, no vmbr0 touch, no secrets committed (verified by secrets-leak grep), no advanced-mode hooks before novice path is solid, no wide PR (only files in scope), no unrelated project context, no internal jargon in user copy.

Verification

  • bash scripts/check.sh --core-only → GREEN (182 PASS markers, 0 FAIL across the chain)
  • bash scripts/checks/tailscale-self-test-smoke.sh → 10/10 PASS
  • python3 -m pytest homeassistant/packages/tests/test_tailscale_self_test.py -v → 52/52 PASS in 0.19s

Rollback

Revert this single commit. All changes are additive; no rewrites, no deletions. Nothing else in the repo depends on the new helpers until the parent wizard reaches tailscale_done.

User-facing

Lets me confirm with one tap that I can reach my van from anywhere — and tells me honestly if the tunnel isn't working both ways.

User-facing: Lets me confirm with one tap that I can reach my van from
anywhere — and tells me honestly if the tunnel isn't working both ways.

…el → phone → tunnel → HA)

User-facing: Lets me confirm with one tap that I can reach my van from
anywhere — and tells me honestly if the tunnel isn't working both ways.

Context

  This slice lands the Gate E 'tunnel works both ways' sub-slice of
  the Phase 6 Tailscale wizard (Wave 9 #122.d.iii). The previous
  slice #122.a (commit 5f94405, PR #108) wired Path A (Tailscale)
  end-to-end + shipped Path B/C/D as stubs; this slice adds a
  one-tap self-test that proves the tunnel really works in both
  directions — not just that the dashboard loads (a stale one-way
  tunnel can still serve a cached page, which is the failure mode
  this slice catches). Two probes (outbound HTTPS via magicdns +
  inbound nonce POST) plus a round-trip nonce comparison tell the
  operator honestly whether their van is reachable from anywhere.

  Per the doctrine ('must not fail + super intuitive + critical
  infrastructure'): the probes have 10-second timeouts so a stalled
  tunnel never blocks the dashboard; the recovery automation fires
  after 60s and surfaces a plain-English notification instead of
  dropping the operator into a manual recovery; the §8.T.3 wizard-
  advance automation only fires when the global setup stage is at
  'networking' (idempotent — no-ops if the parent wizard already
  advanced it).

Changes

  NEW (4 files):
    - homeassistant/packages/roamcore_tailscale_self_test.yaml
        Inputs (tunnel URL password-mode + running flag + Run now
        button + last_run stamp), 2 shell_command probes (10s
        timeout each), 3 command_line sensors (HTTP code +
        expected/received nonce files), 4 template binary_sensors
        (outbound_ok / inbound_ok / ok / recovery), 1 template
        sensor (status — 6 plain-English branches, zero jargon),
        3 §8 MANDATORY automations: §8.T.1 run (button + stage +
        event triggers, 30s settle, no URL clear), §8.T.2 recovery
        (60s timeout, persistent_notification, no URL clear),
        §8.T.3 wizard advance (only fires when rc_setup_stage ==
        networking, flips to 'map', idempotent).
    - homeassistant/packages/tests/test_tailscale_self_test.py
        Pytest rig (52 tests, all green): YAML parses, every
        required helper / automation / template entity present,
        §8.T.1/§8.T.2/§8.T.3 trigger+action contract verified,
        tunnel URL is mode: password (sensitive), running flag
        defaults off, last_run initial epoch, status template
        covers all 6 (url, running, outbound, inbound, recovery,
        never_run) combos via a pure-function helper that pins
        the YAML strings (catches silent drift), no operator
        jargon in user-facing output (forbidden terms: entity ID
        prefixes, bash, curl, magicdns, .ts.net, tskey-), YAML
        idempotent (PyYAML twice → same dict), shell_command has
        exactly 2 probes, command_line sensors have unique
        unique_ids, §8.T.1 + §8.T.2 do NOT call input_text.set_value
        on the tunnel URL (idempotent retry), rc-entity-naming
        compliance on every entity_id / automation id, IKEA doc
        5-step shape, no SUPERSEDED / Cron-handoff refs in user
        copy.
    - scripts/checks/tailscale-self-test-smoke.sh
        Bash smoke wrapper (10 standalone bash assertions +
        pytest rig): file presence, YAML parse, rc-naming pre-
        check, helpers + automations + templates pre-check, status
        template covers all branches, no operator jargon in status
        copy, secrets-leak check (no tskey- / no IPs), idempotency
        probe (PyYAML twice → same dict + exactly 2 probes), IKEA
        doc 5-step shape + translation table, §8.T.1 + §8.T.2
        wiring verified, pytest rig green.
    - docs/setup/tailscale-self-test.md
        IKEA 5-step user guide: What this is / What you see (6
        plain-English status phrases) / What you do (3 numbered
        steps) / What to do if it goes wrong (3 sub-cases) /
        Useful links + an operator→vanlifer translation table
        (Self-test / Round-trip / Outbound probe / Inbound probe /
        MagicDNS / ACL rules / Tunnel / Tailscale / Two-way /
        Persistent notification / input_button / input_text /
        Template sensor / Automation).

  ADDITIVE EDITS (3 files):
    - connections/remote-access/connection.yml (+45 lines)
        Extended wizard.setup_paths with the self_test entry
        (separate from the four remote-access paths A/B/C/D —
        doesn't pick a path, verifies the chosen path honestly
        works two-way). tier: b, recipe_over, estimated_time,
        requires_inputs, side_effects, setup_notes all
        documented. Path A (tier-a promotion candidate) is still
        wired; this slice is purely additive.
    - homeassistant/packages/roamcore_setup_wizard.yaml (+19 lines)
        New automation.rc_setup_trigger_tailscale_self_test_after_done:
        when rc_remote_access_setup_stage reaches 'tailscale_done',
        fires the 'rc_run_tailscale_self_test' event so the
        self-test package runs its probes. Idempotent: re-firing
        does NOT reset the tunnel URL or wizard progress.
    - scripts/check.sh (+1 line)
        Wire scripts/checks/tailscale-self-test-smoke.sh into the
        core-only chain at a stable slot next to
        remote-access-setup-smoke.sh (no duplicate wiring — the
        in-flight sub-agent's duplicate line was cleaned up).

Verification

  - bash scripts/check.sh --core-only is GREEN on this branch tip:
      • Tailscale self-test smoke: 10/10 PASS
      • Tailscale self-test pytest rig: 52/52 PASS (0.19s)
      • All other 32+ connection manifest smokes: GREEN
      • Connection state field smoke: GREEN
      • PWA install smoke: GREEN (26/26)
      • Hub golden-image smoke: GREEN (10/10)
      • Final summary: 'all requested smoke checks passed'
        (182 PASS markers / 0 FAIL markers across the chain).
  - The 52 pytest tests cover:
      • YAML structural sanity + idempotency.
      • Every required helper / automation / template entity
        present and named per rc-entity-naming.md.
      • §8.T.1 / §8.T.2 / §8.T.3 trigger + condition + action
        contract (including idempotent retry: no input_text.set_value
        on the tunnel URL helper).
      • status template covers all 6 state combinations via the
        pure-function helper pinned to the YAML strings.
      • status output has no operator jargon (entity IDs / bash /
        curl / magicdns / .ts.net / tskey-).
      • secrets-leak grep: no tskey- / no hard-coded IPv4 in the
        package.
      • IKEA doc 5-step shape + opener plain-English + translation
        table present + no SUPERSEDED / Cron-handoff.
      • shell_command probes both have 10s timeout.
      • Wizard advance advances to 'map' (NOT 'done') so it
        doesn't skip the rest of the setup wizard.

Rollback

  Revert this single commit. All changes are additive (no rewrites,
  no deletions). The new helpers / automations / sensors live in a
  brand-new file that nothing else in the repo depends on until
  the parent wizard reaches 'tailscale_done'. Removing the file +
  the bridge automation in roamcore_setup_wizard.yaml + the
  self_test entry in the connection's setup_paths block + the
  check.sh wire restores main to its prior state bit-for-bit.

  No Proxmox / HA / OpenWrt / networking / vmbr0 touched → no
  snapshot required (GOLDEN.md E3 N/A).

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

  Product principles served (quoted verbatim from GOLDEN.md):
    P1 (Novice-first UX — Apple-grade onboarding. Power users get an
       'Advanced mode.' Fail-softly UI everywhere.):
       The self-test card surfaces one plain-English sentence at a
       time (6 status branches, zero operator jargon); the IKEA doc
       walks the operator through 3 numbered steps; the recovery
       automation fires a plain-English notification instead of an
       error; the running flag defaults off so the dashboard doesn't
       lie about state on first boot.
    P2 (Mission-critical connectivity — LTE primary, Starlink
       failover, Tailscale-like remote access. Network failures must
       not lose data or lock the user out.):
       The two-probe round-trip catches the failure mode where a
       one-way tunnel still serves a cached dashboard (the user's
       exact symptom: 'the page loads but nothing works'); the
       10s probe timeouts + 60s recovery automation ensure a stalled
       tunnel never locks the operator out; the tunnel URL is
       preserved across retries so the operator doesn't have to
       re-type it under stress.

  Engineering principles respected:
    E1 (Customer-facing repo): IKEA doc lives at
       docs/setup/tailscale-self-test.md (public MkDocs surface);
       developer plumbing stays in homeassistant/packages/ + tests/
       + scripts/checks/.
    E2 (Documentation-driven): the MkDocs site picks up the new
       doc; cross-linked from docs/setup/guided-remote-access.md
       and docs/setup/local-access-fallback.md.
    E4 (Git fast mode): direct-to-main push on subagent/<slug>;
       no force-push; no PR review chain. This recovery commit
       completes the slice on the in-flight branch.
    E7 (Naming follows docs/reference/rc-entity-naming.md): every
       new entity_id starts with 'rc_tailscale_self_test_' or
       'rc_tailscale_run_self_test'; verified by 4 dedicated pytest
       tests.

  Anti-patterns explicitly avoided (the GOLDEN.md ❌ list):
    ❌ Hand-configuring Victron instead of using capability
       discovery → N/A, no Victron changes.
    ❌ Touching vmbr0 without explicit Bernard override → N/A, pure
       Home Assistant package YAML + IKEA doc.
    ❌ Committing secrets or HA tokens to the repo → verified: no
       tskey-, no hard-coded IPs, no PII; the tunnel URL is
       'mode: password' and operator-entered; the secrets-leak
       check is in the smoke and runs in CI.
    ❌ Adding 'advanced' features before the novice path is solid
       → the entire slice IS the novice path; no advanced mode
       hooks touched.
    ❌ Wide PRs that mix networking + UI + infra changes → only
       files in scope were touched; 4 NEW + 3 MODIFIED, all in
       the connections/remote-access + packages + docs tree.
    ❌ Pulling in unrelated project context → no other projects
       referenced; RoamCore-only.
    ❌ Putting internal engineering logs on the public GitHub →
       the IKEA doc opens with a plain-English sentence and has
       an operator→vanlifer translation table at the bottom.

  Tier discipline: kept tier: b on the connection manifest entry;
  Path A (tier-a promotion candidate) is the wired-up one; this
  slice is purely additive and does not promote any tier.

Doc-target check: docs/setup/tailscale-self-test.md (IKEA 5-step,
plain English, no jargon, with translation table). Confirmed in
docs/ tree, not in memory/ or Cron-handoff/.

Co-authored-by: Bernard <bernard@roamcore.local>

import re
from pathlib import Path
from typing import Any
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