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:
1 parent
8fbacd9f46
commit
fef79b1836
9 files changed
+253
-313
No files matched your search
+31
-26
@@ -1152,7 +1152,6 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
|
||||
var errorMsg string
|
||||
var devices []handlers.DeviceInfo
|
||||
var pendingList []templates.PendingRegistrationData
|
||||
|
||||
devices, err = cfg.DeviceHandler.GetDevicesData(c)
|
||||
if err != nil {
|
||||
@@ -1161,22 +1160,40 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
errorMsg = "Error loading devices"
|
||||
}
|
||||
|
||||
pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
||||
if err != nil {
|
||||
log.Printf("GetPendingRegistrationsData failed: %v", err)
|
||||
pendingList = []templates.PendingRegistrationData{}
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Error loading pending registrations"
|
||||
}
|
||||
} else {
|
||||
pendingList = convertPending(pendingMaps)
|
||||
}
|
||||
|
||||
// Get base URL from database config with fallback to config/env var
|
||||
baseURL := cfg.getBaseURL(c.Request().Context())
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.Devices(user, devices, pendingList, errorMsg, baseURL).Render(c.Request().Context(), &buf)
|
||||
err = templates.Devices(user, devices, errorMsg, baseURL).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Pairing-code lookup partial: renders the confirm card (or the
|
||||
// reason nothing matched) into #pairing-lookup-result. Errors render
|
||||
// as content — htmx only swaps 2xx bodies, and the pairing section is
|
||||
// a web-only surface (the JSON API route keeps real status codes).
|
||||
frontendProtected.POST("/devices/pairing/lookup", func(c *echo.Context) error {
|
||||
userID, ok := c.Get("user_id").(string)
|
||||
if !ok {
|
||||
return c.HTML(http.StatusUnauthorized, "")
|
||||
}
|
||||
|
||||
status := "found"
|
||||
var deviceName, deviceType, pairingCode string
|
||||
registration, err := handlers.ResolvePairingCode(userID, c.RealIP(), c.FormValue("pairing_code"))
|
||||
if err != nil {
|
||||
status = handlers.PairingErrorStatus(err)
|
||||
} else {
|
||||
deviceName = registration.DeviceName
|
||||
deviceType = registration.DeviceType
|
||||
pairingCode = registration.PairingCode
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.PairingLookupResult(deviceName, deviceType, pairingCode, status).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1834,7 +1851,6 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
}
|
||||
var errorMsg string
|
||||
var devices []handlers.DeviceInfo
|
||||
var pendingList []templates.PendingRegistrationData
|
||||
|
||||
devices, err = cfg.DeviceHandler.GetDevicesData(c)
|
||||
if err != nil {
|
||||
@@ -1843,22 +1859,11 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
errorMsg = "Error loading devices"
|
||||
}
|
||||
|
||||
pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
||||
if err != nil {
|
||||
log.Printf("GetPendingRegistrationsData failed: %v", err)
|
||||
pendingList = []templates.PendingRegistrationData{}
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Error loading pending registrations"
|
||||
}
|
||||
} else {
|
||||
pendingList = convertPending(pendingMaps)
|
||||
}
|
||||
|
||||
// Get base URL from database config with fallback to config/env var
|
||||
baseURL := cfg.getBaseURL(c.Request().Context())
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.Devices(user, devices, pendingList, errorMsg, baseURL).Render(c.Request().Context(), &buf)
|
||||
err = templates.Devices(user, devices, errorMsg, baseURL).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user