Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
310 lines
17 KiB
Markdown
310 lines
17 KiB
Markdown
# 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.
|