Skip to content

Panel P4.1: check-in loop — idempotent check-in, station UI, launch ceremony - #77

Merged
thevladbog merged 26 commits into
mainfrom
panel/p4.1-checkin-loop
Jul 18, 2026
Merged

Panel P4.1: check-in loop — idempotent check-in, station UI, launch ceremony#77
thevladbog merged 26 commits into
mainfrom
panel/p4.1-checkin-loop

Conversation

@thevladbog

@thevladbog thevladbog commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Server-side check-in settings, check-in-station registration/heartbeat, and a DB-idempotent single-scan check-in endpoint (guarded UPDATE, zero-double-checkin at the source) with undo and a durable checkin_actions feed.
  • Check-in station UI: three scan-input modes (wedge, agent scanner, manual search fallback), a split-rail layout with a live verdict panel + recent-scans rail (reprint/undo/details), and a degraded/offline mode (banner + read-only search, no offline write queue by design).
  • Launch ceremony: event/station/zone confirm, editable check-in settings, agent printer check + test badge, frontend-only readiness lock, station registration → navigation to the station.
  • Printing reuses P3.2's usePrintBadge; verdict rendering reuses @idento/ui's existing verdict vocabulary (no new colors); physical-output dialogs follow the established block-dismissal-while-sending convention.

Test plan

  • Backend: OPENAPI_COVERAGE=1 go test ./... -count=1 (416 tests) + golangci-lint run ./internal/... — clean
  • Panel: npm run typecheck -w panel && npm test -w panel (1072 tests) + npx eslint . + npm run build -w panel — clean
  • packages/ui untouched-green (132 tests)
  • npm run generate:api -w panel — zero schema drift
  • router.tsx diff is purely additive (only the two new top-level routes; /register guards byte-for-byte unchanged)
  • web/ diff is empty (frozen for this phase)
  • i18n EN/RU key parity (63 new keys, real translations)
  • 13 tasks each independently reviewed (spec compliance + code quality), 5 fix-and-re-review rounds along the way
  • Final whole-branch review (cross-task integration pass) — 2 Important findings fixed and re-verified (inert manual_search_enabled setting wired up; implicit auto-print no longer mislabeled as a reprint in the feed)
  • Manual: run the check-in loop end-to-end against a real Zebra printer + physical scanner (cannot be verified in this environment)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an end-to-end event check-in workflow: check-in settings, station register/heartbeat, attendee check-in + undo, and a recent activity rail with Reprint/Undo.
    • Introduced scanner/manual/wedge scan inputs and a printer test + launch ceremony flow for starting check-in.
    • Extended printed marking to optionally include event/station context for improved reprint attribution.
  • Bug Fixes
    • Improved degraded/offline behavior and ensured printed-count updates remain reliable even when reprint metadata logging fails.
  • Documentation
    • Updated API contracts and English/Russian UI translations for the full check-in station and recent activity experience.

CI Bot and others added 20 commits July 17, 2026 12:13
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
P4.1 Task 1: adds events.checkin_settings JSONB plus the checkin_stations
and checkin_actions tables Tasks 2-3 will use, and the GET/PUT
/api/events/{id}/checkin-settings endpoints (openapi-first, contract- and
pgxmock-tested) for the per-event check-in station configuration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
P4.1 Task 2: builds POST/GET /api/events/{event_id}/checkin-stations and
POST .../checkin-stations/{id}/heartbeat on Task 1's checkin_stations
table. Registration is an upsert keyed on (event_id, name) — re-registering
the same name updates its zone binding and refreshes last_seen_at instead
of erroring or duplicating; heartbeat 0-row (unknown/foreign station id)
maps to a store.ErrCheckinStationNotFound sentinel -> 404; zone_id, when
present, must belong to the same event (400 otherwise). openapi-first with
kin-openapi contract tests (incl. the upsert same-id/zone-updated proof)
and pgxmock tests asserting the exact ON CONFLICT/guarded-UPDATE SQL text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e_id

GetEventZoneByID surfaces raw pgx.ErrNoRows for a missing zone rather than
(nil, nil), so RegisterCheckinStation's err != nil check was catching a
genuinely non-existent zone_id before it reached the zone==nil 400 branch,
returning 500 instead. Fold pgx.ErrNoRows into the not-found path (matching
the checkins_override.go / checkins_batch.go precedent) while still
surfacing real DB errors as 500. Also fixes the contract test's fakeStore
stub, which returned (nil, nil) for an unknown zone and therefore never
exercised the real no-rows code path.
P4.1 Task 3: the phase's core zero-double-checkin endpoint. store.CheckInAttendee
performs a guarded UPDATE (WHERE checkin_status = false) in one transaction,
falling back to a joined SELECT to distinguish "already checked in" (original
metadata preserved) from a genuinely missing attendee; store.UndoCheckin mirrors
this for clearing a check-in (idempotent, no-op on an already-clear attendee);
store.GetCheckinActions backs the recent-scans feed. The handler short-circuits
blocked attendees to a distinct "blocked" outcome before ever attempting a write,
and validates a caller-supplied station_id (400 if foreign) via a new
GetCheckinStationByID lookup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract a shared InsertCheckinAction store method (interface.go/pg_store.go)
consumed by CheckInAttendee/UndoCheckin (tx-scoped, unchanged behavior) and
by the printed-counter endpoint (pool-scoped, new): POST
/api/attendees/{attendee_id}/printed now accepts an optional
{event_id?, station_id?} body and, when event_id is present, logs a
'reprint' checkin_actions row after the counter increment commits. Absent
body stays counter-only (back-compat for the badge-editor's bulk print).
Regenerate panel/src/shared/api/schema.d.ts (also picks up Tasks 1-3's
deferred schema additions).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ndee's event

MarkAttendeePrinted discarded requireAttendeeOwnership's *models.Attendee
return value and trusted the reprint body's bare event_id/station_id as-is,
letting an authenticated caller who owns the attendee log a reprint row
into an arbitrary other event's/tenant's checkin_actions feed. Now the
attendee's own EventID is the source of truth: a body event_id is validated
against it (400 "Attendee does not belong to this event" on mismatch, same
wording as StationCheckin/UndoCheckin/BadgeZPL), and a present station_id is
checked via the existing resolveCheckinStation helper before any insert is
attempted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Panel data layer for P4.1's check-in loop (Task 5): CheckinSettings type +
DEFAULT_CHECKIN_SETTINGS + parseCheckinSettings (defensive null/partial/
out-of-range narrowing, mirrors badge/templateTypes.ts's parser shape) in
settingsTypes.ts, and the nine $api hooks + three query-key helpers
(useCheckinSettings/useSaveCheckinSettings, useCheckinStations/
useRegisterStation/useStationHeartbeat, useCheckinActions, useStationCheckin/
useUndoCheckin, CHECKIN_SETTINGS_KEY/CHECKIN_STATIONS_KEY/CHECKIN_ACTIONS_KEY)
in hooks.ts, consuming Tasks 1-4's already-regenerated schema.d.ts. Mutation
hygiene: useStationCheckin/useUndoCheckin unconditionally invalidate both
CHECKIN_ACTIONS_KEY and attendees/hooks.ts' ATTENDEES_LIST_KEY.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…elected value

useSaveCheckinSettings' onSuccess was writing the already-select-ed
CheckinSettings object into the CHECKIN_SETTINGS_KEY cache slot instead
of the raw {settings} envelope useCheckinSettings' own select expects
there. Any mounted useCheckinSettings observer had its select re-run
against the wrong shape the instant a save succeeded, flashing
DEFAULT_CHECKIN_SETTINGS until the follow-up invalidateQueries refetch
resolved. Pass the mutation's raw data through unmodified, matching the
useSaveTemplate.ts precedent.

Adds a regression test that renders both hooks together and asserts the
read hook's data immediately after the save resolves, using a delayed
server.use() override plus explicit destructuring in the render
callback so the assertion isn't masked by TanStack Query's
tracked-properties re-render optimization or by the refetch completing
before the check runs.
Maps the check-in station's four outcomes onto @idento/ui's existing
verdict vocabulary (checked_in->allowed, already_checked_in->
already_checked_in, blocked->no_access, not_found->not_registered) and
adds useCheckinFlow, the state machine that resolves a scanned code or
manually-picked attendee, fires the idempotent check-in mutation, and
prints ONLY on a checked_in outcome when print_on_checkin is enabled --
a print failure surfaces without undoing the check-in. Also extends
P3.2's PrintAttendeeOptions with an optional printContext forwarded to
the /printed body, preserving back-compat (no body key when absent).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds useScanInput (wedge hidden-input + Enter-to-emit, scanner 200ms
agent-poll with {code,time} dedup + degraded flag, manual no-op) and
ScanInput (mode-appropriate affordance plus the always-present debounced
manual search box backed by useAttendeesPage). Extends agentClient with
getLastScan/clearLastScan against the agent's existing /scan/last and
/scan/clear contract (agent/openapi.yaml — no gap found), and adds an
optional `enabled` escape hatch to useAttendeesPage so the manual search
doesn't fetch the roster before the operator types anything.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds eventCheckinRoute as a top-level protected route (sibling of
eventWorkspaceRoute, not one of its children) so /events/$eventId/checkin
renders StationPage rail-less, escaping the workspace shell. StationPage
wires Task 5's settings, Task 6's verdict flow, and Task 7's scan input
into a near-fullscreen split layout (top bar, VerdictCard, a placeholder
rail region for Task 9); missing/invalid ?station= redirects to the launch
ceremony. VerdictCard renders the four outcomes through @idento/ui's
verdictClasses, with first-scan metadata on already_checked_in.

The routing proof (StationPage.test.tsx) mounts a router shaped like the
real registration and confirms none of the workspace rail's nav markers
leak through, plus a deliberately-misregistered counter-example proving
the assertion actually discriminates.
Fills StationPage's Task 8 placeholder aside with the last-50
checkin-actions feed (name/code/action/time, newest-first, server order
trusted verbatim). Per-row Reprint fetches the full Attendee (the feed's
own slim projection can't feed attendeeToPreviewData) and sends through
usePrintBadge with a printContext so the backend logs the reprint, then
refetches the feed itself (P4.2 will make this live via SSE). Undo and
Reprint both use hand-built confirm dialogs that block every dismissal
path while their mutation is in flight, per the P3.2 PR-#74 convention.
Details is a compact DropdownMenu popover (name/code/scan time) rather
than the full attendee drawer.
…ns rail

anyMutationPending stayed false while a dialog was open but unconfirmed,
so a row's still-enabled trigger could open the OTHER dialog type
concurrently and both could be confirmed independently against the
same attendee. Gate every list-level trigger on reprintTarget !== null
|| undoTarget !== null too, so once either dialog is open no other
row's Reprint/Undo trigger can open a second, competing one.
Adds useConnectionState(eventId), folding navigator.onLine/the browser's
online/offline events and the check-in actions feed's own isError into one
debounced online signal. StationPage shows the amber "Connection is
unstable" banner while offline, blocks check-in submits before any network
call is attempted (an explicit offline verdict instead of a silently
dropped scan), and keeps manual search read-only against the cached roster
(no check-in CTA). RecentScansRail's Undo/Reprint are disabled the same
way. Display/UX degradation only -- no offline write queue, per this
phase's spec.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the P4.1 launch ceremony (event/station confirm, editable check-in
settings, agent printer check reusing TestPrintDialog, Start check-in CTA
that upserts a station then navigates to the station route) as a new
top-level rail-less route, mirroring Task 8's sibling-registration
pattern. Unlocks the workspace rail's pinned Check-in row into a real
link once readiness is green, and wires the pre-existing header CTA
(previously a "coming soon" placeholder dialog) to the same destination.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds useHeartbeat(eventId, stationId): fires an immediate heartbeat POST
on mount then every 20s via Task 5's useStationHeartbeat mutation,
clearing the interval on unmount. Uses fire-and-forget .mutate() so a
failed heartbeat is non-fatal (never throws, retried next tick). Mounted
unconditionally in StationPage alongside its other hooks.
Task 13 — verification-only, no new features. i18n sweep (63 checkin*/
launch* keys, exact en/ru parity, real Russian translations, no hardcoded
strings); full gates green (panel typecheck/test/lint/build, packages/ui
tests, zero API-client drift, backend go test + golangci-lint); router.tsx/
backend/web cross-checks all clean; spec walk against
docs/superpowers/specs/2026-07-17-panel-p4.1-checkin-loop-design.md §3-§7
confirms every section satisfied with no reconciliations needed. Marks all
plan checkboxes done.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…logging as reprints

Final whole-branch review of the P4.1 check-in loop found two Important
integration gaps visible only once every task was assembled:

- settings.manual_search_enabled (Task 5/11) had no consumer at the
  station: ScanInput always rendered the manual search box regardless of
  the toggle. StationPage now threads the setting into ScanInput as
  manualSearchEnabled, which gates the entire search box/results/hints
  block (wedge/scanner scan-input mechanism is unaffected).

- useCheckinFlow's implicit auto-print (the automatic badge print on a
  fresh checked_in outcome) passed printContext to printAttendee, causing
  the backend to log a mislabeled 'reprint' feed row for what was actually
  the first print of a check-in already logged by CheckInAttendee itself.
  The implicit print now omits printContext, falling back to the
  pre-existing counter-only behavior; RecentScansRail's own Reprint button
  (a genuine operator-initiated reprint) is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 18, 2026 00:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added documentation Improvements or additions to documentation backend panel labels Jul 18, 2026
