Files
bookhoard/templates/devices.templ
T
john-okeefe b3c0c0c225 feat(handlers): Add GetDevicesData helper and update devices template for SSR
- Add GetDevicesData() to DeviceHandler (returns raw data, not JSON)
- Update devices template signature to accept pre-rendered data
- Add server-side rendering of devices and pending registrations
- Update JavaScript to use location.reload() after CRUD operations
- Remove getDeviceIcon dependency on JavaScript function
- Use templ if/else instead of ternary operators for device status

Preserves all API endpoints and backward compatibility
2026-01-31 22:42:21 -05:00

431 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
templ Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegistrationData) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Device Management - Bookmann</title>
<script src="/static/htmx.min.js"></script>
<script src="/static/toast.js"></script>
<link href="/static/style.css" rel="stylesheet">
<script src="/static/theme.js"></script>
</head>
<body class="theme-{ user.Theme }">
@Header(user, "/devices")
<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 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 onclick="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 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)">{ device.DeviceName }</h3>
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ device.DeviceType }</p>
<div class="space-y-2 text-sm">
<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 != "" {
<span style="color: var(--text-primary)">{ device.LastSync }</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 != "" {
<span style="color: var(--text-primary)">{ device.LastSeen }</span>
} else {
<span style="color: var(--text-primary)">Never</span>
}
</div>
</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 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>
}
</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 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">
<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="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>
<script>
function getDeviceIcon(typeName) {
const deviceIcons = {
'koreader': '📖',
'kobo': '📚',
'web': '🌐',
'mobile': '📱'
};
return deviceIcons[typeName] || '📱';
}
function showAddDeviceModal() {
document.getElementById('add-device-modal').classList.remove('hidden');
}
function hideAddDeviceModal() {
document.getElementById('add-device-modal').classList.add('hidden');
document.getElementById('add-device-form').reset();
}
function handleAddDevice(event) {
event.preventDefault();
const data = {
device_name: document.getElementById('device-name').value,
device_type: document.getElementById('device-type').value,
device_identifier: document.getElementById('device-identifier').value
};
fetch('/api/devices/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
showToast('Device registered! Check your device for sync instructions.', 'success');
hideAddDeviceModal();
loadDevices();
})
.catch(error => {
showToast('Failed to register device', 'error');
});
}
function showDeviceSettings(deviceId) {
const device = devices.find(d => d.id === deviceId);
if (!device) return;
document.getElementById('settings-device-id').value = deviceId;
document.getElementById('settings-device-name').value = device.device_name;
document.getElementById('settings-sync-enabled').checked = device.sync_enabled;
document.getElementById('settings-auto-sync').checked = device.auto_sync;
document.getElementById('settings-sync-frequency').value = device.sync_frequency_minutes;
document.getElementById('device-settings-modal').classList.remove('hidden');
}
function hideDeviceSettingsModal() {
document.getElementById('device-settings-modal').classList.add('hidden');
}
function handleSaveDeviceSettings(event) {
event.preventDefault();
const deviceId = document.getElementById('settings-device-id').value;
const data = {
device_name: document.getElementById('settings-device-name').value,
sync_enabled: document.getElementById('settings-sync-enabled').checked,
auto_sync: document.getElementById('settings-auto-sync').checked,
sync_frequency_minutes: parseInt(document.getElementById('settings-sync-frequency').value)
};
fetch(`/api/devices/${deviceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
showToast('Device settings saved', 'success');
hideDeviceSettingsModal();
location.reload();
})
.catch(error => {
showToast('Failed to save settings', 'error');
});
}
function handleRevokeDevice() {
const deviceId = document.getElementById('settings-device-id').value;
if (!confirm('Are you sure you want to revoke this device? It will no longer be able to sync.')) return;
fetch(`/api/devices/${deviceId}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (response.ok) {
showToast('Device revoked successfully', 'success');
hideDeviceSettingsModal();
location.reload();
} else {
showToast('Failed to revoke device', 'error');
}
})
.catch(error => {
showToast('Failed to revoke device', 'error');
});
}
function approveDevice(registrationId) {
fetch(`/api/devices/approve/${registrationId}`, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => response.json())
.then(result => {
showToast('Device approved successfully', 'success');
location.reload();
})
.catch(error => {
showToast('Failed to approve device', 'error');
});
}
function rejectDevice(registrationId) {
fetch(`/api/devices/reject/${registrationId}`, {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => response.json())
.then(result => {
showToast('Device registration rejected', 'info');
location.reload();
})
.catch(error => {
showToast('Failed to reject device', 'error');
});
}
function clearSyncQueue() {
if (!confirm('Are you sure you want to clear all sync queue items?')) return;
fetch('/api/queue/clear', {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
})
.then(response => {
if (response.ok) {
showToast('Sync queue cleared', 'success');
} else {
showToast('Failed to clear queue', 'error');
}
});
}
function logout() {
localStorage.removeItem('token');
window.location.href = '/login';
}
// Load devices on page load
loadDevices();
</script>
</body>
</html>
}