feat: Add SSR for conflicts and queue pages
- Update conflicts.templ to accept pre-rendered data - Update queue.templ to accept pre-rendered data - Update /conflicts and /queue routes in main.go for SSR - Update stats rendering to use server-side values - Add server-side conflict list rendering - Add server-side queue list rendering - Update conflicts.js to use location.reload() after operations - Update queue.js to use location.reload() after operations - Remove initial load calls from JavaScript files Preserves all API endpoints and backward compatibility
This commit is contained in:
+89
-6
@@ -423,7 +423,7 @@ func main() {
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Device Management route (protected)
|
||||
// Device Management route (protected) - SSR version
|
||||
protected.GET("/devices", func(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userEmail := c.Get("user_email").(string)
|
||||
@@ -437,15 +437,63 @@ func main() {
|
||||
Role: userRole,
|
||||
}
|
||||
|
||||
// Fetch devices for SSR (uses new helper method)
|
||||
devices, err := deviceHandler.GetDevicesData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading devices")
|
||||
}
|
||||
|
||||
// Convert to template format
|
||||
deviceData := make([]templates.DeviceData, len(devices))
|
||||
for i, device := range devices {
|
||||
var lastSync, lastSeen string
|
||||
if device.LastSync != nil {
|
||||
lastSync = device.LastSync.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
if device.LastSeen != nil {
|
||||
lastSeen = device.LastSeen.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
|
||||
deviceData[i] = templates.DeviceData{
|
||||
ID: device.ID.String(),
|
||||
DeviceName: device.DeviceName,
|
||||
DeviceType: device.DeviceType,
|
||||
SyncEnabled: device.SyncEnabled,
|
||||
AutoSync: device.AutoSync,
|
||||
SyncFrequency: device.SyncFrequency,
|
||||
LastSync: lastSync,
|
||||
LastSeen: lastSeen,
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch pending registrations for SSR (uses new helper)
|
||||
pendingRegs, err := deviceHandler.GetPendingRegistrationsData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading pending registrations")
|
||||
}
|
||||
|
||||
// Convert to template format
|
||||
pendingData := make([]templates.PendingRegistrationData, len(pendingRegs))
|
||||
for i, reg := range pendingRegs {
|
||||
expiresAt := reg["expires_at"].(time.Time)
|
||||
pendingData[i] = templates.PendingRegistrationData{
|
||||
RegistrationID: reg["registration_id"].(string),
|
||||
DeviceName: reg["device_name"].(string),
|
||||
DeviceType: reg["device_type"].(string),
|
||||
ExpiresAt: expiresAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
|
||||
// Render template WITH data (SSR)
|
||||
var buf bytes.Buffer
|
||||
err := templates.Devices(user).Render(c.Request().Context(), &buf)
|
||||
err = templates.Devices(user, deviceData, pendingData).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Conflicts route (protected)
|
||||
// Conflicts route (protected) - SSR version
|
||||
protected.GET("/conflicts", func(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userEmail := c.Get("user_email").(string)
|
||||
@@ -459,15 +507,22 @@ func main() {
|
||||
Role: userRole,
|
||||
}
|
||||
|
||||
// Fetch conflicts for SSR (uses existing handler method)
|
||||
apiConflicts, total, unresolved, err := conflictHandler.GetConflictsData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading conflicts")
|
||||
}
|
||||
|
||||
// Render template WITH data (SSR) - use apiConflicts directly
|
||||
var buf bytes.Buffer
|
||||
err := templates.Conflicts(user).Render(c.Request().Context(), &buf)
|
||||
err = templates.Conflicts(user, apiConflicts, total, unresolved).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Queue Management route (protected)
|
||||
// Queue Management route (protected) - SSR version
|
||||
protected.GET("/queue", func(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userEmail := c.Get("user_email").(string)
|
||||
@@ -481,8 +536,36 @@ func main() {
|
||||
Role: userRole,
|
||||
}
|
||||
|
||||
// Fetch queue items for SSR (uses existing handler method)
|
||||
queueItems, err := queueHandler.GetQueueData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading queue")
|
||||
}
|
||||
|
||||
// Calculate stats from items
|
||||
stats := handlers.QueueStatsResponse{
|
||||
PendingCount: 0,
|
||||
ProcessingCount: 0,
|
||||
FailedCount: 0,
|
||||
CompletedCount: 0,
|
||||
TotalCount: int64(len(queueItems)),
|
||||
}
|
||||
for _, item := range queueItems {
|
||||
switch item.Status {
|
||||
case "pending":
|
||||
stats.PendingCount++
|
||||
case "processing":
|
||||
stats.ProcessingCount++
|
||||
case "failed":
|
||||
stats.FailedCount++
|
||||
case "completed":
|
||||
stats.CompletedCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Render template WITH data (SSR)
|
||||
var buf bytes.Buffer
|
||||
err := templates.Queue(user).Render(c.Request().Context(), &buf)
|
||||
err = templates.Queue(user, queueItems, stats).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -55,10 +55,9 @@ templ Collection(user User, collections []CollectionData) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ col.Name }</h3>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ col.Description }</p>
|
||||
<p class="text-xs" style="color: var(--text-secondary)">Created { col.CreatedAt }</p>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ col.Name }</h3>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ col.Description }</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
+44
-14
@@ -1,6 +1,8 @@
|
||||
package templates
|
||||
|
||||
templ Conflicts(user User) {
|
||||
import "bookmann/internal/handlers"
|
||||
|
||||
templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int, unresolved int) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -69,24 +71,52 @@ templ Conflicts(user User) {
|
||||
</div>
|
||||
|
||||
<div id="stats-bar" class="mt-4 flex space-x-6 text-sm">
|
||||
<span style="color: var(--text-secondary)">Total: <strong id="total-count" style="color: var(--text-primary)">0</strong></span>
|
||||
<span style="color: var(--text-secondary)">Unresolved: <strong id="unresolved-count" style="color: #f59e0b;">0</strong></span>
|
||||
<span style="color: var(--text-secondary)">Resolved: <strong id="resolved-count" style="color: #10b981;">0</strong></span>
|
||||
<span style="color: var(--text-secondary)">Total: <strong style="color: var(--text-primary)">{ total }</strong></span>
|
||||
<span style="color: var(--text-secondary)">Unresolved: <strong style="color: #f59e0b;">{ unresolved }</strong></span>
|
||||
<span style="color: var(--text-secondary)">Resolved: <strong style="color: #10b981;">{ total - unresolved }</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loading" class="text-center py-16" style="color: var(--text-secondary)">
|
||||
<div class="loading-spinner mx-auto mb-4"></div>
|
||||
<p>Loading conflicts...</p>
|
||||
</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 conflicts...</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)">No Conflicts</h3>
|
||||
<p>Your devices are in sync! No conflicts to resolve.</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)">No Conflicts</h3>
|
||||
<p>Your devices are in sync! No conflicts to resolve.</p>
|
||||
</div>
|
||||
|
||||
<div id="conflicts-list" class="space-y-6 hidden"></div>
|
||||
<div id="conflicts-list" class="space-y-6">
|
||||
if len(conflicts) == 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)">No Conflicts</h3>
|
||||
<p>Your devices are in sync! No conflicts to resolve.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
for _, conflict := range conflicts {
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border); border-left: 4px solid #f59e0b;">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">{ conflict.MediaItemTitle }</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Type: { conflict.ConflictType }</p>
|
||||
</div>
|
||||
<button onclick="showConflictModal('{ conflict.ID }')" class="btn-primary px-4 py-2 rounded-lg">
|
||||
Resolve
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm" style="color: var(--text-secondary)">
|
||||
Created: { conflict.CreatedAt.Format("2006-01-02 15:04:05") }
|
||||
if conflict.ResolvedBy != "" {
|
||||
| Resolved by: { conflict.ResolvedBy }
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div id="conflict-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-4xl mx-4 my-8" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
|
||||
File diff suppressed because one or more lines are too long
+213
-2
File diff suppressed because one or more lines are too long
+44
-7
@@ -1,6 +1,8 @@
|
||||
package templates
|
||||
|
||||
templ Queue(user User) {
|
||||
import "bookmann/internal/handlers"
|
||||
|
||||
templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.QueueStatsResponse) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -65,19 +67,19 @@ templ Queue(user User) {
|
||||
|
||||
<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" id="stat-pending" style="color: #f59e0b;">0</div>
|
||||
<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" id="stat-processing" style="color: #3b82f6;">0</div>
|
||||
<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" id="stat-completed" style="color: #10b981;">0</div>
|
||||
<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" id="stat-failed" style="color: #ef4444;">0</div>
|
||||
<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>
|
||||
@@ -124,7 +126,7 @@ templ Queue(user User) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loading" class="text-center py-16" style="color: var(--text-secondary)">
|
||||
<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>
|
||||
@@ -135,7 +137,42 @@ templ Queue(user User) {
|
||||
<p>No items in the sync queue</p>
|
||||
</div>
|
||||
|
||||
<div id="queue-list" class="space-y-3 hidden"></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="status-badge status-{ item.Status }">{ item.Status }</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">{ item.SyncType }</span>
|
||||
</div>
|
||||
<span class="priority-badge 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);">
|
||||
|
||||
+205
-2
File diff suppressed because one or more lines are too long
@@ -60,6 +60,9 @@ type ConflictData struct {
|
||||
ConflictType string
|
||||
ResolutionStatus string
|
||||
CreatedAt string
|
||||
ResolutionData map[string]interface{}
|
||||
ResolvedBy string
|
||||
ResolvedAt string
|
||||
}
|
||||
|
||||
type QueueItemData struct {
|
||||
|
||||
Reference in New Issue
Block a user