feat(ui): add first-run setup wizard with 4 guided steps

Add a single-page multi-step setup wizard that guides new users
through initial configuration:

Step 1 - Admin Registration: Creates the first user (auto-admin)
  using the existing POST /api/auth/register endpoint, with
  real-time password validation and confirmation matching.

Step 2 - Library Creation: Create one or more libraries (Ebooks,
  Audiobooks, Comics, Manga) using POST /api/libraries. Libraries
  list updates inline as they're added.

Step 3 - Folder Configuration: Add filesystem folders to each
  library using the existing GET /api/libraries/browse endpoint for
  a visual directory browser. Folders are attached via POST
  /api/libraries/:id/folders.

Step 4 - Initial Scan: Triggers a manual scan of all libraries via
  POST /api/libraries/scan with real-time progress polling using the
  existing scan status endpoint.

On completion, the wizard calls PUT /api/setup/complete and sets the
selectedLibrary cookie to the first library's UUID, ensuring the
dashboard loads with populated content instead of an empty 'All
Libraries' view. Handles the edge case where a stale JWT from a
previous database instance triggers an 'already exists' error by
auto-advancing to step 2.

The wizard reuses all existing API calls, Alpine.js utilities, and
form validation functions — no backend logic was duplicated.
This commit is contained in:
john-okeefe committed 2026-06-06 00:04:09 -04:00
1 parent f37de6c07c
commit f75d68bf66
4 files changed
+889

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func Setup() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
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>Setup - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"setupWizard\" x-init=\"initSetup()\"><div class=\"container mx-auto px-4 py-8 max-w-2xl\"><div class=\"text-center mb-8\"><h1 class=\"text-4xl font-bold mb-2\" style=\"color: var(--text-primary)\">Welcome to Bookhoard</h1><p class=\"text-lg\" style=\"color: var(--text-secondary)\">Let's get your library set up.</p></div><!-- Step indicator --><div class=\"flex items-center justify-center mb-8 gap-2\"><template x-for=\"n in 4\" :key=\"n\"><div class=\"flex items-center gap-2\"><div class=\"w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all\" :class=\"currentStep >= n ? 'bg-[var(--accent)] text-[var(--bg-primary)]' : 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border)]'\"><span x-text=\"currentStep > n ? '✓' : n\"></span></div><div x-text=\"['Admin', 'Libraries', 'Folders', 'Scan'][n-1]\" class=\"text-sm hidden sm:inline\" :style=\"currentStep >= n ? 'color: var(--text-primary)' : 'color: var(--text-secondary)'\"></div><div x-show=\"n < 4\" class=\"w-8 h-0.5 mx-1\" :class=\"currentStep > n ? 'bg-[var(--accent)]' : 'bg-[var(--border)]'\"></div></div></template></div><!-- Error display --><div x-show=\"errorMsg\" class=\"mb-4 p-4 rounded-lg border border-red-500/50 bg-red-500/10 text-red-400 text-sm\"><span x-text=\"errorMsg\"></span></div><!-- ==================== STEP 1: Create Admin ==================== --><div x-show=\"currentStep === 1\" class=\"card p-6 rounded-lg shadow-md border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h2 class=\"text-2xl font-bold mb-2\" style=\"color: var(--text-primary)\">Create Administrator Account</h2><p class=\"mb-6\" style=\"color: var(--text-secondary)\">This first account will have full admin privileges.</p><form @submit.prevent=\"submitAdmin()\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Email</label> <input type=\"email\" id=\"email\" x-model=\"admin.email\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" x-model=\"admin.username\" id=\"username\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"grid grid-cols-2 gap-4 mb-4\"><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" x-model=\"admin.firstName\" placeholder=\"Optional\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" x-model=\"admin.lastName\" placeholder=\"Optional\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\"></div></div><div id=\"password-requirements\" class=\"mb-4 p-4 rounded-lg border text-sm\" style=\"background-color: var(--bg-primary); border-color: var(--border)\"><p class=\"font-semibold mb-2\" style=\"color: var(--text-primary)\">Password Requirements:</p><ul class=\"space-y-1\" style=\"color: var(--text-secondary)\"><li id=\"req-length\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> At least 8 characters</li><li id=\"req-upper\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One uppercase letter</li><li id=\"req-lower\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One lowercase letter</li><li id=\"req-number\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One number</li><li id=\"req-special\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One special character</li><li id=\"req-match\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> Passwords match</li></ul></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" x-model=\"admin.password\" id=\"password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-colLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate