@AdminSidebar(user, "/admin")
diff --git a/templates/admin_templ.go b/templates/admin_templ.go
index f75f8d5..1782b5c 100644
--- a/templates/admin_templ.go
+++ b/templates/admin_templ.go
@@ -29,7 +29,7 @@ func Admin(user User) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/templates/collections.templ b/templates/collections.templ
index 503b6d8..8585647 100644
--- a/templates/collections.templ
+++ b/templates/collections.templ
@@ -12,86 +12,8 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
@@ -190,7 +112,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
diff --git a/templates/collections_templ.go b/templates/collections_templ.go
index 466c96f..c9da4d2 100644
--- a/templates/collections_templ.go
+++ b/templates/collections_templ.go
@@ -31,7 +31,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Collections - Bookhoard")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Collections - Bookhoard")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -57,7 +57,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("/collections/" + col.ID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 141, Col: 82}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 63, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -70,7 +70,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(col.Color)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 145, Col: 30}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 67, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -83,7 +83,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 148, Col: 41}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 70, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -96,7 +96,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("/collections/" + col.ID + "/edit-modal")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 151, Col: 60}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 73, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -109,7 +109,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("/api/collections/" + col.ID + "")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 160, Col: 56}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 82, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -122,7 +122,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 170, Col: 92}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 92, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -135,7 +135,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 171, Col: 86}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 93, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -190,13 +190,13 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 188, Col: 27}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 110, Col: 27}
}
_, 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, 15, " - Bookhoard")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " - Bookhoard")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -211,7 +211,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 201, Col: 81}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 123, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -224,7 +224,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 203, Col: 90}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 125, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -237,7 +237,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 204, Col: 71}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 126, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -261,7 +261,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 261, Col: 22}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 183, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -279,7 +279,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 268, Col: 27}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 190, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -302,7 +302,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(book.CoverImagePath)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 276, Col: 36}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 198, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
diff --git a/web/src/admin.ts b/web/src/admin.ts
index e79b2cc..e9cd903 100644
--- a/web/src/admin.ts
+++ b/web/src/admin.ts
@@ -1,5 +1,46 @@
import { Alpine } from "./alpine";
import { showToast } from "./toast";
+import { createWebSocket } from "./websocket";
+
+function initializeScanWebSocket(): void {
+ createWebSocket({
+ onMessage: (message) => {
+ switch (message.type) {
+ case "scan_progress":
+ updateScanProgress(message.data);
+ break;
+ case "scan_complete":
+ showScanComplete(message.data);
+ break;
+ case "scan_error":
+ showScanError(message.data);
+ break;
+ }
+ },
+ enableReconnect: true,
+ reconnectDelay: 5000,
+ });
+}
+
+function updateScanProgress(data: { progress: number; files_scanned: number; new_items: number }): void {
+ const progressBar = document.getElementById("scan-progress-bar");
+ if (progressBar) {
+ progressBar.style.width = (data.progress * 100) + "%";
+ }
+
+ const progressText = document.getElementById("scan-progress-text");
+ if (progressText) {
+ progressText.textContent = `${data.files_scanned} files scanned (${data.new_items} new)`;
+ }
+}
+
+function showScanComplete(_data: unknown): void {
+ console.log("Scan complete:", _data);
+}
+
+function showScanError(_data: unknown): void {
+ console.error("Scan error:", _data);
+}
async function triggerLibraryScan(): Promise {
const token = localStorage.getItem("token");
@@ -339,6 +380,7 @@ function stopScanStatusPolling(): void {
export {
hideScanProgress,
+ initializeScanWebSocket,
loadSystemStats,
loadWatchStatus,
scanAllLibraries,
@@ -349,6 +391,7 @@ export {
Alpine.data("admin", () => ({
hideScanProgress,
+ initializeScanWebSocket,
loadSystemStats,
loadWatchStatus,
scanAllLibraries,
diff --git a/web/src/collections.ts b/web/src/collections.ts
index 73e8b98..48304ad 100644
--- a/web/src/collections.ts
+++ b/web/src/collections.ts
@@ -1,5 +1,50 @@
import { Alpine } from "./alpine";
import { showToast } from "./toast";
+import { createWebSocket } from "./websocket";
+
+let collectionId: string | null = null;
+
+function initializeCollectionWebSocket(): void {
+ const dataEl = document.getElementById("collection-data");
+ if (!dataEl) return;
+
+ collectionId = dataEl.dataset.id || null;
+
+ if (!collectionId) return;
+
+ createWebSocket({
+ onMessage: (message) => {
+ if (message.type === "collection_updated" && message.data.collection_id === collectionId) {
+ const actionText =
+ message.data.action === "books_added"
+ ? `Added ${message.data.count || 0} book(s)`
+ : message.data.action === "book_removed"
+ ? "Removed a book"
+ : message.data.action === "books_bulk_removed"
+ ? `Removed ${message.data.count || 0} book(s)`
+ : "Collection updated";
+
+ showToast(actionText, "info");
+
+ const activeElement = document.activeElement;
+ const isUserActive =
+ activeElement &&
+ (activeElement.tagName === "INPUT" ||
+ activeElement.tagName === "TEXTAREA" ||
+ activeElement.tagName === "SELECT" ||
+ activeElement.getAttribute("contenteditable") === "true");
+
+ if (!isUserActive) {
+ setTimeout(() => {
+ location.reload();
+ }, 1000);
+ }
+ }
+ },
+ enableReconnect: true,
+ reconnectDelay: 5000,
+ });
+}
async function loadCollections(): Promise {
const token = localStorage.getItem("token");
@@ -410,6 +455,7 @@ export {
deleteRule,
filterIcons,
initColorSelection,
+ initializeCollectionWebSocket,
loadCollectionRules,
loadCollections,
navigateToCollection,
@@ -426,6 +472,7 @@ Alpine.data("collections", () => ({
deleteRule,
filterIcons,
initColorSelection,
+ initializeCollectionWebSocket,
loadCollectionRules,
loadCollections,
navigateToCollection,