feat(devices): pair-a-device card replaces the pending registration list

The Devices page's Pending Device Registrations section (Approve/Reject
cards broadcast to every account) is replaced by an always-visible Pair
a Device field: type the 6-digit code shown on the registering e-reader,
a templ-rendered confirm card shows what will be bound, and confirming
claims the device to the signed-in account.

Rendering ownership (AGENTS.md): the confirm card and every lookup
outcome are templ partials (PairingLookupResult) swapped by htmx from a
new SSR route POST /devices/pairing/lookup — the server decides what UI
looks like, no JSON-built HTML. Errors render as content because htmx
only swaps 2xx bodies (the route is web-only; the JSON API keeps real
status codes), and one delegated htmx:responseError listener in
device-management.ts toasts the non-swap failure path.

The claim confirm button posts straight to /api/devices/claim; success
comes back as HX-Refresh (same pattern as token regeneration), so the
page reloads into the paired state. No new TS module is needed — plain
htmx attributes drive the whole flow.

Ride-alongs: the Add New Device modal loses the device-type select and
the kobo manual form (its post-register UX depended on the broadcast
list and silently produced an unclaimable registration; native Kobo
sync is still server-side-only and will get its own pairing UX when it
lands) — the modal is now a single Connect-a-KOReader-Device guide with
the pairing steps. PendingRegistrationData, convertPending, and the
pending plumbing in the page handler are gone with the section.
ui-controls.md glossary and tests/ui COVERAGE.md updated in the same
commit as the control rename, per the drift gates.
This commit is contained in:
John O'Keefe committed 2026-11-02 16:11:15 -05:00
1 parent 8fbacd9f46
commit fef79b1836
9 files changed
+253 -313

No files matched your search

-13
View File
@@ -82,19 +82,6 @@ func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, err
}, nil
}
func convertPending(pending []map[string]interface{}) []templates.PendingRegistrationData {
result := make([]templates.PendingRegistrationData, len(pending))
for i, p := range pending {
result[i] = templates.PendingRegistrationData{
RegistrationID: p["registration_id"].(string),
DeviceName: p["device_name"].(string),
DeviceType: p["device_type"].(string),
ExpiresAt: p["expires_at"].(time.Time).Format(time.RFC3339),
}
}
return result
}
func pingDB(cfg *Config, ctx context.Context) error {
if cfg.DBPool != nil {
if pool, ok := cfg.DBPool.(*pgxpool.Pool); ok {