Update all page templates to accept an optional error message parameter and display it via the ErrorToast component when data loading fails.
312 lines
48 KiB
Go
312 lines
48 KiB
Go
// Code generated by templ - DO NOT EDIT.
|
||
|
||
// templ: version: v0.3.977
|
||
package templates
|
||
|
||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||
|
||
import "github.com/a-h/templ"
|
||
import templruntime "github.com/a-h/templ/runtime"
|
||
|
||
import "bookhoard/internal/handlers"
|
||
|
||
func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData, errorMessage string, 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 {
|
||
return templ_7745c5c3_CtxErr
|
||
}
|
||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||
if !templ_7745c5c3_IsBuffer {
|
||
defer func() {
|
||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err == nil {
|
||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||
}
|
||
}()
|
||
}
|
||
ctx = templ.InitializeContext(ctx)
|
||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||
if templ_7745c5c3_Var1 == nil {
|
||
templ_7745c5c3_Var1 = templ.NopComponent
|
||
}
|
||
ctx = templ.ClearChildren(ctx)
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Device Management - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script src=\"/static/toast.js\"></script><script src=\"/static/device-management.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><script src=\"/static/theme.js\"></script></head><body class=\"theme-{ user.Theme }\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Header(user, "/devices").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8\"><!-- Header Section --><div class=\"mb-8\"><div class=\"flex justify-between items-center\"><div><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Device Management</h1><p style=\"color: var(--text-secondary)\">Manage your reading devices and sync settings</p></div><button onclick=\"showAddDeviceModal()\" class=\"btn-primary px-4 py-2 rounded-lg\">➕ Add New Device</button></div></div><!-- Devices Grid --><div id=\"devices-container\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\"><!-- Empty state -->")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if len(devices) == 0 {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div id=\"empty-state\" class=\"text-center py-16 col-span-full\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📱</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Devices Yet</h3><p class=\"mb-4\">Add your reading devices to enable cross-device sync</p><button onclick=\"showAddDeviceModal()\" class=\"btn-primary px-4 py-2 rounded-lg\">Add Your First Device</button></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- Devices Grid -->")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if len(devices) > 0 {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div id=\"devices-grid\" class=\"grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
for _, device := range devices {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex items-start justify-between mb-4\"><div class=\"text-4xl\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if device.DeviceType == "koreader" {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "📖")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
} else if device.DeviceType == "kobo" {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "📚")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
} else if device.DeviceType == "web" {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "🌐")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
} else {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "📱")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div><div class=\"flex space-x-2\"><button onclick=\"showShelfMappings('{ device.ID }')\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-secondary); background-color: var(--bg-primary);\">📚</button> <button onclick=\"showDeviceSettings('{ device.ID }')\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-secondary); background-color: var(--bg-primary);\">⚙️</button></div></div><h3 class=\"text-lg font-semibold mb-1\" style=\"color: var(--text-primary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
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: 72, Col: 102}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h3><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
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: 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, "</p><div class=\"space-y-2 text-sm mb-4\"><div class=\"flex justify-between\"><span style=\"color: var(--text-secondary)\">Sync Status</span> ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if device.SyncEnabled {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span style=\"color: var(--accent)\">✓ Enabled</span>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
} else {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<span style=\"color: var(--text-secondary)\">✗ Disabled</span>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"flex justify-between\"><span style=\"color: var(--text-secondary)\">Last Sync</span> ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if device.LastSync != nil {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span style=\"color: var(--text-primary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
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: 86, Col: 97}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
} else {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span style=\"color: var(--text-primary)\">Never</span>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div><div class=\"flex justify-between\"><span style=\"color: var(--text-secondary)\">Last Seen</span> ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if device.LastSeen != nil {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span style=\"color: var(--text-primary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
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: 94, Col: 97}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</span>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
} else {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span style=\"color: var(--text-primary)\">Never</span>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div></div><!-- NEW: Sync URL & Token Management --><div class=\"border-t pt-4\" style=\"border-color: var(--border);\"><p class=\"text-xs font-semibold mb-2\" style=\"color: var(--text-secondary)\">DEVICE SYNC CONFIGURATION</p>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if device.DeviceType == "kobo" {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<!-- Kobo: Copy Full Sync URL --> <div class=\"mb-3\"><label class=\"block text-xs mb-1\" style=\"color: var(--text-secondary)\">Kobo Sync URL</label><div class=\"flex space-x-2\"><input type=\"text\" id=\"sync-url-{ device.ID }\" readonly value=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var6 string
|
||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(baseURL + "/api/sync/kobo/" + device.AuthToken)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 114, Col: 68}
|
||
}
|
||
_, 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, 26, "\" class=\"flex-1 px-3 py-2 text-xs rounded border\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"> <button onclick=\"copyToClipboard(document.getElementById('sync-url-{ device.ID }').value, 'Kobo sync URL', event)\" class=\"px-3 py-2 text-xs rounded hover:opacity-80\" style=\"background-color: var(--accent); color: white;\">📋 Copy</button></div><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary);\">Paste this URL into Kobo's <code class=\"px-1 py-0.5 rounded\" style=\"background-color: var(--bg-primary);\">api_endpoint</code> setting</p></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
if device.DeviceType == "koreader" {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<!-- KOReader: Copy Auth Token --> <div class=\"mb-3\"><label class=\"block text-xs mb-1\" style=\"color: var(--text-secondary)\">Auth Token (for plugin)</label><div class=\"flex space-x-2\"><input type=\"text\" id=\"auth-token-{ device.ID }\" readonly value=\"")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var7 string
|
||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(device.AuthToken)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 139, Col: 38}
|
||
}
|
||
_, 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, 28, "\" class=\"flex-1 px-3 py-2 text-xs rounded border\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border); font-family: monospace;\"> <button onclick=\"copyToClipboard(document.getElementById('auth-token-{ device.ID }').value, 'Auth token', event)\" class=\"px-3 py-2 text-xs rounded hover:opacity-80\" style=\"background-color: var(--accent); color: white;\">📋 Copy</button></div><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary);\">Enter this token in the KOReader plugin settings</p></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<!-- Regenerate Token Button --><button onclick=\"regenerateDeviceToken('{ device.ID }', event)\" class=\"w-full px-3 py-2 text-xs rounded border hover:opacity-80\" style=\"border-color: var(--border); color: var(--text-secondary); background-color: var(--bg-primary);\">🔄 Regenerate Token</button><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary);\">⚠️ Old token will immediately stop working</p></div></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><!-- Pending Registrations -->")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if len(pendingRegistrations) > 0 {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div id=\"pending-section\" class=\"mt-12\"><h2 class=\"text-2xl font-bold mb-4\" style=\"color: var(--text-primary)\">Pending Device Registrations</h2><div class=\"space-y-4\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
for _, reg := range pendingRegistrations {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div class=\"card p-4 rounded-lg border flex items-center justify-between\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var8 string
|
||
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: 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, 34, "</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||
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, "</p></div><div class=\"flex space-x-2\"><button onclick=\"approveDevice('{ reg.RegistrationID }')\" class=\"btn-primary px-4 py-2 rounded-lg text-sm\">Approve</button> <button onclick=\"rejectDevice('{ reg.RegistrationID }')\" class=\"btn-danger px-4 py-2 rounded-lg text-sm\">Reject</button></div></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<!-- Sync Queue Section --><div id=\"sync-queue-section\" class=\"mt-12 hidden\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-2xl font-bold\" style=\"color: var(--text-primary)\">Sync Queue</h2><button onclick=\"clearSyncQueue()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Clear Queue</button></div><div id=\"sync-queue\" class=\"space-y-3\"></div></div></div><!-- Add Device Modal --><div id=\"add-device-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Add New Device</h2><button onclick=\"hideAddDeviceModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><form id=\"add-device-form\" onsubmit=\"handleAddDevice(event)\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Device Name</label> <input type=\"text\" id=\"device-name\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"My Kindle Paperwhite\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Device Type</label> <select id=\"device-type\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"\">Select device type...</option> <option value=\"koreader\">KOReader (Kindle, Kobo, PocketBook)</option> <option value=\"kobo\">Kobo E-Reader</option> <option value=\"web\">Web Browser</option> <option value=\"mobile\">Mobile App</option></select></div><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Device Identifier</label> <input type=\"text\" id=\"device-identifier\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"Hardware ID or Serial Number\"><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">Enter your device's unique identifier</p></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"hideAddDeviceModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Register Device</button></div></form></div></div><!-- Device Settings Modal --><div id=\"device-settings-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Device Settings</h2><button onclick=\"hideDeviceSettingsModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><form id=\"device-settings-form\" onsubmit=\"handleSaveDeviceSettings(event)\"><input type=\"hidden\" id=\"settings-device-id\"> <input type=\"hidden\" id=\"settings-device-type\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Device Name</label> <input type=\"text\" id=\"settings-device-name\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div class=\"mb-4\"><label class=\"flex items-center space-x-3 cursor-pointer\"><input type=\"checkbox\" id=\"settings-sync-enabled\" checked class=\"w-5 h-5 rounded\"> <span style=\"color: var(--text-primary)\">Enable Sync</span></label><p class=\"text-xs mt-1 ml-8\" style=\"color: var(--text-secondary)\">Allow this device to sync reading progress</p></div><div class=\"mb-4\"><label class=\"flex items-center space-x-3 cursor-pointer\"><input type=\"checkbox\" id=\"settings-auto-sync\" checked class=\"w-5 h-5 rounded\"> <span style=\"color: var(--text-primary)\">Auto Sync</span></label><p class=\"text-xs mt-1 ml-8\" style=\"color: var(--text-secondary)\">Automatically sync changes</p></div><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Sync Frequency</label> <select id=\"settings-sync-frequency\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"1\">Every 1 minute</option> <option value=\"5\" selected>Every 5 minutes</option> <option value=\"15\">Every 15 minutes</option> <option value=\"30\">Every 30 minutes</option> <option value=\"60\">Every hour</option></select></div><div class=\"border-t pt-6 mb-6\" style=\"border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary)\">Collection View Settings</h3><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Configure how collections are displayed on this device</p><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Default View Mode</label> <select id=\"settings-view-mode\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"grid\">Grid View</option> <option value=\"list\">List View</option> <option value=\"compact\">Compact View</option></select></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Sort Collections By</label> <select id=\"settings-sort-order\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"name\">Collection Name</option> <option value=\"created\">Date Created</option> <option value=\"book_count\">Book Count</option> <option value=\"recent\">Recently Added</option></select></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Items Per Page</label> <select id=\"settings-items-per-page\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"12\">12 items</option> <option value=\"24\" selected>24 items</option> <option value=\"48\">48 items</option> <option value=\"96\">96 items</option></select></div><div class=\"mb-4\"><label class=\"flex items-center space-x-3 cursor-pointer\"><input type=\"checkbox\" id=\"settings-show-covers\" checked class=\"w-5 h-5 rounded\"> <span style=\"color: var(--text-primary)\">Show Cover Images</span></label><p class=\"text-xs mt-1 ml-8\" style=\"color: var(--text-secondary)\">Display book covers in collection views</p></div><div class=\"mb-4\"><label class=\"flex items-center space-x-3 cursor-pointer\"><input type=\"checkbox\" id=\"settings-show-progress\" class=\"w-5 h-5 rounded\"> <span style=\"color: var(--text-primary)\">Show Reading Progress</span></label><p class=\"text-xs mt-1 ml-8\" style=\"color: var(--text-secondary)\">Display progress indicators for books</p></div></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"hideDeviceSettingsModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"button\" onclick=\"handleRevokeDevice()\" class=\"btn-danger px-4 py-2 rounded-lg\">Revoke Device</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Save Settings</button></div></form></div></div><!-- Device Shelf Mappings Modal --><div id=\"shelf-mappings-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Collection to Shelf Mappings</h2><button onclick=\"hideShelfMappingsModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><input type=\"hidden\" id=\"mappings-device-id\"><div id=\"shelf-mappings-container\" class=\"space-y-4 mb-6\"><p style=\"color: var(--text-secondary)\">Loading mappings...</p></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"hideShelfMappingsModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Close</button> <button type=\"button\" onclick=\"showAddMappingModal()\" class=\"btn-primary px-4 py-2 rounded-lg\">➕ Add Mapping</button></div></div></div><!-- Add/Edit Shelf Mapping Modal --><div id=\"add-mapping-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Add Collection Mapping</h2><button onclick=\"hideAddMappingModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><form id=\"mapping-form\" onsubmit=\"handleSaveMapping(event)\"><input type=\"hidden\" id=\"mapping-device-id\"> <input type=\"hidden\" id=\"mapping-id\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Collection</label> <select id=\"mapping-collection\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"\">Select collection...</option></select></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Device Shelf Name</label> <input type=\"text\" id=\"mapping-shelf-name\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"e.g., Sci-Fi\"><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">Name of the shelf on this device</p></div><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Sync Direction</label> <select id=\"mapping-sync-direction\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"bidirectional\">Bidirectional (sync both ways)</option> <option value=\"book_to_device\">Bookhoard → Device</option> <option value=\"device_to_book\">Device → Bookhoard</option> <option value=\"none\">No sync</option></select></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"hideAddMappingModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Save Mapping</button></div></form></div></div><script>\n function getDeviceIcon(typeName) {\n const deviceIcons = {\n 'koreader': '📖',\n 'kobo': '📚',\n 'web': '🌐',\n 'mobile': '📱'\n };\n return deviceIcons[typeName] || '📱';\n }\n\n function showAddDeviceModal() {\n document.getElementById('add-device-modal').classList.remove('hidden');\n }\n\n function hideAddDeviceModal() {\n document.getElementById('add-device-modal').classList.add('hidden');\n document.getElementById('add-device-form').reset();\n }\n\n function handleAddDevice(event) {\n event.preventDefault();\n\n const data = {\n device_name: document.getElementById('device-name').value,\n device_type: document.getElementById('device-type').value,\n device_identifier: document.getElementById('device-identifier').value\n };\n\n fetch('/api/devices/register', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n },\n body: JSON.stringify(data)\n })\n .then(response => response.json())\n .then(result => {\n showToast('Device registered! Check your device for sync instructions.', 'success');\n hideAddDeviceModal();\n loadDevices();\n })\n .catch(error => {\n showToast('Failed to register device', 'error');\n });\n }\n\n function showDeviceSettings(deviceId) {\n const device = devices.find(d => d.id === deviceId);\n if (!device) return;\n\n document.getElementById('settings-device-id').value = deviceId;\n document.getElementById('settings-device-type').value = device.device_type;\n document.getElementById('settings-device-name').value = device.device_name;\n document.getElementById('settings-sync-enabled').checked = device.sync_enabled;\n document.getElementById('settings-auto-sync').checked = device.auto_sync;\n document.getElementById('settings-sync-frequency').value = device.sync_frequency_minutes;\n\n const deviceType = device.device_type;\n \n fetch('/api/collections', {\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => response.json())\n .then(result => {\n if (result.collections && result.collections.length > 0) {\n const firstCollection = result.collections[0];\n const viewSettings = firstCollection.view_settings || {};\n const deviceSettings = viewSettings[deviceType] || {};\n \n document.getElementById('settings-view-mode').value = deviceSettings.view_mode || 'grid';\n document.getElementById('settings-sort-order').value = deviceSettings.sort_order || 'name';\n document.getElementById('settings-items-per-page').value = deviceSettings.items_per_page || 24;\n document.getElementById('settings-show-covers').checked = deviceSettings.show_covers !== false;\n document.getElementById('settings-show-progress').checked = deviceSettings.show_progress || false;\n }\n })\n .catch(error => {\n console.error('Failed to load view settings:', error);\n });\n\n document.getElementById('device-settings-modal').classList.remove('hidden');\n }\n\n function showShelfMappings(deviceId) {\n document.getElementById('mappings-device-id').value = deviceId;\n loadShelfMappings(deviceId);\n document.getElementById('shelf-mappings-modal').classList.remove('hidden');\n }\n\n function hideShelfMappingsModal() {\n document.getElementById('shelf-mappings-modal').classList.add('hidden');\n }\n\n function loadShelfMappings(deviceId) {\n const container = document.getElementById('shelf-mappings-container');\n\n fetch(`/api/devices/${deviceId}/collections`, {\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => response.json())\n .then(result => {\n if (result.mappings && result.mappings.length > 0) {\n container.innerHTML = result.mappings.map(mapping => `\n <div class=\"card p-4 rounded-lg border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\">\n <div class=\"flex justify-between items-center\">\n <div>\n <h4 class=\"font-semibold\" style=\"color: var(--text-primary)\">${mapping.collection_name}</h4>\n <p class=\"text-sm\" style=\"color: var(--text-secondary)\">\n → Device Shelf: <strong>${mapping.device_shelf_name}</strong>\n </p>\n <p class=\"text-xs\" style=\"color: var(--text-secondary)\">\n Sync: ${mapping.sync_direction}\n </p>\n </div>\n <div class=\"flex space-x-2\">\n <button onclick=\"editMapping('${mapping.id}', '${mapping.collection_id}', '${mapping.device_shelf_name}', '${mapping.sync_direction}')\"\n class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-secondary); background-color: var(--bg-secondary);\">\n ✏️\n </button>\n <button onclick=\"deleteMapping('${mapping.id}')\"\n class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-secondary); background-color: var(--bg-secondary);\">\n 🗑️\n </button>\n </div>\n </div>\n </div>\n `).join('');\n } else {\n container.innerHTML = '<p style=\"color: var(--text-secondary)\">No shelf mappings configured. Click \"Add Mapping\" to create one.</p>';\n }\n })\n .catch(error => {\n container.innerHTML = '<p style=\"color: var(--error)\">Failed to load mappings</p>';\n });\n }\n\n function showAddMappingModal() {\n const deviceId = document.getElementById('mappings-device-id').value;\n document.getElementById('mapping-device-id').value = deviceId;\n document.getElementById('mapping-id').value = '';\n document.getElementById('mapping-collection').value = '';\n document.getElementById('mapping-shelf-name').value = '';\n document.getElementById('mapping-sync-direction').value = 'bidirectional';\n\n loadCollections();\n document.getElementById('add-mapping-modal').classList.remove('hidden');\n }\n\n function hideAddMappingModal() {\n document.getElementById('add-mapping-modal').classList.add('hidden');\n document.getElementById('mapping-form').reset();\n }\n\n function loadCollections() {\n const select = document.getElementById('mapping-collection');\n\n fetch('/api/collections', {\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => response.json())\n .then(result => {\n if (result.collections) {\n select.innerHTML = '<option value=\"\">Select collection...</option>' +\n result.collections.map(col => `<option value=\"${col.id}\">${col.name}</option>`).join('');\n }\n })\n .catch(error => {\n console.error('Failed to load collections', error);\n });\n }\n\n function editMapping(mappingId, collectionId, shelfName, syncDirection) {\n document.getElementById('mapping-id').value = mappingId;\n document.getElementById('mapping-collection').value = collectionId;\n document.getElementById('mapping-shelf-name').value = shelfName;\n document.getElementById('mapping-sync-direction').value = syncDirection;\n\n document.getElementById('add-mapping-modal').classList.remove('hidden');\n }\n\n function handleSaveMapping(event) {\n event.preventDefault();\n\n const deviceId = document.getElementById('mapping-device-id').value;\n const mappingId = document.getElementById('mapping-id').value;\n const isUpdate = mappingId !== '';\n\n const data = {\n collection_id: document.getElementById('mapping-collection').value,\n device_shelf_name: document.getElementById('mapping-shelf-name').value,\n sync_direction: document.getElementById('mapping-sync-direction').value\n };\n\n const url = isUpdate ? `/api/devices/${deviceId}/collections/${mappingId}` : `/api/devices/${deviceId}/collections`;\n const method = isUpdate ? 'PUT' : 'POST';\n\n fetch(url, {\n method: method,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n },\n body: JSON.stringify(data)\n })\n .then(response => response.json())\n .then(result => {\n showToast(isUpdate ? 'Mapping updated' : 'Mapping created', 'success');\n hideAddMappingModal();\n loadShelfMappings(deviceId);\n })\n .catch(error => {\n showToast('Failed to save mapping', 'error');\n });\n }\n\n function deleteMapping(mappingId) {\n if (!confirm('Are you sure you want to delete this mapping?')) return;\n\n const deviceId = document.getElementById('mappings-device-id').value;\n\n fetch(`/api/devices/${deviceId}/collections/${mappingId}`, {\n method: 'DELETE',\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => {\n if (response.ok) {\n showToast('Mapping deleted', 'success');\n loadShelfMappings(deviceId);\n } else {\n showToast('Failed to delete mapping', 'error');\n }\n })\n .catch(error => {\n showToast('Failed to delete mapping', 'error');\n });\n }\n\n function hideDeviceSettingsModal() {\n document.getElementById('device-settings-modal').classList.add('hidden');\n }\n\n function handleSaveDeviceSettings(event) {\n event.preventDefault();\n\n const deviceId = document.getElementById('settings-device-id').value;\n const deviceType = document.getElementById('settings-device-type')?.value || '';\n \n const deviceData = {\n device_name: document.getElementById('settings-device-name').value,\n sync_enabled: document.getElementById('settings-sync-enabled').checked,\n auto_sync: document.getElementById('settings-auto-sync').checked,\n sync_frequency_minutes: parseInt(document.getElementById('settings-sync-frequency').value)\n };\n\n const viewSettings = {\n view_mode: document.getElementById('settings-view-mode').value,\n sort_order: document.getElementById('settings-sort-order').value,\n items_per_page: parseInt(document.getElementById('settings-items-per-page').value),\n show_covers: document.getElementById('settings-show-covers').checked,\n show_progress: document.getElementById('settings-show-progress').checked\n };\n\n fetch(`/api/devices/${deviceId}`, {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n },\n body: JSON.stringify(deviceData)\n })\n .then(response => response.json())\n .then(result => {\n return fetch('/api/collections', {\n method: 'GET',\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n });\n })\n .then(response => response.json())\n .then(result => {\n const updatePromises = result.collections.map(collection => {\n const currentSettings = collection.view_settings || {};\n currentSettings[deviceType] = viewSettings;\n \n return fetch(`/api/collections/${collection.id}`, {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n },\n body: JSON.stringify({\n ...collection,\n view_settings: currentSettings\n })\n });\n });\n \n return Promise.all(updatePromises);\n })\n .then(() => {\n showToast('Device settings saved', 'success');\n hideDeviceSettingsModal();\n location.reload();\n })\n .catch(error => {\n showToast('Failed to save settings', 'error');\n });\n }\n\n function handleRevokeDevice() {\n const deviceId = document.getElementById('settings-device-id').value;\n if (!confirm('Are you sure you want to revoke this device? It will no longer be able to sync.')) return;\n\n fetch(`/api/devices/${deviceId}`, {\n method: 'DELETE',\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => {\n if (response.ok) {\n showToast('Device revoked successfully', 'success');\n hideDeviceSettingsModal();\n location.reload();\n } else {\n showToast('Failed to revoke device', 'error');\n }\n })\n .catch(error => {\n showToast('Failed to revoke device', 'error');\n });\n }\n\n function approveDevice(registrationId) {\n fetch(`/api/devices/approve/${registrationId}`, {\n method: 'GET',\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => response.json())\n .then(result => {\n showToast('Device approved successfully', 'success');\n location.reload();\n })\n .catch(error => {\n showToast('Failed to approve device', 'error');\n });\n }\n\n function rejectDevice(registrationId) {\n fetch(`/api/devices/reject/${registrationId}`, {\n method: 'POST',\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => response.json())\n .then(result => {\n showToast('Device registration rejected', 'info');\n location.reload();\n })\n .catch(error => {\n showToast('Failed to reject device', 'error');\n });\n }\n\n function clearSyncQueue() {\n if (!confirm('Are you sure you want to clear all sync queue items?')) return;\n\n fetch('/api/queue/clear', {\n method: 'DELETE',\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => {\n if (response.ok) {\n showToast('Sync queue cleared', 'success');\n } else {\n showToast('Failed to clear queue', 'error');\n }\n });\n }\n\n function logout() {\n localStorage.removeItem('token');\n window.location.href = '/login';\n }\n\n // Load devices on page load\n loadDevices();\n </script>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = ErrorToast(errorMessage).Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</body></html>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
var _ = templruntime.GeneratedTemplate
|