@thevladbog

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@thevladbog, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b935e6d-77ce-4584-9ffb-6eb05d30234a

📥 Commits

Reviewing files that changed from the base of the PR and between b5c8bff and 5f64792.

📒 Files selected for processing (12)
  • panel/src/features/checkin/LaunchCeremony.test.tsx
  • panel/src/features/checkin/LaunchCeremony.tsx
  • panel/src/features/checkin/RecentScansRail.test.tsx
  • panel/src/features/checkin/ScanInput.tsx
  • panel/src/features/checkin/StationPage.test.tsx
  • panel/src/features/checkin/StationPage.tsx
  • panel/src/features/checkin/useCheckinFlow.test.tsx
  • panel/src/features/checkin/useCheckinFlow.ts
  • panel/src/features/checkin/useScanInput.test.tsx
  • panel/src/features/checkin/useScanInput.ts
  • panel/src/shared/agent/useAgentPrinters.test.tsx
  • panel/src/shared/agent/useAgentPrinters.ts
📝 Walkthrough

Walkthrough

Adds a complete event check-in loop: backend settings, stations, idempotent check-in/undo actions, reprint logging, OpenAPI contracts, panel scanner flows, station UI, launch ceremony, offline handling, heartbeat, printing context, and localized translations.

Changes

Check-in loop

