wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
# ADR 0015: explicit K1 recovery beside the one-intent connection flow
|
||||
|
||||
Status: accepted product, recovery and presentation contract; executable
|
||||
coverage and remaining hardware acceptance are tracked in
|
||||
`docs/k1-connection-acceptance.manifest.json`.
|
||||
|
||||
Related acceptance items: `CONN-16` through `CONN-19`, `CONN-28`, `CONN-29`,
|
||||
`CONN-65`, and `CONN-68` through `CONN-78` in
|
||||
[`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
|
||||
|
||||
## Problem
|
||||
|
||||
Loss of K1 power, the router, Mac Wi-Fi, MQTT control or the backend does not
|
||||
prove whether K1 is physically scanning. Retained points, an open TCP port and a
|
||||
historical START are insufficient. Replaying START or STOP after an ambiguous
|
||||
dispatch boundary can create a second physical edge.
|
||||
|
||||
The durable physical-command ledger, exact read-only classification and
|
||||
fail-closed supervisor must remain. They must not make ordinary connection slow
|
||||
or surprising. In particular, selecting a device must not secretly connect,
|
||||
Verify, retire/reopen history or delay network credentials.
|
||||
|
||||
## Decision
|
||||
|
||||
### Existing product surface
|
||||
|
||||
K1 connection stays in the existing device plugin section headed
|
||||
**Подключение XGRIDS LixelKity K1**. The surrounding job, entity and lifecycle
|
||||
models do not change. This is novelty A: an improvement to an existing product
|
||||
surface. A separate wizard, modal flow and mandatory preflight/recovery surface
|
||||
are rejected.
|
||||
|
||||
The section reuses canonical shared `Button`, `TextField`, `ActivityIndicator`
|
||||
and `StatusBadge`. It creates no shared entity and uses no raw local HTML
|
||||
controls or literal local status colors.
|
||||
|
||||
### One-intent normal flow
|
||||
|
||||
The normal flow is:
|
||||
|
||||
1. choose Bridge, Direct Connect or Quick Connect locally;
|
||||
2. press the explicit Bluetooth search action;
|
||||
3. wait for exactly one six-second discovery;
|
||||
4. press **Выбрать** on one result;
|
||||
5. enter Bridge/Direct credentials immediately, or review the Quick Connect
|
||||
summary;
|
||||
6. press **Применить** once.
|
||||
|
||||
Opening the section, changing mode, selecting a row and every
|
||||
SSID/password keystroke perform zero browser-controller, device or host I/O.
|
||||
They create no backend operation and show no operation loader. An admitted fresh
|
||||
selection retains the selected card and exposes applicable inputs immediately.
|
||||
A candidate without current draft authority is omitted or presented only as
|
||||
non-actionable evidence; it never receives a misleading disabled primary.
|
||||
|
||||
Each explicit search owns exactly one bounded six-second discovery. It performs
|
||||
no connect, Verify, selection or mutation. Results are never auto-selected.
|
||||
|
||||
One Apply owns the normal connection intent. It may commit the local desired-mode
|
||||
draft under backend CAS and may cross at most one reviewed K1 mutation boundary.
|
||||
Its frontend handler performs no hidden Scan, Verify, reconnect, retirement,
|
||||
reopen, candidate substitution or retry. Quick, Bridge and Direct use the same single primary
|
||||
**Применить** action; credentials are required only for Bridge and Direct.
|
||||
|
||||
Ordinary Bridge Apply never opts into changing the controlling Mac's Wi-Fi
|
||||
association. Host switching is a separate future consequential operator action,
|
||||
not an Apply substep. K1 provisioning can therefore succeed as
|
||||
`network_applied` while control is `control_not_ready`. That result must not
|
||||
repeat that intent's BLE write. Recommended separately explicit read-only
|
||||
Verify/recovery may establish route, endpoint and DeviceInfo/control evidence
|
||||
for the applied topology; a new intent remains separately policy-gated.
|
||||
|
||||
The exact REST response owns completion of the Apply mutation. A snapshot with
|
||||
`connection_attempt.phase=network_applied` is accepted immediately when
|
||||
`control_state` is `control_not_ready` or `unknown`; the controller does not
|
||||
wait for WebSocket/poll convergence or call the full connection-ready
|
||||
requirement. The service may continue supervised same-intent control bootstrap
|
||||
after this fast durable ACK, but only read-only: no BLE/host mutation, mutation
|
||||
retry, new UI action or second Apply. This is not a hidden frontend Scan or
|
||||
Verify. Exact connection-ready remains mandatory before control or physical
|
||||
START. This separation spends the old intent before a delayed state channel
|
||||
could invite its duplicate replay. `connection_attempt` is a read model, not
|
||||
permanent lifecycle authority; current server policy may admit a separately
|
||||
explicit new intent.
|
||||
|
||||
While the exact service-owned bootstrap child is `accepted` or `running` and
|
||||
projects `safe_next_action=wait-for-current-attempt`, the UI shows only one
|
||||
passive **Сеть настроена · подтверждаем управление** indicator. It enables no
|
||||
Verify, mode change, Scan, row or Apply action. Terminal unready/unknown child
|
||||
state then exposes the separately explicit policy-gated recovery choices.
|
||||
|
||||
`network_applied` plus unready or unknown control spends the old Apply and gates
|
||||
ordinary mode change, Scan, row selection and Apply. It first waits passively
|
||||
for an exact active service child; after terminal settlement it presents an
|
||||
explicit recovery choice, regardless of browser-local mode. It never authorizes
|
||||
automatic or same-intent replay. Recommended Verify is pinned to the backend
|
||||
`serverBound` current/configured transport and mode; it never falls back to a
|
||||
selected browser row and is not a prerequisite for every new intent.
|
||||
|
||||
Current server policy may admit a distinct, explicit new-intent path. Bridge
|
||||
uses `prepare-select-device`, a local-only CAS with zero device/host I/O; only
|
||||
after its success may the operator initiate a fresh six-second Scan. Quick and
|
||||
Direct use explicit policy-gated `scan-ble`, then the latest fresh row and a new
|
||||
idempotency Apply. A mode change requires backend `mode_selection` authority and
|
||||
then a fresh explicit Scan. No recovery choice performs hidden Scan, selection,
|
||||
Verify, provisioning or continuation of the old Apply, and the browser never
|
||||
manufactures authority.
|
||||
|
||||
### Freshness and outcome semantics
|
||||
|
||||
Apply is admitted only for the exact selected transport, completed discovery
|
||||
generation, backend runtime, desired-mode revision, reconfiguration intent and
|
||||
policy snapshot. Authority drift before dispatch is a terminal, zero-device-I/O
|
||||
`stale` result. The UI keeps the result understandable, labels it explicitly and
|
||||
offers a new explicit six-second search. It never starts that search itself.
|
||||
|
||||
A failure before the reviewed mutation boundary is `not-dispatched` or
|
||||
`failed`, with zero K1 mutation. A lost response, timeout, power failure or
|
||||
process death after dispatch is `outcome-unknown`, with
|
||||
`safe_to_retry=false`. The durable network-attempt ledger prevents replay.
|
||||
Credentials are never reused automatically. A later operator Apply is a new
|
||||
intent and must pass all current gates.
|
||||
|
||||
## Physical safety remains separate
|
||||
|
||||
Network attempts are disposable; physical START/STOP ambiguity is durable:
|
||||
|
||||
- START and STOP never replay automatically;
|
||||
- control loss does not prove scanning stopped;
|
||||
- local receiver/camera/ingress cleanup is not physical STOP;
|
||||
- a wrong K1/transport/profile/project cannot reconcile the record;
|
||||
- READY records cessation without rewriting historical command outcome;
|
||||
- exact same-project SCANNING may mint one single-use confirmed STOP permit on
|
||||
the still-open exact control binding;
|
||||
- accepted STOP without READY or SCAN_STOPPING by the backend deadline closes
|
||||
only host-owned resources, records `timed_out` / `standby-unknown`, preserves
|
||||
the unresolved ledger and keeps every mutation fenced.
|
||||
|
||||
The composite supervisor and physical-command ledger can disable Apply before
|
||||
device I/O. Their denial does not turn mode, selection or input into recovery.
|
||||
|
||||
### Explicit read-only recovery
|
||||
|
||||
Recovery is a distinct, explicitly requested exceptional action. It is never a
|
||||
continuation of row selection or Apply. The browser supplies neither endpoint,
|
||||
substitute transport nor ledger authority. The backend pins the durable record's
|
||||
exact transport, identity/profile, operation/revision, acquisition/project,
|
||||
topology revision and host epoch.
|
||||
|
||||
The non-reconnecting observation is:
|
||||
|
||||
```text
|
||||
topology-probed
|
||||
-> pre-start-control-opened
|
||||
-> device-info-requested (ordinal 1; exactly one publish)
|
||||
-> device-info-verified
|
||||
-> awaiting-passive-fresh-status
|
||||
-> cessation | active-same-project | foreign-active | inconclusive | failed
|
||||
```
|
||||
|
||||
It publishes exactly one canonical DeviceInfo request and then accepts only a
|
||||
fresh non-retained DeviceStatus from the same socket generation after that
|
||||
barrier. It publishes no status solicitation, DeviceConfig, time sync,
|
||||
workspace, project, START or STOP; it never scans, reconnects, provisions or
|
||||
continues into Apply.
|
||||
|
||||
Canonical READY records cessation/standby. Initialized SCANNING may rebind only
|
||||
when operation/acquisition, identity/profile, transport, host epoch and project
|
||||
all match; it exposes one separate single-use confirmed STOP checkpoint.
|
||||
Foreign, stale or inconclusive evidence changes no topology or authority.
|
||||
|
||||
### Explicit retirement and reopen
|
||||
|
||||
`physical-command.retire-unavailable` is a separately confirmed local durable
|
||||
recovery action for one unresolved target that is truly unavailable or replaced.
|
||||
Admission requires stable idempotency identity and exact backend runtime,
|
||||
operation, ledger revision and transport CAS plus safe lifecycle ownership. It
|
||||
preserves the original unknown outcome, activates the exact-transport deny,
|
||||
performs zero device/host I/O and starts no discovery.
|
||||
|
||||
`physical-command.reopen-retired-reconciliation` is also separately confirmed.
|
||||
It requires an exact fresh same-transport candidate, stable `reopening_id`, exact
|
||||
runtime/revision/retirement/transport/discovery CAS and safe lifecycle ownership.
|
||||
It preserves retirement audit, removes only that retirement's active deny and
|
||||
performs zero device/host I/O. The explicit recovery intent may then run one
|
||||
exact read-only observation. **Выбрать** never invokes retirement, reopen or
|
||||
Verify. The only Apply exception is an internal, request-bound local reopen
|
||||
checkpoint for an explicit scenario reset plus its exact successor Scan. It is
|
||||
ordered after network PREPARED and before the sole dispatch edge, remains
|
||||
invisible in the wizard and grants no command authority. The same applied
|
||||
intent may then settle it read-only from fresh DeviceInfo plus non-retained
|
||||
READY/SCANNING evidence.
|
||||
|
||||
FW 3.0.2 BLE `7f02` contains no stable DeviceInfo identity. Mission Core cannot
|
||||
prove during BLE-only discovery that the same physical unit has a new
|
||||
CoreBluetooth UUID. This remains an explicit protocol/hardware gap.
|
||||
|
||||
### Bounded durable audit rollover
|
||||
|
||||
An explicit local scenario reset must not become unavailable merely because
|
||||
closed retire/reopen history filled the 64 KiB hot ledger. Before a transition
|
||||
would exceed that bound, Mission Core durably publishes the complete previous
|
||||
ledger as a private, owner-only, content-addressed archive segment and then
|
||||
atomically publishes a compact v4 main record. The main record retains every
|
||||
active retirement deny, the newest lost-response retire/reopen checkpoint, and
|
||||
all reconciliation/confirmation proof required by the current physical
|
||||
operation. Compaction never changes a device outcome and performs no device,
|
||||
network or host I/O.
|
||||
|
||||
Archive segments form a predecessor hash chain with exact sequence and byte
|
||||
accounting. Reload verifies directory and file ownership/mode, rejects symlink
|
||||
traversal, bounds total segments and bytes, reparses every embedded ledger and
|
||||
fails closed for a missing, replayed, reordered or tampered segment. Operation,
|
||||
reconciliation, verification, confirmation, retirement and reopening identities
|
||||
remain globally one-use across the hot record and archive. The archive segment
|
||||
is fsynced before the main-file replace: a crash may leave only an inert orphan,
|
||||
while retry of the same CAS reuses identical bytes and cannot duplicate the
|
||||
referenced chain.
|
||||
|
||||
Scenario reset asks the ledger to build the exact prospective retirement or
|
||||
prepared→not-dispatched plan before closing any local receiver, camera,
|
||||
control-session or network ownership. That shared planner applies the same hot
|
||||
serialization, compaction, segment, count and total-byte bounds as commit. When
|
||||
rollover is required, preflight may idempotently prepublish only the immutable
|
||||
content-addressed predecessor; the main revision/CAS and physical disposition
|
||||
remain unchanged. This also proves owner/mode, symlink and content-collision
|
||||
conditions before teardown.
|
||||
|
||||
Archive publication is restart-safe at the hard-link boundary. A process death
|
||||
after destination link and directory fsync but before temporary-name unlink may
|
||||
leave exactly two private names for one inode. Retry removes only a strictly
|
||||
named, owner-only temporary alias whose bytes and inode exactly match the
|
||||
expected destination and whose link count is exactly two, fsyncs that cleanup,
|
||||
then reuses the destination. Any unrelated hard link, extra temporary, symlink,
|
||||
metadata mismatch or byte mismatch remains a fail-closed corruption condition.
|
||||
|
||||
## Failure and restart semantics
|
||||
|
||||
- UI entry, mode, selection, input, polling, refresh and layout changes
|
||||
start no device operation.
|
||||
- Search starts only when pressed, runs once for six seconds and terminalizes.
|
||||
- Apply starts only when pressed, uses one exact fresh candidate and may perform
|
||||
at most one K1 mutation.
|
||||
- Candidate/runtime/intent drift is explicit stale, never hidden rescan.
|
||||
- Post-dispatch uncertainty is explicit outcome-unknown, never automatic replay.
|
||||
- K1 power loss revokes the active session without inventing standby.
|
||||
- Wi-Fi loss and WAN loss are distinct: local LAN control may survive WAN loss;
|
||||
route/association loss revokes only dependent host/control evidence.
|
||||
- Browser refresh restores no live local selection and causes no I/O.
|
||||
- Backend restart restores durable audit and safety ledgers, but no live BLE,
|
||||
control or operator intent.
|
||||
- Mac sleep/restart rotates host/runtime authority and rejects late work.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Mode, selection and input result in zero controller calls.
|
||||
- Each Search click issues exactly one scan with duration `6`; no effect, timer,
|
||||
selection or Apply path calls Scan.
|
||||
- Every result keeps the same ordinary **Выбрать** action. Selection retains
|
||||
the card, shows applicable inputs immediately, shows no loader and calls no
|
||||
controller. After an explicit committed scenario reset and its successfully
|
||||
completed successor Scan, this includes the exact UUID used by the retired
|
||||
prior scenario; the row never exposes a reconnect/reopen/Verify CTA.
|
||||
- During unresolved physical recovery, a completed explicit Scan still renders
|
||||
passive BLE evidence but cannot substitute a foreign target for the durable
|
||||
recovery record. Exact recovery remains a separate established-session
|
||||
action outside the cold result list; ordinary **Выбрать** never invokes its
|
||||
reopen or read-only Verify. A new network flow first requires explicit reset
|
||||
and a successor Scan.
|
||||
- Bridge/Direct show SSID/password; Quick Connect does not.
|
||||
- Exactly one primary **Применить** owns the connection request. Its frontend
|
||||
handler calls no Scan/Verify/reopen helper and it permits at most one device
|
||||
mutation. For an exact reset-owned retired UUID, the backend may append only
|
||||
the internal local settlement checkpoint described above before dispatch.
|
||||
A later SCANNING settlement grants only explicit STOP authority and never
|
||||
restarts the reset-owned receiver, camera, writer or acquisition.
|
||||
- Stale/pre-dispatch and unknown/post-dispatch outcomes are visibly distinct.
|
||||
- Applied-but-unready/unknown spends the old Apply and gates ordinary mode,
|
||||
Scan, selection and Apply behind an explicit recovery choice; recommended
|
||||
Verify has only a server-bound backend target and no browser fallback.
|
||||
- A new intent remains possible only through current backend policy. Bridge
|
||||
uses explicit local-only `prepare-select-device`; Quick/Direct use an explicit
|
||||
admitted Scan and latest fresh row; mode change requires `mode_selection`.
|
||||
Each route starts no hidden frontend or mutating continuation and ends in a
|
||||
later fresh Scan/new idempotency Apply. The declared service-owned
|
||||
same-intent read-only bootstrap after the durable ACK is the sole continuation
|
||||
exception and creates no UI action.
|
||||
- The exact Apply REST snapshot with `phase=network_applied` completes the
|
||||
network intent for both `control_not_ready` and `unknown`, without requiring
|
||||
connection-ready or waiting for WebSocket/poll convergence.
|
||||
- A service-owned supervised control bootstrap may continue read-only after
|
||||
that ACK. It performs no BLE/host mutation or retry and creates no frontend
|
||||
Scan/Verify/new-Apply action or blocking Apply loader. Its exact
|
||||
accepted/running state may own one passive settling indicator only.
|
||||
- Operator error copy comes only from an allowlisted public error-code mapping;
|
||||
unknown/raw messages use a canonical secret-free fallback and never render
|
||||
credentials, SSIDs, payloads or stack traces.
|
||||
- No timeout, disconnect, refresh, restart or state update starts a continuation
|
||||
or replays an ended action.
|
||||
- Supervisor, identity pin, network-attempt ledger, physical-command ledger,
|
||||
process/BLE lease and one-use recovery STOP remain authoritative.
|
||||
- The plugin uses shared `Button`, `TextField`, `ActivityIndicator` and
|
||||
`StatusBadge`; contract tests reject raw local controls and literal colors.
|
||||
- Geometry and long-copy tests keep all actions reachable without overlap.
|
||||
- Bridge and Quick Connect retain separate real-hardware acceptance.
|
||||
|
||||
This ADR does not itself declare hardware coverage. The manifest may mark a
|
||||
scenario software-covered only when named executable tests cover the software
|
||||
invariant; remaining K1/macOS/router and Quick Connect gaps stay explicit.
|
||||
Reference in New Issue
Block a user