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:
DCCONSTRUCTIONS
2026-08-14 14:57:50 +03:00
parent aff331082f
commit 0ca7316a24
157 changed files with 152962 additions and 4036 deletions
+393
View File
@@ -0,0 +1,393 @@
# K1 connection lifecycle and recovery runbook
Canonical model: [`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
This runbook is the operator-facing projection of that model. Technical terms in
the internal-safety sections are engineering evidence; they are not wizard copy.
## One operator wizard
The connection surface is one progressive wizard, not a recovery dashboard.
Its only model-bearing heading is **Подключение XGRIDS LixelKity K1**. Inside
the wizard the two step names are exactly **Подключение** and **Сеть**.
### Cold entry
On a clean cold entry show only:
- the connection-mode selector;
- Step 01 **Подключение** with the explicit Bluetooth search action.
Historical K1 identity never adds a reconnect choice to cold entry. If local
session ownership or an older connection scenario exists, one explicit
`reset_scenario` CAS first closes only that local scenario. Only after the reset
is accepted does clean Step 01 expose **Найти по Bluetooth** as a separate
click; reset never starts Scan itself.
Do not render Step 02 yet and do not start discovery automatically. The mode
selector and same-mode **Подключить новый K1** escape remain available through
every other lifecycle state. The first gesture may supersede another pending
local action. While that one bounded scenario reset itself owns `mode`, all
three reset entry points show pending and dispatch no B intent. Each accepted
gesture sends one idempotent local `reset_scenario` CAS: it queues behind old
local lifecycle ownership, seals retained receiver/camera/control resources,
invalidates candidates/drafts/credentials and retires the old physical lineage
without resolving its outcome. It sends no BLE, device or host network command,
MQTT publish, Verify, provisioning, START, STOP or automatic Scan. Opening and
polling the surface still do nothing. Scan, Verify, provisioning and START
retain separate backend gates.
The top-right refresh-shaped utility is the same explicit emergency reset, not
a passive state refresh. Its accessible label is **Сбросить подключение**; while
the request owns the current action it reads **Сбрасываем подключение** and
does not dispatch a second reset until that bounded request settles. It remains
available to supersede any other local action. The accepted revision always
returns the new local scenario and even a dirty browser selector to canonical
**Bridge**, clears the old browser/backend presentation and source owners, and
leaves Scan as a separate click. It performs no hidden Scan, Verify, Connect,
START, STOP, BLE or network write and never substitutes a passive `state.read`
for the reset mutation. After settlement the product surface contains no prior
UUID, result count, **Повторить поиск**, reconnect error, selected device,
credentials or recovery card; it returns to **Найти по Bluetooth**. Late
Scan/Verify settlements from the retired scenario cannot repopulate it.
The retained reset marker fences only work that belonged to the retired
scenario. A newly correlated post-reset network attempt that fails or has an
unknown outcome must immediately render its current recovery/error surface;
reload must neither hide that new failure nor resurrect the prior prompt.
### Step 01 — Подключение
Step 01 **Подключение** is visible immediately.
1. Bluetooth discovery starts only after the operator presses the search
action.
2. For the full bounded search, show an activity indicator and a visible
seconds countdown in the same step.
3. After search completes, show the result count or the empty result. Every
connectable result row keeps the same one enabled **Выбрать** action,
including an exact UUID used in an earlier scenario. That action is
local-only and never invokes reopen or Verify. Fresh results never render a
reconnect CTA, a disabled competing primary or an old/new-device decision.
A successful admitted Scan first settles only the reset marker whose
id/revision/mode it captured at action entry, recording the later admitted
discovery generation while retaining the marker for idempotent reset replay.
Failed/cancelled Scan and an older Scan racing a newer pending reset leave
the marker active and do not expose stale recovery authority.
4. After ordinary **Выбрать**, retain the chosen device card and reveal only
the applicable local draft inputs. Selection itself has no loader and makes
no controller call.
5. Only an authoritative successful connection outcome renders Step 01 green
as **Подключение установлено** and advances the normal connection flow.
The wizard never labels a candidate as saved, original, retired or physically
ambiguous and never exposes ledger, CAS, retirement, reopen or reconnect
terminology in the search list. Exact recovery belongs only to a previously
established session after an actual interruption.
### Step 02 — Сеть
Step 02 **Сеть** exists only after Step 01 has a confirmed green connection.
- If the selected device is already usable on the chosen connection path,
show the network result without asking for credentials.
- If backend policy has safely admitted explicit network setup, show the exact
retained device context and SSID/password fields here, never beside the
candidate list.
- One explicit submit owns any exact hidden revalidation and at most one
reviewed network write.
- **Изменить сеть** belongs only to this step and starts no Bluetooth search
when its form opens.
- A stale tab, changed runtime/binding or policy denial fails before a write and
never exposes a foreign candidate.
## Ordinary selection
The ordinary **Выбрать** action is presentation simplification, not relaxed
safety. It creates only a browser-local candidate draft and performs no
controller I/O. It never selects an internal recovery path:
- an ordinary fresh candidate becomes only the selected local draft;
- an exact prior candidate uses the same **Выбрать** action as every row;
- no candidate selection retires old authority, reopens a ledger record, calls
Verify, connects GATT, scans again or changes topology or the device;
- a foreign, stale, non-connectable or policy-denied candidate remains
unavailable and changes no topology, ledger or device.
After an explicit committed scenario reset, only its successfully completed
successor Scan may make an exact previously retired transport eligible for a
new network draft. Apply still captures that exact current-generation handle,
validates the live GATT baseline and crosses at most one reviewed write edge.
Selection performs no recovery action. At the final Apply boundary, the backend
may append one exact request-bound local reopen checkpoint after network
PREPARED and before write dispatch. That checkpoint preserves physical
retirement/original-outcome audit, performs no device I/O and authorizes no
START or STOP. The same applied intent then uses fresh DeviceInfo plus a
non-retained DeviceStatus to settle READY as standby or identity-bound SCANNING as
active, without a visible Verify step or command replay. In the SCANNING case
it materializes only explicit STOP authority; it does not restart the retired
receiver, camera, evidence writer or acquisition.
### Exact internal recovery for an established session
For an exact target belonging to a previously established session with one
active retirement,
`physical-command.reopen-retired-reconciliation` requires:
- `operator_confirmed=true`, bound to a separately explicit session-recovery
action outside cold entry and the Bluetooth result list;
- a stable `reopening_id` and reason
`device-returned-for-explicit-reconciliation`;
- exact `expected_snapshot_runtime_id`, `expected_revision`,
`expected_retirement_id`, `expected_transport_ref` and
`expected_discovery_generation` CAS;
- a current connectable candidate and safe lifecycle/process ownership.
The transaction changes only the local durable ledger. It appends reopen audit,
preserves the retirement and unknown command outcome as history, restores the
original unresolved `dispatching` or `observing` stage and removes only that
retirement's active deny. It performs zero BLE, Wi-Fi, MQTT, DeviceConfig,
ModelingStatus, workspace, project, START or STOP I/O and starts no Scan.
The same still-current session-recovery action may then own one exact read-only
Verify. Ordinary **Выбрать** never invokes either half. Recovery never replays
historical START/STOP and never silently provisions:
- fresh non-retained READY resolves standby;
- fresh exact same-project SCANNING resolves active and permits only the
separately guarded stop path;
- identity, GATT, CAS, route/control or policy failure leaves the outcome
unknown and ends the established-session recovery without entering the new
connection wizard.
If the action response is lost, refreshed state may continue the same click
only when it proves that exact `reopening_id` audit was committed and every
original runtime, candidate and authority fence still matches. A second tab,
new discovery generation, new retirement or different reopening identity cannot
inherit the continuation.
### Internal retirement
`physical-command.retire-unavailable` is a local durable primitive for an
unresolved target that is truly unavailable or replaced. It may run only from
its separately confirmed recovery/reset path, never from ordinary candidate
selection, UI entry, polling or a timer. Admission requires explicit
confirmation, stable `retirement_id` and exact backend runtime,
operation, revision and transport CAS while every local owner is safe.
Retirement preserves the complete old attempt and unknown command outcome,
activates an exact-transport deny and performs zero device I/O or automatic
discovery. Retirement history remains durable even if an exact later recovery
action uses the reopen transaction. The wizard exposes no retirement
transaction or historical label. Any plain-language exact recovery CTA belongs
only to the established-session surface when backend authority permits it.
Current FW 3.0.2 BLE `7f02` does not expose stable DeviceInfo identity. The same
hardware under a new CoreBluetooth UUID cannot be recognized before DeviceInfo
becomes available. This remains an explicit protocol/hardware acceptance gap;
the wizard must not speculate.
## Session and freshness rules
A scan result is an unselected presence candidate owned by the latest explicit
scan generation. Wall-clock age does not remove its row while the operator is
reading or completing the form. A successor Scan, explicit scenario reset,
runtime-owner teardown or proven exact-target GATT failure invalidates it. The
row itself is never network authority: Apply still requires the exact captured
CoreBluetooth object and live GATT validation before any write.
The selected session ends on proven disconnect, explicit lifecycle stop, a
committed network transition, selection of another device, backend restart or
proven native cleanup. A later connection always requires an explicit search
and **Выбрать**. Polling can update presentation but starts neither operation.
Bridge, Quick Connect and Direct Connect are separate topologies. In
any state, changing the mode or choosing another K1 in the same mode sends one
local scenario-reset CAS. It can wait for and supersede live/recovery ownership,
seal retained local producers and retire unresolved old lineage, but performs
zero device/host I/O and starts no Scan. Scan, Verify, provisioning and START
remain independently fenced until an explicit candidate intent crosses its
reviewed transition. No old host route, endpoint, control, data or BLE authority
crosses a committed reset boundary.
## Active scanning: transient host-path recovery
This is the sole automatic read-only rebind exception. It exists only after
Mission Core itself has a composite-confirmed START and still owns the exact
acquisition/runtime/device/connection/evidence lineage. It does not apply on a
cold connection screen, after backend restart, to an external SCANNING K1 or to
an unresolved/foreign START.
When the Mac loses Wi-Fi/route or the data socket while that acquisition is
running, the active scanning pane shows a neutral spinner and
**Восстанавливаем соединение** with attempt/elapsed time. Do not show a red
terminal operation banner for the expected late failure of the superseded old
control socket. Keep the acquisition and evidence session owned while the
backend retries exact route/TCP and inspection-only DeviceInfo/status proof.
The recovery loop never sends BLE, changes Wi-Fi, writes DeviceConfig, repeats
START or sends STOP. Outcomes are:
- exact same-device/same-project initialized `SCANNING`: silently resume the
point stream/control binding and, when necessary, CAS-restart the dead or
stalled acquisition-owned right-camera FFmpeg epoch;
- fresh `READY`: interrupt/seal host-owned acquisition resources truthfully,
without STOP;
- fresh `SCAN_OVER`: persist cessation, interrupt/seal locally and retain a
read-only `awaiting READY` fence that denies a new START;
- wrong identity/same IP, changed lineage or failed camera CAS: remain blocked
for explicit operator handling; and
- device/system fault or unsafe status: show a truthful terminal fault, with no
command retry.
While state is `reconnecting` or `blocked`, expose **Завершить локально**. The
action `acquisition.force-finish-local` requires the current snapshot runtime,
acquisition id/state revision, recovery generation, producer generation match,
an idempotency key and explicit confirmation. It cancels recovery first, then
seals only local receiver/camera/control/perception owners. It preserves the
physical START ledger and sends no STOP. If a connection-mode reset races this
action, the shared lifecycle gate makes cleanup idempotent; the loser cannot
overwrite the new mode or revive the old acquisition.
If receiver, camera or evidence sealing fails, the recovery generation is
still irrevocably cancelled first. The force-finish operation ends with a
visible `local-cleanup-failed` result whose retryability applies only to local
finalization; the terminal acquisition retains `cleanup_pending` and blocks a
replacement session. A later explicit local stop or exact connection-scenario
reset may retry those host resources. It must not retry START, STOP, BLE or a
network write, and a late success from the retired recovery generation remains
fenced.
## Failure matrix
| Event | Product result | Operator path |
| --- | --- | --- |
| Cold entry | Mode plus Step 01 and explicit Scan; zero device I/O before Scan | Start search explicitly |
| Disconnected/idle mode or same-mode new-device request with unresolved durable physical history | Local session/audit lineage is retired under one reset CAS; zero device/host I/O and no automatic Scan | Start the clean Step 01 search explicitly; the old physical outcome remains auditable |
| Mode reset while live, reconnecting or terminal cleanup still owns local sources | Reset supersedes recovery and locally seals receiver/camera/control; previous K1 may still scan | Wait for the bounded local cleanup or retry the same reset if local sealing fails |
| Search running | Step 01 spinner and visible countdown | Wait or let the bounded search end |
| Search finds no candidates | Step 01 reports no matches | Repeat search explicitly |
| Search finds one or many candidates | Every connectable row has one enabled **Выбрать**, including the exact prior UUID | Select one row; no reconnect or recovery action appears in search results |
| Wall-clock time passes after Scan before selection | Latest-generation rows remain stable; no operation starts | Select normally; exact capture and live GATT will gate Apply |
| A new Scan/reset/runtime teardown or exact-target GATT failure invalidates the generation | Old rows disappear or the attempted action fails cleanly before mutation | Run one explicit new search if needed |
| Selection is rejected by identity, GATT, CAS, lifecycle or safety policy | Loader ends; nothing changed; Step 02 remains absent | **Повторить** or **Выбрать другое** |
| Selection completes exact device connection | Step 01 turns green | Continue in Step 02 **Сеть** |
| Network setup is safely required | Credentials appear only in Step 02 | Submit once |
| Network write becomes ambiguous after dispatch | Attempt ends unknown; no replay | Wait for cleanup, then create a distinct explicit attempt |
| Device powers off or BLE disconnects | Live selection and authority revoke after proof | Search and select explicitly after the device is available |
| Router, Mac Wi-Fi or MQTT control is lost while idle/pre-START | Host/control authority revokes; data may remain evidence only | Restore reachability, then use the same wizard flow |
| Mac Wi-Fi/route is briefly lost during one composite-confirmed owned acquisition | Active pane remains neutral **Восстанавливаем соединение**; no START/STOP/network retry | Wait for exact automatic read-only rebind or press **Завершить локально** |
| Active recovery returns READY or SCAN_OVER | Local receiver/camera seal without STOP; SCAN_OVER remains fenced until fresh READY | Start another scenario only after backend policy reports it safe |
| Active recovery sees another K1 on the same IP or changed lineage | Recovery blocks fail-closed; no camera/data resurrection | Finish locally or explicitly choose/reset connection scenario |
| A physical START/STOP edge is unresolved | Mutation stays fenced; no technical wizard ceremony | Search/select remains explicit; backend admits only a safe exact path |
| Exact actively retired UUID is present after committed reset and successor Scan | The row exposes the same enabled **Выбрать** as every candidate | Select locally; Apply remains exact-handle/live-GATT gated and may append one internal local settlement checkpoint before its sole write; audit remains append-only and START/STOP stay denied until fresh read-only classification |
| Another candidate is selected while old authority is unavailable and no reset-owned new scenario exists | Selection stays local and Apply remains denied | Start an explicit new connection scenario, then Scan and select again |
| Browser refresh or backend restart | No automatic operation and no restored live selection | Begin from the cold progressive wizard |
## Physical START/STOP safety remains separate
The simplified wizard never weakens physical-command safety:
- loss of control does not prove that K1 stopped recording;
- START and STOP are never replayed automatically;
- local receiver/camera/ingress cleanup is not physical STOP;
- an ambiguous post-dispatch command remains unknown until exact fresh proof;
- read-only recovery is pinned to the durable transport, identity/profile,
host epoch and project;
- each observation publishes exactly one DeviceInfo request and may classify
only a fresh non-retained DeviceStatus after that barrier;
- READY records cessation without inventing a successful STOP;
- SCAN_OVER records cessation without inventing STOP, but keeps a durable
read-only fence until a later fresh unbound READY observation;
- exact same-project SCANNING may mint one single-use, separately confirmed STOP
checkpoint; it does not send STOP automatically;
- a wrong transport/device/project changes no topology or ledger state;
- accepted STOP without READY or SCAN_STOPPING by the backend deadline closes
only host-owned resources, yields `timed_out` / `standby-unknown`, preserves
the unresolved ledger and keeps mutation fenced.
Engineering logs and state APIs retain these distinctions. The connection
wizard projects only the ordinary progressive flow and a non-technical terminal
selection result.
## No automatic action rule
None of these events may scan, select, reconnect, Verify, provision, START or
STOP:
- opening or resizing the connection surface;
- backend event delivery or state polling;
- an acknowledged scenario reset (it may perform only its explicit local
retirement, never any listed device/network action or automatic Scan);
- candidate list refresh after an ended search;
- browser refresh, sleep/wake or backend restart;
- timeout, disconnect or a historical audit record.
The only exception is the service-owned active-stream read-only rebind above.
It is triggered by the already-owned receiver's transport loss, not UI entry or
polling, and is limited to route/TCP, DeviceInfo/status inspection, receiver
resubscribe and exact local camera-epoch restart. It never performs discovery,
provisioning, START, STOP or any device/network write.
Only the currently pressed search, distinct exact recovery CTA, network submit
or separately guarded acquisition control may own corresponding I/O. Ordinary
**Выбрать** owns only a browser-local draft and never owns a loader. Every
loader belongs to the explicit action that created it and ends with it.
## Hardware acceptance order
Software tests do not replace a real K1/macOS/router run. Accept sequentially:
1. Open cold and prove mode plus Step 01 and its explicit Scan action are
visible, while Step 02 is absent and no discovery starts automatically. With both empty and
unresolved durable physical history, change the mode and prove one local
reset CAS, zero device/host calls and no automatic Scan. Repeat from active,
reconnecting and terminal `cleanup_pending` states; prove local sources are
sealed, the old K1 is not claimed stopped, and a local cleanup failure leaves
the exact reset retryable. With an exact prior connection, prove cold entry
contains no historical reconnect prompt; after one reset CAS and zero Scan,
a separate clean **Найти по Bluetooth** action remains clean after reload.
2. Start discovery and prove the spinner and seconds countdown remain visible
for the bounded search, then the exact result count appears.
3. With multiple advertisements, prove every ordinary connectable row keeps
exactly one enabled **Выбрать** action and none auto-selects or auto-connects.
Repeat with the exact prior UUID after reset and prove it has the same
**Выбрать** action, with no reconnect/reopen/Verify path.
4. Select a Bridge device, including that prior UUID, and prove the card remains visible through
**Подключение…**, then Step 01 turns green before Step 02 **Сеть** appears.
5. Prove network fields never coexist with candidate rows, and one explicit
submit owns at most one write.
6. Wait beyond the legacy candidate TTL and prove both the latest-generation
unselected rows and an admitted selected session remain stable; then prove a
missing exact handle/live GATT failure blocks Apply before any write.
7. Exercise identity, GATT, stale-CAS, lifecycle-busy, disconnect and power-off
failures; each ends the loader, leaves Step 02 absent and offers only ordinary
retry/choose-another copy.
8. Retire an unresolved target in controlled fault injection, perform one
scenario reset and rediscover its exact UUID in the successor Scan. Prove its
sole action is **Выбрать**, selection performs no I/O and Step 02 appears
immediately. Apply once and prove exact current-generation handle capture,
live GATT baseline, exactly one request-bound append-only physical reopen
checkpoint and at most one network write. The original retirement/outcome
audit remains immutable; the service-owned continuation uses only DeviceInfo
and non-retained status, with zero START/STOP and no browser Verify. Inject
failed and outcome-unknown network results; each current error/recovery
surface remains visible after reload.
9. Try a different device while old authority is unavailable and prove its
ordinary selection triggers no hidden retirement/reopen/Verify and cannot
bypass the durable target.
10. Prove no row labels a device saved/original/retired, says
**Переподключиться**, or exposes physical-state/ledger terminology. The model name
appears only in the top heading; step names remain **Подключение / Сеть**.
11. During a composite-confirmed live acquisition, remove host Wi-Fi for longer
than the old control keepalive and restore it. Prove neutral reconnecting,
same-lineage SCANNING resume, raw-writer continuity, exact camera epoch
restart when stalled, and zero START/STOP/BLE/network mutation. Repeat with
READY, SCAN_OVER, wrong identity and permanent loss plus
**Завершить локально**.
12. Repeat idle/pre-START Bridge network loss, Mac Wi-Fi switch, sleep/wake,
hard K1 power loss, STOP deadline and backend restart; prove zero automatic
command or retry outside the sole active-stream exception.
13. Repeat the entire acceptance separately for Quick Connect before claiming
Quick coverage.
The current software contract is not real-hardware acceptance. The acceptance
manifest lists executable coverage and the remaining Bridge/Quick field gaps.