fix(web): scan progress — correct percentages, no layout shift, refresh-survivable
Bug: the admin 'Scanning Libraries' widget fed the server's 0-1 progress straight into percent strings and bar widths, so the overall and per-library percentages capped at '1%' and the bars never filled. Both now scale by 100 (matching the setup page, which was already correct). Bug: scan state only reached clients via websocket events, so a page opened (or refreshed) mid-scan showed nothing until the next tick — and the admin widget never re-attached at all. The worker now exposes active jobs (with their library_id so rows keep their names), served as GET /api/scanner/active (admin, re-attaches the widget + polling) and GET /api/scanner/active-summary (any user, restores the sidebar indicator on load). Idle hides both, exactly as before. Annoyance: the sidebar's 'Scanning… N%' was a block element between the logo and the nav, shoving every menu entry down while a scan ran. The percent now lives inside the fixed-height logo row next to the spinner (spinner last, so growing digits extend leftward); nothing below it moves.
This commit is contained in:
1 parent
9bc149f3d9
commit
fd6f0e3353
11 files changed
+245
-38
No files matched your search
@@ -10,7 +10,7 @@ templ Admin(user User, stats AdminStats) {
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="admin" x-init="loadWatchStatus()" class="theme-{ user.Theme }">
|
||||
<body x-data="admin" x-init="loadWatchStatus(); checkActiveScans()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/admin")
|
||||
<main class="p-8">
|
||||
<div class="max-w-4xl">
|
||||
|
||||
@@ -29,7 +29,7 @@ func Admin(user User, stats AdminStats) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Admin Dashboard - Bookhoard</title><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"admin\" x-init=\"loadWatchStatus()\" class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Admin Dashboard - Bookhoard</title><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"admin\" x-init=\"loadWatchStatus(); checkActiveScans()\" class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+19
-13
@@ -24,11 +24,24 @@ templ Header(user User, currentPath string) {
|
||||
@Icon("book-open", "h-6 w-6")
|
||||
</span>
|
||||
<span class="font-bold text-lg tracking-tight" style="color: var(--text-primary)">Bookhoard</span>
|
||||
<!-- Scanning indicator: lives INSIDE the fixed-height logo row so
|
||||
it can never push the nav entries down. Spinner sits last so
|
||||
the growing percent extends leftward, not into the spinner. -->
|
||||
<div
|
||||
x-show="scanning"
|
||||
x-cloak
|
||||
x-transition
|
||||
class="ml-auto flex items-center gap-1.5 shrink-0"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span
|
||||
x-show="scanProgress > 0"
|
||||
class="text-xs font-medium"
|
||||
style="color: var(--text-secondary);"
|
||||
x-text="scanProgress + '%'"
|
||||
></span>
|
||||
<svg
|
||||
x-show="scanning"
|
||||
x-cloak
|
||||
x-transition
|
||||
class="h-4 w-4 animate-spin ml-auto"
|
||||
class="h-4 w-4 animate-spin"
|
||||
style="color: var(--accent);"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -36,15 +49,8 @@ templ Header(user User, currentPath string) {
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<span
|
||||
x-show="scanning && scanProgress > 0"
|
||||
x-cloak
|
||||
x-transition
|
||||
class="block px-5 py-1.5 text-xs font-medium -mt-1"
|
||||
style="color: var(--text-secondary);"
|
||||
x-text="'Scanning… ' + scanProgress + '%'"
|
||||
></span>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Primary navigation -->
|
||||
<nav class="flex-1 px-3 py-4 space-y-0.5 overflow-y-auto">
|
||||
<a href="/dashboard" class={ activeClass(currentPath, "/dashboard") }>
|
||||
|
||||
+11
-11
@@ -37,7 +37,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> <span class=\"font-bold text-lg tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span> <svg x-show=\"scanning\" x-cloak x-transition class=\"h-4 w-4 animate-spin ml-auto\" style=\"color: var(--accent);\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle> <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg></a> <span x-show=\"scanning && scanProgress > 0\" x-cloak x-transition class=\"block px-5 py-1.5 text-xs font-medium -mt-1\" style=\"color: var(--text-secondary);\" x-text=\"'Scanning… ' + scanProgress + '%'\"></span><!-- Primary navigation --><nav class=\"flex-1 px-3 py-4 space-y-0.5 overflow-y-auto\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> <span class=\"font-bold text-lg tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span><!-- Scanning indicator: lives INSIDE the fixed-height logo row so\n\t\t\t it can never push the nav entries down. Spinner sits last so\n\t\t\t the growing percent extends leftward, not into the spinner. --><div x-show=\"scanning\" x-cloak x-transition class=\"ml-auto flex items-center gap-1.5 shrink-0\" aria-live=\"polite\"><span x-show=\"scanProgress > 0\" class=\"text-xs font-medium\" style=\"color: var(--text-secondary);\" x-text=\"scanProgress + '%'\"></span> <svg class=\"h-4 w-4 animate-spin\" style=\"color: var(--accent);\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle> <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg></div></a><!-- Primary navigation --><nav class=\"flex-1 px-3 py-4 space-y-0.5 overflow-y-auto\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -290,7 +290,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("changeTheme('" + opt.Name + "'); openPanel = null")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 107, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 113, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -303,7 +303,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 109, Col: 29}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 115, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -316,7 +316,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + opt.Color)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 112, Col: 130}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 118, Col: 130}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -329,7 +329,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 113, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 119, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -382,7 +382,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue("changeWoodPaneling('" + w.Name + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 124, Col: 55}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 130, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -395,7 +395,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(w.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 126, Col: 26}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 132, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -418,7 +418,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-image: url(/static/textures/thumb-" + w.Name + ".webp); background-size: cover;")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 132, Col: 191}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 138, Col: 191}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -436,7 +436,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(w.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 134, Col: 48}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 140, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -676,7 +676,7 @@ func SidebarUserMenu(user User) templ.Component {
|
||||
var templ_7745c5c3_Var37 string
|
||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 224, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 230, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -760,7 +760,7 @@ func SidebarSignIn(currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 272, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 278, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
Reference in new issue
Block a user