From e9cd445ff32a87ac5f3bfd36fac7208133e07b8a Mon Sep 17 00:00:00 2001
From: John O'Keefe
Date: Fri, 13 Feb 2026 12:12:38 -0500
Subject: [PATCH] feat: add device token management UI
- Display sync URLs for Kobo devices with copy button
- Display auth tokens for KOReader devices with copy button
- Add regenerate token button with confirmation
- Show warning about token invalidation
---
internal/router/frontend.go | 2 +-
templates/devices.templ | 78 ++++++++++++++++++++--
templates/devices_templ.go | 128 ++++++++++++++++++++++++------------
3 files changed, 158 insertions(+), 50 deletions(-)
diff --git a/internal/router/frontend.go b/internal/router/frontend.go
index 5c39575..7af29fc 100644
--- a/internal/router/frontend.go
+++ b/internal/router/frontend.go
@@ -168,7 +168,7 @@ func registerFrontendRoutes(cfg *Config) {
}
pendingList := convertPending(pendingMaps)
var buf bytes.Buffer
- err = templates.Devices(user, devices, pendingList).Render(c.Request().Context(), &buf)
+ err = templates.Devices(user, devices, pendingList, cfg.Cfg.BaseURL).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
diff --git a/templates/devices.templ b/templates/devices.templ
index 32074ec..a4837c8 100644
--- a/templates/devices.templ
+++ b/templates/devices.templ
@@ -2,17 +2,18 @@ package templates
import "bookhoard/internal/handlers"
-templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData) {
+templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData, baseURL string) {
Device Management - Bookhoard
-
-
-
-
+
+
+
+
+
@Header(user, "/devices")
@@ -70,7 +71,7 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
{ device.DeviceName }
{ device.DeviceType }
-
+
Sync Status
if device.SyncEnabled {
@@ -96,6 +97,71 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
}
+
+
+
+
DEVICE SYNC CONFIGURATION
+
+ if device.DeviceType == "kobo" {
+
+
+
+
+
+
+
+
Paste this URL into Kobo's api_endpoint setting
+
+ }
+
+ if device.DeviceType == "koreader" {
+
+
+
+
+
+
+
+
Enter this token in the KOReader plugin settings
+
+ }
+
+
+
+
⚠️ Old token will immediately stop working
+
}
diff --git a/templates/devices_templ.go b/templates/devices_templ.go
index ab69003..b553ca1 100644
--- a/templates/devices_templ.go
+++ b/templates/devices_templ.go
@@ -10,7 +10,7 @@ import templruntime "github.com/a-h/templ/runtime"
import "bookhoard/internal/handlers"
-func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData) templ.Component {
+func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData, baseURL string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -31,7 +31,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Device Management - Bookhoard")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Device Management - Bookhoard")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -91,7 +91,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(device.DeviceName)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 71, Col: 102}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 72, Col: 102}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -104,13 +104,13 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(device.DeviceType)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 72, Col: 89}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 73, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
Sync Status ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
Sync Status ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -137,7 +137,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(device.LastSync.Format("2006-01-02 15:04"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 85, Col: 97}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 86, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -165,7 +165,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(device.LastSeen.Format("2006-01-02 15:04"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 93, Col: 97}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 94, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -181,76 +181,118 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
DEVICE SYNC CONFIGURATION
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if device.DeviceType == "kobo" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if device.DeviceType == "koreader" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
⚠️ Old token will immediately stop working
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(pendingRegistrations) > 0 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Pending Device Registrations
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
Pending Device Registrations
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, reg := range pendingRegistrations {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- var templ_7745c5c3_Var6 string
- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceName)
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 112, Col: 87}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- var templ_7745c5c3_Var7 string
- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceType)
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 114, Col: 27}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " - Expires in ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
- templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(reg.ExpiresAt)
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceName)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 114, Col: 58}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 178, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceType)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 180, Col: 27}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " - Expires in ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(reg.ExpiresAt)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 180, Col: 58}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}