Files
bookhoard/templates/devices.templ
T
john-okeefe af7533529c refactor(templates): remove duplicate main.js script tags, consolidate to header component
Remove redundant <script src="/static/main.js" defer></script> tags from 17+
templates that include the @Header component, eliminating duplicate script
loading that was causing Alpine.js to initialize twice per page load.

The header.templ component now serves as the single source of truth for
main.js inclusion, following the DRY principle and ensuring consistent
script loading across all pages that use the header navigation.

Additionally, add type="button" attribute to all buttons in header navigation
to prevent default form submission behavior when buttons are clicked.

Changes:
- Remove main.js script tag from templates using @Header component
- Keep main.js in header.templ (line 279) as universal inclusion point
- Preserve main.js in special pages: index.templ, login.templ, register.templ
  (these don't use @Header and are standalone entry points)
- Add type="button" to theme toggle, theme selection, wood paneling, and user menu buttons
  to prevent unwanted form submissions or page navigation

Benefits:
- Eliminates Alpine.js double-initialization bug
- Reduces HTTP requests (one script load instead of two)
- Improves maintainability (add header, get scripts automatically)
- Fixes broken @click handlers on collections, devices, and other pages
- Prevents buttons from triggering default form submission behavior

Technical notes:
- Templates affected: admin, analytics, bookshelf, collection_rules,
  collections, conflicts, custom_section, dashboard, devices, docs,
  library, profile, progress, queue, unlinked_books
- No changes to entry pages (index, login, register) which don't use @Header
- HTMX script remains in individual templates (stateless, no double-load issue)
- All interactive buttons in header now explicitly marked type="button" to
  prevent default browser form submission behavior

Related to: previous commit fixing Vite code-splitting
2026-03-13 22:25:12 -04:00

478 lines
22 KiB
Templ
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package templates
import "bookhoard/internal/handlers"
templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData, errorMessage string, baseURL string) {
<!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>
<link href="/static/style.css" rel="stylesheet"/>
</head>
<body class="theme-{ user.Theme }" x-data="devices" x-init="setupEventDelegation()">
@Header(user, "/devices")
<div class="w-full 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 @click="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 len(devices) == 0 {
<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 @click="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg">
Add Your First Device
</button>
</div>
}
<!-- Devices Grid -->
if len(devices) > 0 {
<div id="devices-grid" class="grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
for _, device := range devices {
<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 device.DeviceType == "koreader" {
📖
} else if device.DeviceType == "kobo" {
📚
} else if device.DeviceType == "web" {
🌐
} else {
📱
}
</div>
<div class="flex space-x-2">
<button @click="showShelfMappings('{ device.ID }')" class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
📚
</button>
<button @click="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)">{ device.DeviceName }</h3>
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ device.DeviceType }</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 device.SyncEnabled {
<span style="color: var(--accent)"> Enabled</span>
} else {
<span style="color: var(--text-secondary)"> Disabled</span>
}
</div>
<div class="flex justify-between">
<span style="color: var(--text-secondary)">Last Sync</span>
if device.LastSync != nil {
<span style="color: var(--text-primary)">{ device.LastSync.Format("2006-01-02 15:04") }</span>
} else {
<span style="color: var(--text-primary)">Never</span>
}
</div>
<div class="flex justify-between">
<span style="color: var(--text-secondary)">Last Seen</span>
if device.LastSeen != nil {
<span style="color: var(--text-primary)">{ device.LastSeen.Format("2006-01-02 15:04") }</span>
} else {
<span style="color: var(--text-primary)">Never</span>
}
</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 device.DeviceType == "kobo" {
<!-- 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={ baseURL + "/api/sync/kobo/" + device.AuthToken }
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
@click="copyToClipboard(document.getElementById('sync-url-{ device.ID }').value, 'Kobo sync URL')"
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 device.DeviceType == "koreader" {
<!-- 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={ device.AuthToken }
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
@click="copyToClipboard(document.getElementById('auth-token-{ device.ID }').value, 'Auth token')"
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>
}
<!-- Regenerate Token Button -->
<button
hx-put={ "/api/devices/" + device.ID.String() + "/regenerate-token" }
hx-confirm="⚠️ Old token will immediately stop working. Regenerate anyway?"
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>
}
</div>
}
</div>
<!-- Pending Registrations -->
if len(pendingRegistrations) > 0 {
<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">
for _, reg := range pendingRegistrations {
<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)">{ reg.DeviceName }</h3>
<p class="text-sm" style="color: var(--text-secondary)">
{ reg.DeviceType } - Expires in { reg.ExpiresAt }
</p>
</div>
<div class="flex space-x-2">
<button @click="approveDevice('{ reg.RegistrationID }')" class="btn-primary px-4 py-2 rounded-lg text-sm">
Approve
</button>
<button @click="rejectDevice('{ reg.RegistrationID }')" class="btn-danger px-4 py-2 rounded-lg text-sm">
Reject
</button>
</div>
</div>
}
</div>
</div>
}
<!-- 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 @click="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 @click="hideAddDeviceModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button>
</div>
<form id="add-device-form" @submit="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" @click="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 @click="hideDeviceSettingsModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button>
</div>
<form id="device-settings-form" @submit="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" @click="hideDeviceSettingsModal()" class="btn-secondary px-4 py-2 rounded-lg">
Cancel
</button>
<button type="button" @click="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 @click="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" @click="hideShelfMappingsModal()" class="btn-secondary px-4 py-2 rounded-lg">
Close
</button>
<button type="button" @click="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 @click="hideAddMappingModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button>
</div>
<form id="mapping-form" @submit="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" @click="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>
@ErrorToast(errorMessage)
</body>
</html>
}