Layer / File(s) Summary
Backend persistence and contracts
backend/migrations/*, backend/internal/models/*, backend/internal/store/*
Adds check-in settings, stations, durable action feeds, transactional attendee check-in/undo behavior, heartbeats, deterministic feed ordering, and SQL-level tests.
HTTP handlers and API wiring
backend/internal/handler/*, backend/openapi.yaml
Adds settings, station, check-in, undo, action-feed, and printed reprint-context endpoints with validation, ownership checks, and error mapping.
Panel station workflow
panel/src/features/checkin/*, panel/src/app/router.tsx, panel/src/features/workspace/*
Adds settings hooks, scan modes, check-in state, verdicts, offline behavior, heartbeat, station UI, recent scans, reprint/undo dialogs, launch ceremony, and protected routes.
Supporting integrations and coverage
panel/src/shared/*, panel/src/features/attendees/*, panel/src/features/badge/*, **/*test*, docs/superpowers/*
Adds generated API types, scanner client methods, printing context/config resolution, attendee query gating, localized strings, and backend/panel contract tests and design documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main check-in loop, station UI, and launch ceremony changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch panel/p4.1-checkin-loop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7681e290b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread panel/src/features/checkin/StationPage.tsx Outdated
Comment thread panel/src/features/checkin/searchParams.ts
Comment thread panel/src/features/checkin/VerdictCard.tsx
Comment thread backend/internal/store/pg_store.go Outdated
Comment thread panel/src/features/checkin/useCheckinFlow.ts Outdated
Comment thread backend/internal/store/pg_store.go Outdated
Comment thread panel/src/features/checkin/useConnectionState.ts
Comment thread panel/src/features/checkin/ScanInput.tsx Outdated
Comment thread panel/src/features/checkin/RecentScansRail.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (5)
backend/internal/handler/openapi_contract_attendee_printed_p4_test.go (2)

87-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Run the independent contract tests in parallel.

  • backend/internal/handler/openapi_contract_attendee_printed_p4_test.go#L87-L139: add t.Parallel() to each independent top-level test in this file.
  • backend/internal/handler/openapi_contract_checkin_p4_test.go#L45-L75: add t.Parallel() to each independent top-level test and eligible subtests in this file.

As per coding guidelines, “Write unit tests using table-driven patterns and parallel execution.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/handler/openapi_contract_attendee_printed_p4_test.go` around
lines 87 - 139, Add t.Parallel() at the start of each independent top-level test
in backend/internal/handler/openapi_contract_attendee_printed_p4_test.go lines
87-139 and backend/internal/handler/openapi_contract_checkin_p4_test.go lines
45-75; also parallelize eligible subtests in the check-in contract test file.
Ensure tests do not rely on shared mutable state before enabling parallel
execution.

Source: Coding guidelines


55-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the action fake enforce event scoping.

The insert and read fakes ignore eventID, so the core test would still pass if a reprint were written to the wrong event—the exact regression this suite is intended to prevent.

Proposed assertions
 insertCheckinAction: func(eventID, attendeeID uuid.UUID, action string, stationID *uuid.UUID, staffUserID uuid.UUID) error {
+	if eventID != event.ID {
+		t.Fatalf("InsertCheckinAction eventID = %s, want %s", eventID, event.ID)
+	}
+	if staffUserID == uuid.Nil {
+		t.Fatal("InsertCheckinAction received a nil staffUserID")
+	}
 	actions = append(actions, store.CheckinActionRow{
 		...
 	})
 	return nil
 },
-getCheckinActions: func(uuid.UUID, int) ([]store.CheckinActionRow, error) {
+getCheckinActions: func(eventID uuid.UUID, _ int) ([]store.CheckinActionRow, error) {
+	if eventID != event.ID {
+		t.Fatalf("GetCheckinActions eventID = %s, want %s", eventID, event.ID)
+	}
 	return actions, nil
 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/handler/openapi_contract_attendee_printed_p4_test.go` around
lines 55 - 66, Update the insertCheckinAction and getCheckinActions fakes to
enforce event scoping by storing the supplied eventID with each action and
filtering reads to the requested eventID. Ensure actions written under a
different event are not returned, so the test fails when reprint data is
associated with the wrong event.
backend/internal/handler/checkin.go (1)

147-161: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Preserve wrapped, correlated diagnostics for internal failures.

These branches return sanitized responses but either discard the underlying error or emit plain-text logs. Route them through a shared helper that wraps with fmt.Errorf, emits structured JSON with request/trace IDs, and keeps client responses sanitized.

  • backend/internal/handler/checkin.go#L147-L161: record wrapped user-resolution and check-in failures.
  • backend/internal/handler/attendee_printed.go#L128-L152: record wrapped counter and reprint-action failures using structured logging.
  • backend/internal/handler/checkin.go#L80-L87: preserve the station lookup error.
  • backend/internal/handler/checkin.go#L217-L223: preserve the undo failure.
  • backend/internal/handler/checkin.go#L252-L255: preserve the feed-query failure.
  • backend/internal/handler/checkin_settings.go#L104-L107: preserve the settings-read failure.
  • backend/internal/handler/checkin_settings.go#L140-L142: preserve the settings-write failure.
  • backend/internal/handler/checkin_stations.go#L78-L90: preserve zone verification and station-upsert failures.
  • backend/internal/handler/checkin_stations.go#L112-L117: preserve heartbeat failures.
  • backend/internal/handler/checkin_stations.go#L133-L136: preserve station-list failures.

As per coding guidelines, “Always check and handle errors explicitly, using wrapped errors for traceability” and “Include unique request IDs and trace context in all logs for correlation.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/handler/checkin.go` around lines 147 - 161, Introduce or
reuse a shared handler error-reporting helper that wraps failures with
fmt.Errorf, emits structured JSON logs containing request and trace IDs, and
preserves sanitized client responses. Apply it to
backend/internal/handler/checkin.go:147-161, 80-87, 217-223, and 252-255;
backend/internal/handler/attendee_printed.go:128-152;
backend/internal/handler/checkin_settings.go:104-107 and 140-142; and
backend/internal/handler/checkin_stations.go:78-90, 112-117, and 133-136,
covering each user-resolution, check-in, counter, reprint, lookup, undo, feed,
settings, zone, station, heartbeat, and station-list failure without changing
expected status responses.

Source: Coding guidelines

backend/internal/store/pg_store_checkin_test.go (1)

24-161: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Convert the new scenario families to table-driven parallel tests.

Group settings, station, check-in, undo, and action-feed cases into tables with subtests, calling t.Parallel() where isolated. This removes repeated mock setup and follows the backend test policy.

As per coding guidelines, “Write unit tests using table-driven patterns and parallel execution.”

Also applies to: 177-435, 473-936

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/store/pg_store_checkin_test.go` around lines 24 - 161,
Convert the new check-in test cases around
TestGetCheckinSettingsReturnsStoredJSON and
TestUpdateCheckinSettingsIssuesGuardedUpdate into table-driven subtests,
consolidating repeated mock setup and assertions for each scenario. Call
t.Parallel() for isolated subtests while preserving the stored, NULL, no-row,
successful update, and soft-delete no-op behaviors; apply the same table-driven
parallel structure to the related settings, station, undo, and action-feed test
families.

Source: Coding guidelines

panel/src/features/checkin/StationPage.test.tsx (1)

59-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Test the actual route registrations.

Both tests construct the desired route relationship themselves, so production routing can regress while they remain green.

  • panel/src/features/checkin/StationPage.test.tsx#L59-L80: exercise or inspect the production station route and its parent.
  • panel/src/features/checkin/LaunchCeremony.test.tsx#L52-L72: exercise or inspect the production launch route and its parent.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@panel/src/features/checkin/StationPage.test.tsx` around lines 59 - 80, Update
buildCorrectRouter in panel/src/features/checkin/StationPage.test.tsx (lines
59-80) to exercise or inspect the production station route and verify its parent
rather than recreating the route registration. Apply the same change in
panel/src/features/checkin/LaunchCeremony.test.tsx (lines 52-72) for the
production launch route and parent; both sites require direct updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/internal/handler/attendee_printed.go`:
- Around line 109-126: Update the stationID validation in the attendee printed
handler to return a bad-request error whenever station_id is supplied without
event_id. Keep the existing UUID parsing and resolveCheckinStation validation
for requests containing both values, and ensure invalid combinations exit before
the counter increment or feed-row processing.

In `@backend/internal/store/interface.go`:
- Around line 109-118: Update UpdateCheckinSettings to detect zero affected rows
from the deleted_at-guarded update and return the store’s sentinel error instead
of succeeding. In the handler that invokes UpdateCheckinSettings, map that
sentinel to the appropriate HTTP error so a concurrent soft-delete does not
return 200.

In `@backend/internal/store/pg_store.go`:
- Around line 738-751: Wrap every database, JSON decoding, action-insert, and
transaction-commit error in the affected store methods, including
GetCheckinSettings, with operation-specific fmt.Errorf messages using %w so
errors.Is behavior is preserved. Keep existing special handling such as
pgx.ErrNoRows and nil-result behavior unchanged.
- Around line 1080-1086: Update the recent-actions query in the method
containing the s.db.Query call to order by ca.created_at DESC, then ca.id DESC
for deterministic ordering of tied timestamps. Extend the corresponding
migration index to include ca.id in the same ordering.

In `@backend/migrations/000019_checkin_loop.up.sql`:
- Around line 10-28: Update the checkin_stations and checkin_actions table
definitions to enforce same-event relationships at the database level: add
composite uniqueness keys and composite foreign keys tying
checkin_stations.zone_id to event_zones and checkin_actions.attendee_id to
attendees, plus station_id to checkin_stations, alongside each row’s event_id.
Preserve nullable behavior and existing delete actions while ensuring
cross-event references are rejected.

In `@panel/src/app/router.tsx`:
- Around line 153-154: Update validateCheckinStationSearch and
checkinStationBeforeLoad to validate the station ID as a UUID, verify that the
station belongs to the current event, and redirect invalid, nonexistent, or
foreign stations before mounting the station workflow. Preserve the existing
valid-station navigation behavior.

In `@panel/src/features/checkin/LaunchCeremony.tsx`:
- Around line 111-130: The check-in surfaces currently operate on default
settings before the remote configuration loads. In
panel/src/features/checkin/LaunchCeremony.tsx lines 111-130, gate setting edits
and whole-object saves on the settings query having successfully seeded the form
via settingsSeededRef; in panel/src/features/checkin/StationPage.tsx lines
74-87, block scan/search submissions until settings load and display the loading
or failure state instead of using defaults.
- Around line 66-70: Replace the hand-built controls with shared primitives from
`@idento/ui`: add the missing Select primitive under packages/ui and use it for
zone and scan-mode selection in LaunchCeremony.tsx and printer selection in
RecentScansRail.tsx; use the shared Button primitive for the attendee action in
ScanInput.tsx. Remove the local SELECT_CLASSNAME and preserve the existing
control behavior and values.

In `@panel/src/features/checkin/RecentScansRail.test.tsx`:
- Around line 397-413: Update the test around AttendeesListObserver to track
requests to the attendees endpoint separately from actionsHitCount. After
completing the Undo flow, assert that the attendees request count increases,
while retaining the existing feed refetch assertion and request-body checks.
- Around line 317-340: Expand the dismissal-guard tests around the reprint
dialog to cover Escape and outside-click interactions in addition to Cancel.
While the print request is pending, trigger each dismissal path and assert the
dialog remains open, then retain the existing assertion that it closes after the
operation completes; update both affected test cases using the existing dialog
and print-flow helpers.

In `@panel/src/features/checkin/RecentScansRail.tsx`:
- Around line 151-159: Revalidate the online state at the start of both reprint
and undo confirmation handlers, including handleReprintConfirm, and return
before initiating any external call when offline. Also disable both dialog
confirm buttons while online is false, covering the relevant button render paths
near the referenced sections.

In `@panel/src/features/checkin/ScanInput.tsx`:
- Around line 86-104: Update the useAttendeesPage call in ScanInput to include
!readOnly in its enabled condition, preventing uncached searches while offline.
Also update showNoMatches so it requires a successful query before displaying
the empty-result state, while preserving the existing loading, fetching, and
results checks.
- Around line 106-110: Update the pick function to return immediately when
enabled is false, preventing onPickAttendee from running during an active
check-in resolution; also disable the attendee result action in the rendering
around the result button so it cannot be triggered while disabled, while
preserving the existing behavior when enabled is true.

In `@panel/src/features/checkin/searchParams.ts`:
- Around line 20-22: Update validateCheckinStationSearch to accept station only
when it is a non-empty string and a valid UUID; otherwise return station as
undefined so invalid station values redirect the operator to launch.

In `@panel/src/features/checkin/settingsTypes.ts`:
- Around line 85-90: Update the verdict_auto_dismiss_sec validation in the
settings parser to require Number.isInteger in addition to numeric and finite
checks, falling back to DEFAULT_CHECKIN_SETTINGS.verdict_auto_dismiss_sec for
fractional values before clamping. Add coverage for a fractional raw value in
settingsTypes.test.ts.

In `@panel/src/features/checkin/useScanInput.ts`:
- Around line 119-130: Update the polling logic around lastHandledRef and
clearLastScan so a duplicate code/time pair retries the pending buffer clear
instead of returning immediately after a failed clear, while still avoiding a
second onCodeRef emission. Track whether clearing succeeded, preserve
healthy-state handling, and add an MSW test covering an initial clear failure
followed by a retry that invokes onCode only once.

In `@panel/src/shared/agent/agentClient.ts`:
- Around line 172-186: Update the scan consumption flow around clearLastScan so
clearing is atomic with reading the scan: use the agent’s atomic consume
endpoint, or pass the observed code and time to a conditional clear request.
Ensure a newer scan arriving after scan A is read is preserved rather than
erased, and update the request handling and endpoint usage consistently.

In `@panel/src/shared/i18n/en.json`:
- Line 558: Update the scanner-degraded messaging used by ScanInput to avoid
directing operators to manual search when manualSearchEnabled is false, while
preserving the existing wording when it is available. Add matching generic
fallback translations in panel/src/shared/i18n/en.json:558-558 and
panel/src/shared/i18n/ru.json:560-560, and extend
panel/src/features/checkin/ScanInput.test.tsx:236-243 to cover scanner failure
with manualSearchEnabled: false and assert that unavailable manual-search
controls are not referenced.

---

Nitpick comments:
In `@backend/internal/handler/checkin.go`:
- Around line 147-161: Introduce or reuse a shared handler error-reporting
helper that wraps failures with fmt.Errorf, emits structured JSON logs
containing request and trace IDs, and preserves sanitized client responses.
Apply it to backend/internal/handler/checkin.go:147-161, 80-87, 217-223, and
252-255; backend/internal/handler/attendee_printed.go:128-152;
backend/internal/handler/checkin_settings.go:104-107 and 140-142; and
backend/internal/handler/checkin_stations.go:78-90, 112-117, and 133-136,
covering each user-resolution, check-in, counter, reprint, lookup, undo, feed,
settings, zone, station, heartbeat, and station-list failure without changing
expected status responses.

In `@backend/internal/handler/openapi_contract_attendee_printed_p4_test.go`:
- Around line 87-139: Add t.Parallel() at the start of each independent
top-level test in
backend/internal/handler/openapi_contract_attendee_printed_p4_test.go lines
87-139 and backend/internal/handler/openapi_contract_checkin_p4_test.go lines
45-75; also parallelize eligible subtests in the check-in contract test file.
Ensure tests do not rely on shared mutable state before enabling parallel
execution.
- Around line 55-66: Update the insertCheckinAction and getCheckinActions fakes
to enforce event scoping by storing the supplied eventID with each action and
filtering reads to the requested eventID. Ensure actions written under a
different event are not returned, so the test fails when reprint data is
associated with the wrong event.

In `@backend/internal/store/pg_store_checkin_test.go`:
- Around line 24-161: Convert the new check-in test cases around
TestGetCheckinSettingsReturnsStoredJSON and
TestUpdateCheckinSettingsIssuesGuardedUpdate into table-driven subtests,
consolidating repeated mock setup and assertions for each scenario. Call
t.Parallel() for isolated subtests while preserving the stored, NULL, no-row,
successful update, and soft-delete no-op behaviors; apply the same table-driven
parallel structure to the related settings, station, undo, and action-feed test
families.

In `@panel/src/features/checkin/StationPage.test.tsx`:
- Around line 59-80: Update buildCorrectRouter in
panel/src/features/checkin/StationPage.test.tsx (lines 59-80) to exercise or
inspect the production station route and verify its parent rather than
recreating the route registration. Apply the same change in
panel/src/features/checkin/LaunchCeremony.test.tsx (lines 52-72) for the
production launch route and parent; both sites require direct updates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dcdab587-cd3b-4d8c-bb60-8c1bf55ac216

📥 Commits

Reviewing files that changed from the base of the PR and between 4e1e82b and 7681e29.

📒 Files selected for processing (55)
  • backend/internal/handler/attendee_printed.go
  • backend/internal/handler/checkin.go
  • backend/internal/handler/checkin_settings.go
  • backend/internal/handler/checkin_stations.go
  • backend/internal/handler/handler.go
  • backend/internal/handler/openapi_contract_attendee_printed_p4_test.go
  • backend/internal/handler/openapi_contract_checkin_p4_test.go
  • backend/internal/handler/testsupport_test.go
  • backend/internal/models/models.go
  • backend/internal/store/interface.go
  • backend/internal/store/pg_store.go
  • backend/internal/store/pg_store_checkin_test.go
  • backend/migrations/000019_checkin_loop.down.sql
  • backend/migrations/000019_checkin_loop.up.sql
  • backend/openapi.yaml
  • docs/superpowers/plans/2026-07-17-panel-p4.1-checkin-loop.md
  • docs/superpowers/specs/2026-07-17-panel-p4.1-checkin-loop-design.md
  • panel/src/app/router.tsx
  • panel/src/features/attendees/hooks.test.tsx
  • panel/src/features/attendees/hooks.ts
  • panel/src/features/badge/zpl/usePrintBadge.test.tsx
  • panel/src/features/badge/zpl/usePrintBadge.ts
  • panel/src/features/checkin/LaunchCeremony.test.tsx
  • panel/src/features/checkin/LaunchCeremony.tsx
  • panel/src/features/checkin/RecentScansRail.test.tsx
  • panel/src/features/checkin/RecentScansRail.tsx
  • panel/src/features/checkin/ScanInput.test.tsx
  • panel/src/features/checkin/ScanInput.tsx
  • panel/src/features/checkin/StationPage.test.tsx
  • panel/src/features/checkin/StationPage.tsx
  • panel/src/features/checkin/VerdictCard.tsx
  • panel/src/features/checkin/hooks.test.tsx
  • panel/src/features/checkin/hooks.ts
  • panel/src/features/checkin/searchParams.ts
  • panel/src/features/checkin/settingsTypes.test.ts
  • panel/src/features/checkin/settingsTypes.ts
  • panel/src/features/checkin/useCheckinFlow.test.tsx
  • panel/src/features/checkin/useCheckinFlow.ts
  • panel/src/features/checkin/useConnectionState.test.tsx
  • panel/src/features/checkin/useConnectionState.ts
  • panel/src/features/checkin/useHeartbeat.test.tsx
  • panel/src/features/checkin/useHeartbeat.ts
  • panel/src/features/checkin/useScanInput.test.tsx
  • panel/src/features/checkin/useScanInput.ts
  • panel/src/features/checkin/verdict.test.ts
  • panel/src/features/checkin/verdict.ts
  • panel/src/features/workspace/EventWorkspaceLayout.test.tsx
  • panel/src/features/workspace/EventWorkspaceLayout.tsx
  • panel/src/features/workspace/WorkspaceRail.test.tsx
  • panel/src/features/workspace/WorkspaceRail.tsx
  • panel/src/shared/agent/agentClient.test.ts
  • panel/src/shared/agent/agentClient.ts
  • panel/src/shared/api/schema.d.ts
  • panel/src/shared/i18n/en.json
  • panel/src/shared/i18n/ru.json

Comment thread backend/internal/handler/attendee_printed.go
Comment thread backend/internal/store/interface.go
Comment thread backend/internal/store/pg_store.go
Comment thread backend/internal/store/pg_store.go
Comment thread backend/migrations/000019_checkin_loop.up.sql
Comment thread panel/src/features/checkin/searchParams.ts
Comment thread panel/src/features/checkin/settingsTypes.ts
Comment thread panel/src/features/checkin/useScanInput.ts
Comment thread panel/src/shared/agent/agentClient.ts
Comment thread panel/src/shared/i18n/en.json
…ce-number clear, settings soft-delete race, reprint station validation, feed tie-breaker

Fixes 5 findings from Codex/CodeRabbit review on the P4.1 check-in loop
backend, TDD throughout:

- CheckInAttendee's guarded UPDATE now also requires blocked = false,
  closing a TOCTOU race where an attendee blocked between StationCheckin's
  pre-read and the UPDATE could still be checked in; the 0-row fallback
  distinguishes this newly-blocked case (outcome "blocked") from
  already_checked_in.
- UndoCheckin now clears checked_in_device_number alongside the other
  check-in metadata, so a panel undo of a mobile-batch check-in no longer
  leaves stale device info on the row.
- UpdateCheckinSettings returns the new ErrEventNotFound sentinel on a
  0-row soft-delete race instead of silently succeeding; the handler maps
  it to 404 instead of a fabricated 200.
- MarkAttendeePrinted now 400s when station_id is supplied without
  event_id, instead of silently discarding it.
- GetCheckinActions orders by (created_at DESC, id DESC) for a
  deterministic "last 50" feed; migration 000019's index (still unapplied
  anywhere) extended in place to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…validation, block reason, mark-printed distinction, connectivity polling, offline gating, settings-load gating, misc coverage

Fixes 14 bot-review findings (Codex + CodeRabbit) on the P4.1 check-in
loop panel:

- F: catch submitCode/submitAttendee rejections in StationPage and
  surface a visible, recoverable error via a new requestError state
- G: UUID-format validation on ?station= (searchParams.ts)
- H: render attendee.block_reason on the blocked/no_access verdict
- I: distinguish MarkPrintedError from a genuine print failure
- J: poll useCheckinActions every 20s so a backend outage mid-shift
  flips the connection signal without an unrelated trigger
- K: stop the degraded manual-search box from issuing new uncached
  requests, and gate the no-matches empty state on query success
- L: re-gate the reprint/undo confirm buttons (and their handlers) on
  connectivity after a dialog is already open
- M: make pick() honor enabled=false and disable the result button
- N: gate LaunchCeremony's settings form/Save and StationPage's scan
  surface on the real settings load, not DEFAULT_CHECKIN_SETTINGS
- O: reject fractional verdict_auto_dismiss_sec instead of clamping it
- P: retry a failed agent scan-buffer clear without re-emitting onCode
- Q: conditional degraded-scanner copy when manual search is disabled
- R: exercise Escape/outside-click in the dismissal-guard tests
- S: assert the attendees-list hit count in the undo invalidation test

Full writeup with the fix + tests for each finding, plus a
cross-finding self-review, in .superpowers/sdd/pr77-panel-fix-report.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@thevladbog

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
backend/internal/store/pg_store_checkin_test.go (1)

145-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Run these isolated pgxmock tests in parallel.

Add t.Parallel() at the start of each changed test. Their mock pools and identifiers are independently scoped.

As per coding guidelines, “Write unit tests using table-driven patterns and parallel execution.”

Proposed change
 func TestUpdateCheckinSettingsSoftDeleteRaceReturnsSentinel(t *testing.T) {
+	t.Parallel()
 	mock, err := pgxmock.NewPool()

 func TestCheckInAttendeeNewlyBlockedReturnsBlockedOutcome(t *testing.T) {
+	t.Parallel()
 	mock, err := pgxmock.NewPool()

 func TestUndoCheckinClearsDeviceNumber(t *testing.T) {
+	t.Parallel()
 	mock, err := pgxmock.NewPool()

Also applies to: 624-664, 767-799

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/store/pg_store_checkin_test.go` around lines 145 - 166, Add
t.Parallel() at the start of
TestUpdateCheckinSettingsSoftDeleteRaceReturnsSentinel and each other changed
test in the referenced ranges. Keep the existing independently scoped pgxmock
pools and identifiers unchanged while enabling these tests to run concurrently.

Source: Coding guidelines

panel/src/features/checkin/searchParams.test.ts (1)

61-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the launch-ceremony redirect destination.

toThrow() also passes for an unrelated error or a redirect to the wrong event/path. Capture the thrown redirect and verify it targets /events/evt-1/checkin/launch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@panel/src/features/checkin/searchParams.test.ts` around lines 61 - 65, Update
the test for checkinStationBeforeLoad to capture the thrown redirect when
station is undefined, then assert its destination is
/events/evt-1/checkin/launch. Keep the existing eventId input and ensure the
assertion verifies the redirect target rather than only checking that an error
was thrown.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/internal/store/pg_store_checkin_test.go`:
- Around line 145-166: Add t.Parallel() at the start of
TestUpdateCheckinSettingsSoftDeleteRaceReturnsSentinel and each other changed
test in the referenced ranges. Keep the existing independently scoped pgxmock
pools and identifiers unchanged while enabling these tests to run concurrently.

In `@panel/src/features/checkin/searchParams.test.ts`:
- Around line 61-65: Update the test for checkinStationBeforeLoad to capture the
thrown redirect when station is undefined, then assert its destination is
/events/evt-1/checkin/launch. Keep the existing eventId input and ensure the
assertion verifies the redirect target rather than only checking that an error
was thrown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e7014bc2-27b7-4f99-a19f-afb4ef327f36

📥 Commits

Reviewing files that changed from the base of the PR and between 7681e29 and 4e9cca2.

📒 Files selected for processing (30)
  • backend/internal/handler/attendee_printed.go
  • backend/internal/handler/checkin_settings.go
  • backend/internal/handler/openapi_contract_attendee_printed_p4_test.go
  • backend/internal/handler/openapi_contract_checkin_p4_test.go
  • backend/internal/store/interface.go
  • backend/internal/store/pg_store.go
  • backend/internal/store/pg_store_checkin_test.go
  • backend/migrations/000019_checkin_loop.up.sql
  • panel/src/features/checkin/LaunchCeremony.test.tsx
  • panel/src/features/checkin/LaunchCeremony.tsx
  • panel/src/features/checkin/RecentScansRail.test.tsx
  • panel/src/features/checkin/RecentScansRail.tsx
  • panel/src/features/checkin/ScanInput.test.tsx
  • panel/src/features/checkin/ScanInput.tsx
  • panel/src/features/checkin/StationPage.test.tsx
  • panel/src/features/checkin/StationPage.tsx
  • panel/src/features/checkin/VerdictCard.test.tsx
  • panel/src/features/checkin/VerdictCard.tsx
  • panel/src/features/checkin/searchParams.test.ts
  • panel/src/features/checkin/searchParams.ts
  • panel/src/features/checkin/settingsTypes.test.ts
  • panel/src/features/checkin/settingsTypes.ts
  • panel/src/features/checkin/useCheckinFlow.test.tsx
  • panel/src/features/checkin/useCheckinFlow.ts
  • panel/src/features/checkin/useConnectionState.test.tsx
  • panel/src/features/checkin/useConnectionState.ts
  • panel/src/features/checkin/useScanInput.test.tsx
  • panel/src/features/checkin/useScanInput.ts
  • panel/src/shared/i18n/en.json
  • panel/src/shared/i18n/ru.json
🚧 Files skipped from review as they are similar to previous changes (19)
  • backend/migrations/000019_checkin_loop.up.sql
  • panel/src/shared/i18n/ru.json
  • panel/src/features/checkin/searchParams.ts
  • panel/src/features/checkin/settingsTypes.ts
  • panel/src/features/checkin/RecentScansRail.test.tsx
  • backend/internal/handler/attendee_printed.go
  • panel/src/features/checkin/ScanInput.test.tsx
  • panel/src/features/checkin/StationPage.tsx
  • panel/src/features/checkin/ScanInput.tsx
  • backend/internal/store/interface.go
  • panel/src/features/checkin/LaunchCeremony.test.tsx
  • panel/src/features/checkin/useScanInput.test.tsx
  • panel/src/features/checkin/useCheckinFlow.ts
  • panel/src/features/checkin/LaunchCeremony.tsx
  • panel/src/features/checkin/StationPage.test.tsx
  • panel/src/features/checkin/RecentScansRail.tsx
  • backend/internal/handler/openapi_contract_attendee_printed_p4_test.go
  • panel/src/features/checkin/useScanInput.ts
  • backend/internal/handler/openapi_contract_checkin_p4_test.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e9cca2003

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread panel/src/features/checkin/StationPage.tsx
Comment thread backend/openapi.yaml Outdated
Comment thread backend/internal/store/pg_store.go Outdated
Comment thread backend/internal/store/pg_store.go Outdated
Comment thread panel/src/features/checkin/LaunchCeremony.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e9cca2003

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread panel/src/features/checkin/useCheckinFlow.ts Outdated
Comment thread panel/src/features/checkin/LaunchCeremony.tsx Outdated
Comment thread panel/src/features/checkin/useScanInput.ts
Comment thread panel/src/features/checkin/StationPage.tsx
Comment thread backend/internal/store/pg_store.go Outdated
CI Bot and others added 2 commits July 18, 2026 07:56
…number clear on checkin, openapi dependency docs

Fixes 3 further Codex findings on the P4.1 check-in loop backend, TDD
throughout for the store-layer findings:

- CheckInAttendee's 0-row fallback could misclassify an attendee that is
  neither checked in nor blocked (a narrow race) as already_checked_in,
  which is factually wrong and skips printing entirely. The
  guarded-UPDATE-then-fallback sequence is now retried once (bounded, 2
  attempts total) inside the same transaction on that specific "conflict"
  state; if the retry lands there too, the store returns the new
  ErrCheckinConflict sentinel, mapped to 409 by StationCheckin.
- CheckInAttendee's guarded UPDATE now also clears checked_in_device_number,
  mirroring UndoCheckin's existing clear (previous bot-review round) — a
  fresh panel check-in no longer inherits a stale device number left over
  from an earlier mobile check-in.
- openapi.yaml's MarkAttendeePrinted request schema now documents the
  station_id/event_id dependency and the event_id/attendee mismatch
  rejection the handler has enforced since two prior rounds, in prose
  (kin-openapi's version has no clean dependentRequired support). Also
  documents CheckInAttendee's new 409. schema.d.ts regenerated to match
  (doc comments + the new 409 response type only).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…, unsaved-settings launch guard, test-badge config parity, wedge refocus

Fixes 5 Codex bot-review findings on the P4.1 check-in loop:
- StationPage.tsx gates the scan surface (never check-in itself) behind an
  explicit "waiting for printer" state when print_on_checkin is on but the
  agent hasn't resolved a default printer yet, mirroring the existing
  settings-loading gate.
- useCheckinFlow.ts's auto-print now checks printBadge.fontsStatus before
  attempting a print (matching every other print surface's own gating) and
  surfaces a distinct printFontsPending state instead of risking a stale
  font-list race.
- LaunchCeremony.tsx's "Start check-in" is disabled while there's an
  unsaved settings edit or a save in flight, so navigating to the station
  can no longer silently discard visible-but-unsaved settings.
- LaunchCeremony.tsx's "Test badge" now resolves label config through the
  same resolveBadgeConfig helper the real check-in print path uses
  (extracted from usePrintBadge.ts), so a configless legacy template's test
  print validates the actual 50x30mm@203dpi fallback, not the editor's
  90x55mm@300dpi default.
- useScanInput.ts's wedge capture input now re-focuses shortly after any
  blur (not just on a wedgeActive transition), unless the operator is
  actively using a text field or a dialog/menu that's now open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@thevladbog

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
panel/src/features/badge/zpl/usePrintBadge.ts (1)

91-91: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Derive event_id from the hook’s eventId.

Accepting another event ID in printContext permits template/attendee scope and reprint-recording scope to diverge. Keep only stationId in the option.

Proposed change
-  printContext?: { eventId: string; stationId: string | null };
+  printContext?: { stationId: string | null };
...
-          body: { event_id: opts.printContext.eventId, station_id: opts.printContext.stationId },
+          body: { event_id: eventId, station_id: opts.printContext.stationId },

Also applies to: 213-217

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@panel/src/features/badge/zpl/usePrintBadge.ts` at line 91, Update the
printContext option used by usePrintBadge to remove eventId and retain only
stationId. Ensure event_id is always derived from the hook’s eventId throughout
template/attendee scope and reprint-recording logic, eliminating any alternate
event ID source.
backend/internal/store/pg_store_checkin_test.go (1)

577-612: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Parallelize these isolated tests and table-drive the conflict variants.

Each test owns its pgxmock pool and UUIDs, so add t.Parallel(). The success/exhausted retry cases should share a scenario table to prevent their expectations drifting.

As per coding guidelines, new Go tests must use table-driven patterns and parallel execution.

Also applies to: 733-828

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/store/pg_store_checkin_test.go` around lines 577 - 612, The
isolated tests in the check-in test section, including
TestCheckInAttendeeClearsStaleDeviceNumber and the tests through the referenced
conflict-variant range, should call t.Parallel() after setup as appropriate.
Consolidate the success and exhausted retry conflict cases into a shared
table-driven test, keeping each scenario’s pgxmock expectations and assertions
defined by the table to prevent drift.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@panel/src/features/checkin/useCheckinFlow.test.tsx`:
- Around line 103-117: Replace the wall-clock settleFonts delay in
panel/src/features/checkin/useCheckinFlow.test.tsx:103-117 with an observable
query/request completion gate, preserving deterministic font-status
synchronization. In panel/src/features/checkin/useCheckinFlow.test.tsx:384-389,
explicitly release the delayed font request before asserting that no print
occurred. In panel/src/features/checkin/StationPage.test.tsx:379-398, keep
printer handlers pending until the waiting state is asserted, then release them;
update the affected useCheckinFlow and StationPage test flows without relying on
scheduler timing.

---

Nitpick comments:
In `@backend/internal/store/pg_store_checkin_test.go`:
- Around line 577-612: The isolated tests in the check-in test section,
including TestCheckInAttendeeClearsStaleDeviceNumber and the tests through the
referenced conflict-variant range, should call t.Parallel() after setup as
appropriate. Consolidate the success and exhausted retry conflict cases into a
shared table-driven test, keeping each scenario’s pgxmock expectations and
assertions defined by the table to prevent drift.

In `@panel/src/features/badge/zpl/usePrintBadge.ts`:
- Line 91: Update the printContext option used by usePrintBadge to remove
eventId and retain only stationId. Ensure event_id is always derived from the
hook’s eventId throughout template/attendee scope and reprint-recording logic,
eliminating any alternate event ID source.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6b43f56-9766-4afb-9a7c-dea6cb85bc10

📥 Commits

Reviewing files that changed from the base of the PR and between 4e9cca2 and b5c8bff.

📒 Files selected for processing (20)
  • backend/internal/handler/checkin.go
  • backend/internal/store/interface.go
  • backend/internal/store/pg_store.go
  • backend/internal/store/pg_store_checkin_test.go
  • backend/openapi.yaml
  • panel/src/features/badge/templateTypes.test.ts
  • panel/src/features/badge/templateTypes.ts
  • panel/src/features/badge/zpl/usePrintBadge.ts
  • panel/src/features/checkin/LaunchCeremony.test.tsx
  • panel/src/features/checkin/LaunchCeremony.tsx
  • panel/src/features/checkin/StationPage.test.tsx
  • panel/src/features/checkin/StationPage.tsx
  • panel/src/features/checkin/VerdictCard.tsx
  • panel/src/features/checkin/useCheckinFlow.test.tsx
  • panel/src/features/checkin/useCheckinFlow.ts
  • panel/src/features/checkin/useScanInput.test.tsx
  • panel/src/features/checkin/useScanInput.ts
  • panel/src/shared/api/schema.d.ts
  • panel/src/shared/i18n/en.json
  • panel/src/shared/i18n/ru.json
🚧 Files skipped from review as they are similar to previous changes (13)
  • panel/src/features/checkin/VerdictCard.tsx
  • panel/src/shared/i18n/ru.json
  • panel/src/features/checkin/useScanInput.test.tsx
  • backend/internal/handler/checkin.go
  • panel/src/features/checkin/useCheckinFlow.ts
  • panel/src/features/checkin/StationPage.tsx
  • panel/src/features/checkin/LaunchCeremony.tsx
  • backend/internal/store/interface.go
  • panel/src/shared/i18n/en.json
  • panel/src/features/checkin/useScanInput.ts
  • backend/openapi.yaml
  • backend/internal/store/pg_store.go
  • panel/src/shared/api/schema.d.ts

Comment thread panel/src/features/checkin/useCheckinFlow.test.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5c8bff8fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread panel/src/features/checkin/LaunchCeremony.tsx Outdated
Comment thread panel/src/features/checkin/ScanInput.tsx Outdated
Comment thread panel/src/features/checkin/StationPage.tsx
Comment thread panel/src/features/checkin/useScanInput.ts
Comment thread panel/src/features/checkin/useCheckinFlow.ts
Comment thread panel/src/features/checkin/LaunchCeremony.tsx Outdated
CI Bot and others added 2 commits July 18, 2026 08:53
RecentScansRail.test.tsx's two three-dismissal-path tests (reprint and
undo) used a 40ms real-timer mock delay while performing three sequential
userEvent interactions plus a waitFor before their final assertion. That
window is tight enough to flake under CI's slower/more loaded runners
(observed: PR #77 CI run 29632317448, "Undo check-in" dialog not found)
because the mutation can genuinely resolve mid-sequence and close the
dialog via its own success path -- not because dismissal-blocking failed.
Widened both to 300ms, matching this feature's own precedent for
similar-shaped tests (useCheckinFlow.test.tsx uses 400ms). No production
code changed; handleUndoOpenChange/preventUndoDialogDismiss's blocking
logic was verified correct.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…, printer-gate polling, scanner poll overlap guard, Button primitive, test race fix

Fixes 7 CodeRabbit/Codex findings from the third bot-review round on the
P4.1 check-in loop: LaunchCeremony's settings-seed ref and useCheckinFlow's
verdict/timer state now reset per event/station instead of leaking across a
route-reused instance; StationPage's printer-readiness gate now polls
useAgentPrinters (new refetch) every 10s while blocking so a late-connecting
agent unblocks scanning without an unrelated trigger; useScanInput's 200ms
scanner poll now guards against overlapping getLastScan()/clearLastScan()
round trips; ScanInput's manual-search result row now uses @idento/ui's
Button instead of a hand-rolled <button>; LaunchCeremony gates Start
check-in on settings actually having loaded, not just being non-dirty;
StationPage.test.tsx's flaky printer-waiting test now uses a deferred
promise instead of a fixed delay racing an unbounded findByText wait.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@thevladbog

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thevladbog
thevladbog merged commit 64e8986 into main Jul 18, 2026
33 checks passed
@thevladbog
thevladbog deleted the panel/p4.1-checkin-loop branch July 18, 2026 13:07
thevladbog added a commit that referenced this pull request Jul 18, 2026
…00020) (#78)

* docs: P4.1 check-in loop design spec

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: P4.1 check-in loop implementation plan

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Migration 000020: composite FKs for same-event check-in relationships

CodeRabbit review on PR #77 flagged that 000019's single-column FKs let
checkin_stations.zone_id, checkin_actions.attendee_id, and
checkin_actions.station_id point at a row from a different event than
the row's own event_id. The app layer already validates this on every
write path, but this closes the gap at the DB level as defense-in-depth
(000019 had already merged to main, so this is a new migration rather
than an in-place edit). Verified against a real Postgres 16 instance
that cross-event inserts are rejected and same-event/NULL cases still
succeed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add 000021: validate 000020's composite FKs outside the lock window

CodeRabbit nitpick on #78: adding a FK with a full validation scan holds
an ACCESS EXCLUSIVE lock on the referencing table for the scan's
duration. 000020's three composite FKs now go in NOT VALID (skips the
scan), and this new migration validates them separately via ALTER
TABLE ... VALIDATE CONSTRAINT, which only needs a SHARE UPDATE
EXCLUSIVE lock and doesn't block concurrent reads/writes. Verified
against real Postgres: convalidated=false right after 000020, flips to
true after 000021, and new-row rejection behavior is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: CI Bot <ci@example.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
thevladbog pushed a commit that referenced this pull request Jul 20, 2026
…in-memory copy

Addendum to the round-2 Finding 1 fix: the store's UPDATE now clears
test_passed_at when config actually changes, but PatchEquipmentDevice's
response was still built from the pre-fetched `existing` struct patched up
in memory — so a config-changing PATCH could return a stale non-null
test_passed_at for hardware that never passed a test. The handler now
re-reads the row via GetEquipmentDeviceForTenant after the UPDATE and
echoes that; a nil re-read (concurrent-delete race after a successful
UPDATE) maps to the house 404 shape, same soft-delete-race precedent as
PutCheckinSettings' ErrEventNotFound handling (PR #77 Finding C).

Contract tests: config-changing PATCH response has test_passed_at null;
rename-only PATCH response preserves it; vanished-row re-read is 404 (and
the re-read provably happens — two get calls). The rename test's fake is
now stateful so the re-read reflects the persisted update.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
thevladbog added a commit that referenced this pull request Jul 20, 2026
…etup wizards (#83)

* docs(spec): P4.3 equipment hub + device registry design

Board screens 5a-5d extracted (all four fully drawn); user-approved
decisions: camera deferred entirely, server-authoritative default
mirrored to agent, tenant-wide readiness rule, wedge+COM scanners,
normalized registry tables keyed by agent machine id (backend #5),
new agent GET /info for machine identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(plan): P4.3 equipment hub implementation plan — 11 tasks

Agent /info -> migration+store -> endpoints+regen -> readiness ->
panel info/cache -> registry hooks+reconcile -> hub page -> printer
wizard -> scanner wizard -> default-precedence repoint -> final sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* agent: GET /info — persisted machine id, hostname, version, uptime

* backend: equipment registry storage — migration 000023, models, store methods

Adds the P4.3 per-tenant, per-machine device registry storage layer: the
equipment_machines/equipment_devices tables (migration 000023, with the
partial-unique-index single-default-printer guarantee and the
class='printer' CHECK), the EquipmentMachine/EquipmentDevice models, and
the 8-method PGStore implementation (upsert-machine, get-machine+devices,
get-device-for-tenant, create/update/delete-device, set-default-printer,
mark-test-passed, tenant-has-tested-default-printer) behind the Store
interface's new Equipment Registry block, with ErrDeviceNotFound as its
not-found sentinel.

Covered by pgxmock tests pinning every statement's SQL, plus a
TEST_DATABASE_URL-gated real-Postgres integration test proving the schema
guarantees pgxmock can't (partial unique index, CHECK constraint, cascade
delete, cross-tenant disjointness) and the id = ANY($3) UUID-array bind
used by the seen-devices freshness touch.

* backend: equipment registry endpoints — tenant-scoped CRUD, default rule, test-passed

Adds the 7 P4.3 equipment-device-registry HTTP endpoints (PUT/GET machine,
POST device, PATCH/DELETE device, PUT default-printer, POST test-passed),
openapi.yaml documentation, contract tests, and the regenerated panel
client. ORG-level resources scoped by tenant_id alone (no
requireEventOwnership); 23505 (partial-unique-index race on
make_default) maps to a clean 409 instead of a raw constraint error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* backend: readiness equipment step — tenant has a tested default printer

GetEventReadiness now sources the equipment step from
TenantHasTestedDefaultPrinter(tenantID) instead of a hardcoded not_done.
Equipment still never blocks ready (spec §4.3), same as zones. Extends
the readiness contract tests with true/false/store-error cases and fixes
two other fakeStore fixtures (badge-template P3 readiness tests) that
now need the new store method wired to avoid a nil-func panic. openapi.yaml's
readiness description drops the "P3/P4 wiring" caveat and states the real
rule; panel schema.d.ts regenerated in the same commit.

* panel: agent /info client, cached machine identity, useAgentInfo with 8s retry

* panel: fix useAgentInfo review findings — gate info on enabled, warn-don't-fail cache write, machine_id-overwrite pin

* panel: equipment registry hooks + agent reconcile logic

Task 6 of P4.3: TanStack Query hooks over the generated equipment
endpoints (machine GET/PUT, device POST/PATCH/DELETE, default-printer
PUT, test-passed POST), each invalidating EQUIPMENT_MACHINE_KEY on
success, plus pure reconcile.ts matching registry devices against the
live agent by config.agent_name/port_name (never display_name) —
wedge scanners always resolve to Liveness "none" per the honesty rule.
Adds AgentScanner + agentClient.getScanners() (GET /scanners) as the
scanner-side counterpart to getPrinters.

* docs(spec): drop impossible ip:port cross-check — agent /printers carries name+type only

* panel: equipment hub — agent card, registry-reconciled device columns, agent-down state

Board 5a/5d: replaces the /equipment PlaceholderPage with the real hub.
AgentCard is a state machine over useAgentInfo (connected/connected_legacy/
checking/disconnected), rendering board 5a's meta line (base-url · version
· hostname · uptime) and board 5d's red "Start the agent" card (numbered
steps, download link, Retry, auto-retry caption — the actual 8s retry timer
already lives inside useAgentInfo). DeviceCard is one parameterized column
component reused for Printers/Scanners, reconciling the registry
(useEquipmentMachine) against the live agent (useAgentPrinters + a new
local useAgentScanners) via reconcile.ts: green/amber liveness dots (never
color-alone — amber pairs with visible "Saved · not seen since <date>"
text), no dot at all for USB-wedge scanners (the agent has no visibility
into them), unsaved live printers surfaced as a Save… affordance, and a
DEFAULT chip on the registry default. The whole grid grays to opacity-.55
with actions hidden when the agent is down, but stays readable from a
cached machine_id — it only fully hides for a true cold start (no cache,
agent down). The reconcile-on-load PUT (seen_device_ids) fires exactly
once per machine_id per visit via a ref guard, gated on info + the
registry settling + both live lists settling; an empty registry (404)
still upserts, since that's what registers a new machine. The overflow
menu (rename/set default/delete) reuses the house ConfirmDialog pattern.
Printer/scanner setup buttons and the unsaved-row Save affordance are
honestly disabled (data-testid="wizard-todo") — no fake wizard ahead of
Tasks 8/9. Also extends ReadinessCell's regression coverage for the
equipment step's newly-reachable "done" status (no component change
needed — the existing generic step renderer already handles it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* panel: equipment hub review fixes — success-gated reconcile, distinct registry-error state, v5 pending skeleton

Task 7 review round (2 Important + 3 Minor, all taken):
1. Reconcile upsert now fires only when BOTH live lists (printers/
   scanners) have succeeded — an errored fetch reads as an empty list and
   would under-report seen_device_ids (live devices later showing a false
   "not seen since"). A skipped attempt does not consume the
   once-per-machine_id ref budget, so a later successful refetch still
   reconciles exactly once (test drives error -> zero PUTs -> recovery ->
   exactly one PUT).
2. A genuine (non-404) machine-registry failure renders a distinct
   error state with Retry (ZonesPage's list-error shape, new
   equipmentRegistryLoadError key EN+RU) instead of masquerading as
   "No printers/scanners saved yet" — the exact silent-empty-list the
   board's 5d caption calls out. True 404 stays the empty state.
3. Loading skeleton now gates on v5-correct pending semantics
   (isPending for the enabled query + the pre-identity checking window)
   instead of isLoading, which is never true for a disabled query.
4. Added the missing clear-default overflow test (PUT device_id: null).
5. Pinned the legacy-agent-with-cached-identity edge: saved devices stay
   visible from the cached machine_id (a reachable-but-legacy agent must
   never show less than a fully dead one), hint renders, no reconcile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* panel: printer setup wizard — find/test/save with physical verification

Board 5b's 3-step wizard (Find -> Test -> Save) with physical
verification via a Cyrillic test label, plus a retest entry point for a
saved printer row's Test print button. Wires the hub's three printer
mount points (header chooser, per-column Set up printer, unsaved-row
Save) from Task 7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* panel: printer wizard review fixes — readable mirror warning, network address at Save, dismissal locks

Important 1: mirror failure no longer auto-closes in the same tick (the
warning never painted under React 18 batching + unanimated Radix
unmount) — dialog stays open with an explicit Close, BulkBar convention.
Important 2: a network printer without a known address collects ip/port
at the Save step instead of dead-ending on a guaranteed-400 create.
Minors: in-handler busy re-checks, complete dismissal locking (+tests),
Find list excludes registered printers, retest carries the real kind,
label-worded timeout copy (equipmentWizardTimeout, EN+RU).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* panel: scanner setup wizard — wedge listen-detect + COM port flow

Board 5c's ScannerWizard: a segmented USB-wedge | COM toggle, each leading
to its own listen step with a physical verification gate (Save/fields don't
exist until a real scan lands), plus a retest entry point mirroring Task 8's
controller-resolved pattern. useWedgeListen does window-scoped keydown
capture with its own inter-key-gap/silence-timeout detection (a different
technique from useScanInput's focused-input + Enter idiom, since this hook
must first tell a scanner apart from a human typing). agentClient gains
getScannerPorts/addComScanner/removeComScanner against agent/openapi.yaml's
already-documented /scanners/* routes. Hub wiring: "+ Set up scanner" and
the "+ Add device" chooser's Scanner option are now the real enabled
affordance (the last wizard-todo placeholder is gone); scanner rows get a
"Test scan" button (wedge rows always, live com rows); a saved com device's
delete flow best-effort releases its agent-side port, warning (not failing)
on mirror failure.

Controller correction honored: retest mode calls useMarkTestPassed on any
confirmed detection (wedge or com), since POST .../test-passed is
device-generic and only printer tests feed the readiness gate.

* panel: scanner wizard review fixes — discard-first COM listen, honest test_passed gating, abortable consume poll

Task 9 review round (1 Critical + 2 Important + 1 Minor):

- CRITICAL: the agent's scan buffer is process-wide (not per port/session),
  so the COM listen phase could pass a pre-session (or wrong-device) scan
  off as this session's physical verification — and retest auto-stamped
  test_passed_at from it with zero operator action. The poll is now an
  imperative effect (useScanInput's interval idiom) whose first successful
  consume per listen session is a pure discard; /scan/consume's atomic
  read-and-clear guarantees everything read after it arrived after
  listening began.
- Important-1 (reviewer's adjudication): Save no longer requires a
  detection — PrinterWizard's brief-mandated test_passed:false precedent
  governs. Fields are editable from the start; test_passed on create is
  simply whether a scan landed this session.
- Important-2: consumeLastScan accepts an optional AbortSignal and the
  poll's cleanup aborts the in-flight request on cancel/close — a
  post-close consume can no longer eat a scan the station needs.
- Minor: COM listen tests converted to fake timers (no real 700ms
  intervals; equipment suite ~3.5s).

* panel: wedge listen editable-target guard — typing in the wizard's fields can't fabricate a detection

Task 9 review round 2 (1 new Important + 1 Minor):

- Important: round 1's always-editable fields exposed the window-level
  wedge listener to fast human typing -- a 3+ char burst at wedge speed
  into the Device name input fabricated a detection (and a later
  test_passed:true) with zero physical scan. useWedgeListen now ignores
  keydowns targeted at editable elements (input/textarea/select/
  contenteditable) and clears the accumulator on such an event (no mixed
  bursts). Accepted, documented limitation (controller decision): a
  physical scan while a text field is focused won't detect -- its chars
  land in the field natively (wedge physics any app has); "Scan again"
  re-arms with button focus.
- Minor: the abort-on-cancel comment now states its honest scope -- the
  abort stops the client's wait and further polls; it cannot un-consume a
  request the agent already processed.

* panel: move equipment machine query to shared/agent (layering)

* panel: default-printer precedence — server registry wins over agent config

* panel: revert printers-gated registry lookup; fix root cause with harness-level agent-info/registry MSW defaults (task 10 review)

* panel: P4.3 whole-branch review polish — RU copy naturalness, no dangling meta separator

RU copy: past-tense equipmentNotSeenSince (matches equipmentNotSeenYet),
"штрихкод"/"сканирование" terminology consistency in the scan-confirmation
strings, and a comma-splice fix in equipmentDefaultFooter. Values only,
keys/EN unchanged.

AgentCard: build the connected meta line by joining only non-empty
segments so a blank hostname can't leave a dangling "· ·" gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(sdd): record P4.3 execution trail — 11 tasks, review rounds, final whole-branch review

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* agent: propagate generateUUIDv4 rand failure instead of Fatal-ing

CodeRabbit (PR #83): generateUUIDv4 called log.Fatalf on a crypto/rand
failure, but it's reachable from loadConfig's MachineID-upgrade branch,
which runs inside HTTP handlers (e.g. /printers) while holding configMu
— a transient RNG failure would os.Exit the whole agent mid-event
instead of failing one request.

generateUUIDv4 now returns (string, error); loadConfig's upgrade branch
propagates the error through its existing error return; main()'s
startup path still Fatalf's on its own discretion (nothing to serve
yet at that point). All loadConfig call sites already 500 or degrade
gracefully on error, so no handler behavior changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* backend: enforce default-printer device_id presence + per-kind config shapes

Two Codex findings (PR #83):

1. PUT /api/equipment/machines/{machine_id}/default-printer bound the
   request body's device_id into a bare *uuid.UUID, which json.Unmarshal
   leaves nil for both an absent key and an explicit null — so an
   accidentally-omitted device_id silently cleared the machine's default
   printer, identical to the documented clear-request. Fixed by decoding
   into a json.RawMessage field first (DisallowUnknownFields) so presence
   is observable: key absent -> 400 ("device_id is required; send null to
   clear"), null -> clear (unchanged), a uuid string -> set (invalid uuid
   -> 400).

2. validateEquipmentDeviceConfig shared one config shape across both
   printer kinds and both scanner kinds, so DisallowUnknownFields only
   caught genuinely unrecognized keys — a usb_wedge scanner could carry
   com's port_name, a com scanner could carry usb_wedge's terminator, and
   a system printer could carry network's ip/port/dpi, all stored
   verbatim and misleading reconciliation readers. Split into four
   per-kind decode shapes (network printer: agent_name/ip/port/dpi;
   system printer: agent_name only; com: port_name only; wedge:
   terminator only) so a cross-kind key is now rejected as unknown.

openapi.yaml updated to match (EquipmentDefaultPrinterRequest's required-
device_id behavior, EquipmentDeviceCreateRequest's per-kind key prose);
panel/src/shared/api/schema.d.ts regenerated via `npm run generate:api -w
panel` in this same commit (doc-comment diff only, no type-shape change).

Contract tests added for both: {} on default-printer -> 400, malformed
uuid -> 400; wedge+port_name, com+terminator, system printer+ip -> 400.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* panel: PR #83 bot-review round 1 fixes — equipment hub (10 findings)

Fixes all 10 CodeRabbit/Codex bot-review findings on the P4.3 equipment
hub (2 counted once as duplicates -> 8 distinct changes), TDD red-first
for each:

1/2. AgentCard's uptime meta line now routes through react-i18next
     (equipmentUptime, EN+RU) instead of a hardcoded English string;
     PrinterWizard's manual-add port guard now matches the Save-step
     address form's Number.isInteger 1..65535 rule instead of
     Number.isFinite (which let ""->0 and 70000+ through).
3. EquipmentPage/DeviceCard disable every Add/Set up/Save control
   while machineId is null (legacy agent, no cached identity) --
   their handlers already no-op there, so the affordance is now
   honest about it.
4. useCreateDevice/useDeleteDevice/useSetDefaultPrinter/
   useMarkTestPassed invalidate every event's readiness query (the
   [method, path-template] prefix idiom) since equipment is org-level
   but its readiness-gated content is per-event.
5. ScannerWizard's retest Close button (and the Escape/outside-click
   paths) now gate on markTestPassed's own pending state, so a late
   test-passed failure can no longer be silently dropped by closing
   mid-flight.
6. PrinterWizard's shared Test/Save footer now includes `printing` in
   Save's busy check, so a device row can no longer be created and the
   dialog closed while the physical test send is still in flight.
7. ScannerWizard's explicit Cancel button now gates on `comAdding`
   too, matching handleOpenChange/preventDialogDismiss.
8. useAgentPrinters resolves configuredDefault to null while a modern
   agent's registry query is still pending, instead of racing to the
   agent's own (possibly stale) configured default.
9. useEquipmentMachine sets retry:false -- a 404 there is a documented
   normal state, matching useAgentInfo/useAgentPrinters convention.
10. DeviceCard's row-menu trigger is now the @idento/ui Button
    (ghost/icon), replacing a hand-rolled <button>.

Gates: npm run test -w panel (121 files / 1382 tests), npm run
typecheck -w panel, npx eslint . -- all clean. Full report at
.superpowers/sdd/pr83-round1-panel-report.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: drop force-added internal fix reports from the branch — .superpowers/sdd is scratch, gitignored by design

* backend: equipment store — atomic test-pass stamp on create, unconditional clear on config PATCH (#83 round 2)

Finding 1: UpdateEquipmentDevice now clears test_passed_at whenever the
supplied config differs (jsonb-semantic) from the device's stored config,
enforced in the UPDATE's own CASE over the OLD row — a PATCH swapping in
different hardware can no longer leave a stale "tested" stamp that
TenantHasTestedDefaultPrinter's readiness check keeps trusting.

Finding 2: CreateEquipmentDevice takes testPassed and stamps
test_passed_at inside its own INSERT (RETURNING), replacing the handler's
separate post-create MarkEquipmentDeviceTestPassed call — closing the
window where the create could commit and the second write could fail,
leaving an already-visible, wrongly-unstamped device a retry would then
409/duplicate against.

Pinned pgxmock SQL updated for both statements; added config-changed vs
identical-config-rename cases and testPassed true/false cases. Added
real-Postgres integration subtests proving the jsonb-semantic CASE
behavior pgxmock can't evaluate. openapi.yaml PATCH device description
updated; panel client regenerated (npm run generate:api -w panel).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* backend: PATCH device response echoes the re-read row, not the stale in-memory copy

Addendum to the round-2 Finding 1 fix: the store's UPDATE now clears
test_passed_at when config actually changes, but PatchEquipmentDevice's
response was still built from the pre-fetched `existing` struct patched up
in memory — so a config-changing PATCH could return a stale non-null
test_passed_at for hardware that never passed a test. The handler now
re-reads the row via GetEquipmentDeviceForTenant after the UPDATE and
echoes that; a nil re-read (concurrent-delete race after a successful
UPDATE) maps to the house 404 shape, same soft-delete-race precedent as
PutCheckinSettings' ErrEventNotFound handling (PR #77 Finding C).

Contract tests: config-changing PATCH response has test_passed_at null;
rename-only PATCH response preserves it; vanished-row re-read is 404 (and
the re-read provably happens — two get calls). The rename test's fake is
now stateful so the re-read reflects the persisted update.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* panel: PR #83 bot-review round 2 fixes — equipment hub wizard races (9 findings)

Closes the two wizard state-machine races (stale session state surviving
close, letting a reopen auto-fire a print or auto-pass a retest off
leftover data), gates retest-confirm/Back on an in-flight test print,
swaps two hand-rolled list-item buttons for @idento/ui's Button, extracts
one isBusy per wizard, routes the test-label preview through i18n, mirrors
row-menu "Make default" onto the agent (warn-don't-fail, same idiom as the
wizard's own mirror), and adds usePatchDevice to the readiness-invalidation
set now that the backend clears test_passed_at on a config-changing PATCH.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: CI Bot <ci@example.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend documentation Improvements or additions to documentation panel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants