c7cc92382b
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
12 KiB
12 KiB
id, title, status, wave, depends_on, plan, requirements, acceptance_criteria, system_design
| id | title | status | wave | depends_on | plan | requirements | acceptance_criteria | system_design | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 06-settings-surface | Reachability data layer and settings surface | done | 4 |
|
plan.md |
|
|
|
Task 06: Reachability Data Layer and Settings Surface
Summary
Add the client types, API calls, and store slice for reachability records, and render one on the SSH executor settings page: state, probed host, failure reason and message, consecutive-failure count, and the age of the last probe, with a probe-now action. Localized in all five supported locales.
In scope
SSHReachabilityRecordinlib/types/http-ssh.tsmirroring the backend shape, includingprobing_enabled,probe_interval_seconds(the effective, clamped interval), andupdated_atalongsidechecked_atandlast_success_at.getSSHReachability,getSSHExecutorReachability, andprobeSSHExecutorReachabilityinlib/api/domains/ssh-api.ts, following the existing options-spread-then-required-fields ordering in that file.- Settings slice state keyed by executor id, plus the
executor.reachability.changedWS handler that applies a pushed record. A refetch racing a pushed event is reconciled by keeping whichever payload carries the laterupdated_at, neverchecked_at— a connection-configuration reset clearschecked_at(it isnull) while still advancingupdated_at, so reconciling onchecked_atwould make a reset compare as older than the record it just invalidated and get discarded, leaving a stalereachable/unreachablestate showing for a host the user just re-pointed. Anullupdated_at(the synthesized never-probed shape) always loses to a real record. - A
SSHReachabilityCardrendering state, the host that was actually probed, reason and message whenunreachable, the consecutive-failure count, and the age of the last completed probe; marking the result stale past three intervals; stating plainly that periodic probing is off whenprobing_enabledisfalse, rather than presenting a retained state as current; and offering the probe-now action. - Refetch on mount and on the configured interval (
probe_interval_seconds, not a constant) while the card is open, because a steady host publishes nothing andchecked_atwould otherwise age in place. No timer runs at all whenprobing_enabledisfalse— the card fetches once on open and again after an immediate probe; reading a0effective interval literally as a cadence would be a zero-delay refetch loop against a record known not to be changing. - A failed load reports that reachability is not known. It must never render
as
unreachable. - Mobile-viewport layout, and state exposed to assistive technology as text rather than by color alone.
- Copy through
t()inexecutors.jsonacrossen,pt-pt,zh-cn,zh-hk,zh-tw, with the failure reason rendered as translated copy rather than the raw token. Usepnpm run i18n:zh-hantfor the Traditional Chinese pair.
Out of scope
- The pre-launch warning. Task 07 owns it. The task-card indicator is
deferred; see the requirement document's
## Out of scope. - Any backend file.
- Changing the existing connection, sessions, agent-readiness, or task-dir-reclamation cards beyond mounting the new one.
- Any history, trend, or sparkline. One record per executor is all that exists.
Acceptance
- The card renders every field the settings surface owes for each of the three states, and a record older than three intervals renders as stale rather than current.
- A pushed
executor.reachability.changedupdates the card without a reload and without the card polling for it. - A pushed record with a
nullchecked_atbut a newerupdated_at(a reset) replaces a storedreachable/unreachablerecord rather than being discarded as stale. - With
probing_enabledfalse the card says periodic probing is off, runs no refresh timer, and a load failure says reachability is not known — neither renders asunreachable.
Verification
# From apps/ (once per fresh worktree):
pnpm install --frozen-lockfile
# From apps/web:
pnpm vitest run components/settings/ssh-reachability-card.test.tsx lib/state/slices/settings/settings-slice.test.ts lib/api/domains/ssh-api.test.ts
pnpm run typecheck
pnpm run lint
pnpm run i18n:check
pnpm run i18n:ratchet
Files likely touched
apps/web/lib/types/http-ssh.tsapps/web/lib/api/domains/ssh-api.tsapps/web/lib/state/slices/settings/types.tsapps/web/lib/state/slices/settings/settings-slice.tsapps/web/lib/state/slices/settings/settings-slice.test.tsapps/web/components/settings/ssh-reachability-card.tsxapps/web/components/settings/ssh-reachability-card.test.tsxapps/web/components/settings/ssh-settings.tsxapps/web/src/locales/{en,pt-pt,zh-cn,zh-hk,zh-tw}/executors.json
Dependencies
Task 04. The record shape, the probing_enabled flag, and the change action
are that task's contract.
Risks
- Translations gate the build:
check-i18n-keys.mjsfails on a missing key, an extra key, a dropped placeholder, or a value left identical to English. The six reason tokens plus the state labels are twelve-plus keys across five locales, so this is the largest single source of build failure in the task. - The staleness threshold and the refresh cadence both depend on the effective
interval. The record carries it as
probe_interval_seconds; use that value rather than a constant, or both rules silently drift from the operator's configuration. - Reconciling on
checked_atinstead ofupdated_atis the one regression that passes every test written against a normal probe sequence and only fails on a reset, because a reset is the single write that advancesupdated_atwithout advancingchecked_at. Write the reconciliation test around a reset payload specifically, not just a newer/older probe pair. i18next/no-literal-stringis a lint error repo-wide andcheck-nonjsx-copy.mjsscans non-JSX positions by exclusion. A reason-to-copy lookup table written as a plain.tsmap is exactly the shape that rule catches.
Parallelism
sequential
Inputs
- System design, sections API and event contracts and Frontend components.
apps/web/AGENTS.mdfor the store-slice and WS-hook conventions and the "never fetch data directly in components" rule.components/settings/ssh-agent-readiness-card.tsxas the nearest existing card shape.docs/i18n.mdfor the five-locale workflow.
Results
Implemented under strict /tdd (RED confirmed before every GREEN
implementation):
SSHReachabilityRecord(+SSHReachabilityState/SSHReachabilityReason) added toapps/web/lib/types/http-ssh.ts, mirroring the backendreachability.RecordDTOwire shape used by both the GET responses and theexecutor.reachability.changedWS payload.getSSHReachability,getSSHExecutorReachability, andprobeSSHExecutorReachabilityadded tolib/api/domains/ssh-api.ts, covered bylib/api/domains/ssh-api.test.ts(GET list, GET single with executor-id URL-encoding, POST probe-now).- A
sshReachability: { byExecutorId }slice in the settings store (lib/state/slices/settings/{types,settings-slice}.ts), reconciled onupdated_at— neverchecked_at— viasetSSHReachability. Anullupdated_at(the synthesized never-probed placeholder) always loses to a record that carries a real timestamp; a first write for an executor is always applied. Covered by 6 tests insettings-slice.test.ts, including the reset case (checked_at: null, newerupdated_at) that achecked_at-based reconciliation would silently discard. executor.reachability.changedregistered inBackendMessageMap(lib/types/backend.ts) and wired inlib/ws/handlers/executors.tsto callsetSSHReachabilitydirectly, so a pushed event and a card's own fetch share one reconciliation path.- Adding
sshReachability/setSSHReachabilityrequired updating both the settings slice's own type file and the hand-writtenAppStatetype inlib/state/app-state-types.ts(which does not derive from the slice types by intersection) — missed on the first pass, caught bypnpm run typecheck. SSHReachabilityCard(components/settings/ssh-reachability-card.tsx), mounted inSSHExecutorView(app/settings/executors/ssh/[executorId]/page.tsx) betweenSSHConnectionCardandSSHSessionsCard, and exported fromssh-settings.tsx. Fetches on mount through the store (never into component-local-only state), so a live WS push and its own fetch/probe-now results converge on the same rendered state. Renders state, probed host, reason + message whenunreachable(reason via a translated lookup, never the raw token), consecutive-failure count, and the age of the last completed and last successful probe. Marks a record stale past three probe intervals (isReachabilityStale, a pure exported helper, covered by 2 direct unit tests plus integration coverage through the card). Refetches on the record's ownprobe_interval_seconds(never a constant) while probing is enabled, and arms no timer at all whenprobing_enabledis false. A load failure with no prior record renders "not known" copy, neverunreachable. 12 tests inssh-reachability-card.test.tsx, split across 4describeblocks (rendering, live updates, refresh cadence, errors/probe-now) to stay under the per-function line lint budget, cover: per-state field rendering, staleness on/off, the live pushed-event update (via aPushReachabilitytest helper calling the same store action the WS handler uses — the same pattern as the existingarchive-confirmation-settings.test.tsx"remote update" helper), the reset-over-stored-record case, the probing-off no-timer guarantee (asetIntervalspy, filtering out React Testing Library's own internal 50ms polling interval), the enabled-cadence timer args, the load-failure "not known" fallback, and the probe-now round trip.- Twelve reachability keys (state labels, six reason tokens, staleness,
probing-off, probe-now, host/age/failure-count copy, load-failure copy)
added to
src/locales/en/executors.json, hand-translated intopt-ptandzh-cn, and propagated tozh-hk/zh-twviapnpm run i18n:zh-hant(which also incidentally re-derives unrelated already-translatedzh-hk/zh-tw/pseudocatalog entries outside theexecutorsnamespace from currentzh-cn/ensource; those out-of-scope regenerated files were reverted withgit checkout --before committing, keeping this commit scoped to theexecutorsnamespace this task owns). - Followed the
ssh-agent-readiness-card.tsxprecedent for the card shape (auseReachabilityStatehook separating fetch/refresh/probe-now from a thin view component) and thessh-connection-card.test.tsx/archive-confirmation-settings.test.tsxprecedent for real React Testing Library component coverage (StateProvider+useAppStoreApi) rather than pure-helper-only tests — justified here because the acceptance criteria are about store-integration behavior (live WS update without reload, timer arm/no-arm, reconciliation-through-render) that a pure helper test cannot exercise.
Verification (all from apps/web, after pnpm install --frozen-lockfile
from apps/):
pnpm vitest run components/settings/ssh-reachability-card.test.tsx lib/state/slices/settings/settings-slice.test.ts lib/api/domains/ssh-api.test.ts
# 3 files, 32 tests passed
pnpm run typecheck
# clean
pnpm run lint
# clean (0 warnings; fixed sonarjs/no-duplicate-string, max-lines-per-function,
# and no-nested-ternary findings during the refactor step)
pnpm run i18n:check
# ✓ all 6 sub-checks pass; pt-pt/zh-cn/zh-hk/zh-tw complete, pseudo in sync
# (135 pre-existing unrelated orphan-key warnings, unchanged by this task)
pnpm run i18n:ratchet
# ✓ 0 added + 9 modified file(s) clean; guard allowlist intact