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
167 lines
7.7 KiB
Templ
167 lines
7.7 KiB
Templ
package templates
|
|
|
|
import "bookhoard/internal/handlers"
|
|
|
|
templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.QueueStatsResponse) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Sync Queue - Bookhoard</title>
|
|
<script src="/static/htmx.min.js"></script>
|
|
<link href="/static/style.css" rel="stylesheet"/>
|
|
</head>
|
|
<body x-data="queue" class="theme-{ user.Theme }">
|
|
@Header(user, "/queue")
|
|
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
|
<div class="mb-8">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Sync Queue</h1>
|
|
<p style="color: var(--text-secondary)">Monitor and manage offline sync queue</p>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<button @click="processPendingItems()" class="btn-primary px-4 py-2 rounded-lg">
|
|
Process Queue
|
|
</button>
|
|
<button @click="clearFailedItems()" class="btn-secondary px-4 py-2 rounded-lg">
|
|
Clear Failed
|
|
</button>
|
|
<button @click="clearAllItems()" class="btn-danger px-4 py-2 rounded-lg">
|
|
Clear All
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="stats-bar" class="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="text-3xl font-bold" style="color: #f59e0b;">{ stats.PendingCount }</div>
|
|
<div class="text-sm" style="color: var(--text-secondary);">Pending</div>
|
|
</div>
|
|
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="text-3xl font-bold" style="color: #3b82f6;">{ stats.ProcessingCount }</div>
|
|
<div class="text-sm" style="color: var(--text-secondary);">Processing</div>
|
|
</div>
|
|
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="text-3xl font-bold" style="color: #10b981;">{ stats.CompletedCount }</div>
|
|
<div class="text-sm" style="color: var(--text-secondary);">Completed</div>
|
|
</div>
|
|
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="text-3xl font-bold" style="color: #ef4444;">{ stats.FailedCount }</div>
|
|
<div class="text-sm" style="color: var(--text-secondary);">Failed</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card p-4 rounded-lg border mb-6" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="flex flex-wrap gap-4 items-center">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Status Filter</label>
|
|
<select
|
|
id="status-filter"
|
|
onchange="filterQueue()"
|
|
class="px-4 py-2 border rounded-lg"
|
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
|
>
|
|
<option value="all">All Items</option>
|
|
<option value="pending" selected>Pending</option>
|
|
<option value="processing">Processing</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="failed">Failed</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Type</label>
|
|
<select
|
|
id="type-filter"
|
|
onchange="filterQueue()"
|
|
class="px-4 py-2 border rounded-lg"
|
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
|
>
|
|
<option value="all">All Types</option>
|
|
<option value="progress">Progress</option>
|
|
<option value="note">Notes</option>
|
|
<option value="highlight">Highlights</option>
|
|
<option value="bookmark">Bookmarks</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device</label>
|
|
<select
|
|
id="device-filter"
|
|
onchange="filterQueue()"
|
|
class="px-4 py-2 border rounded-lg"
|
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
|
>
|
|
<option value="all">All Devices</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex-1"></div>
|
|
<button @click="refreshQueue()" class="btn-secondary px-4 py-2 rounded-lg">
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="loading" class="hidden text-center py-16" style="color: var(--text-secondary)">
|
|
<div class="loading-spinner mx-auto mb-4"></div>
|
|
<p>Loading queue...</p>
|
|
</div>
|
|
<div id="empty-state" class="hidden text-center py-16" 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)">Queue Empty</h3>
|
|
<p>No items in the sync queue</p>
|
|
</div>
|
|
<div id="queue-list" class="space-y-3">
|
|
if len(queueItems) == 0 {
|
|
<div class="text-center py-16" 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)">Queue Empty</h3>
|
|
<p>No items in the sync queue</p>
|
|
</div>
|
|
}
|
|
for _, item := range queueItems {
|
|
<div class="card p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<div class="flex items-center space-x-3">
|
|
<span class="inline-block px-2 py-0.5 rounded text-[11px] font-semibold uppercase status-{ item.Status }">{ item.Status }</span>
|
|
<span class="text-sm" style="color: var(--text-secondary)">{ item.SyncType }</span>
|
|
</div>
|
|
<span class="inline-block px-2 py-0.5 rounded text-[10px] font-semibold priority-{ item.Priority }">P{ item.Priority }</span>
|
|
</div>
|
|
<div class="text-sm mb-2" style="color: var(--text-secondary)">
|
|
Device ID: { item.DeviceID }
|
|
if item.MediaTitle != nil {
|
|
| Book: { *item.MediaTitle }
|
|
}
|
|
</div>
|
|
<div class="flex justify-between items-center text-sm" style="color: var(--text-secondary)">
|
|
<span>Attempts: { item.Attempts }/{ item.MaxAttempts }</span>
|
|
<span>{ item.CreatedAt }</span>
|
|
</div>
|
|
if item.ErrorMessage != nil {
|
|
<div class="mt-2 text-sm" style="color: #ef4444;">
|
|
Error: { *item.ErrorMessage }
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
<div id="queue-item-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" style="background-color: rgba(0, 0, 0, 0.7);">
|
|
<div class="card rounded-lg p-6 w-full max-w-2xl mx-4 my-8" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Queue Item Details</h2>
|
|
<button @click="hideQueueItemModal()" class="p-2 hover:opacity-80 rounded-lg" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
|
X
|
|
</button>
|
|
</div>
|
|
<div id="queue-item-details"></div>
|
|
<div class="flex justify-end space-x-3 mt-6">
|
|
<button @click="hideQueueItemModal()" class="btn-secondary px-4 py-2 rounded-lg">Close</button>
|
|
<button @click="retryQueueItem()" class="btn-primary px-4 py-2 rounded-lg">Retry</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
}
|