Files
bookhoard/templates/setup.templ
T
john-okeefe 3170aa6b77 feat(setup): collect server base URL during first-run wizard
Add a Server URL field to the admin-account step of the setup wizard so
the public base URL (used for device sync, OPDS feed, and API endpoints)
is configured up front instead of requiring a later visit to admin
settings.

- setup.templ: base URL input on the admin step plus a summary entry
- setup.ts: default baseUrl to window.location.origin and persist it via
  PUT /system/config ({ base_url }) right after the admin account is
  created, with a non-fatal warning if the save fails
2026-06-07 00:03:01 -04:00

403 lines
18 KiB
Templ
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package templates
templ Setup() {
<!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-6 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Server URL (Base URL)</label>
<input
type="url"
x-model="admin.baseUrl"
placeholder="https://books.example.com"
class="w-full px-3 py-2 border rounded mt-2"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
required
/>
<p class="text-sm mt-2" style="color: var(--text-secondary)">The public URL of your Bookhoard instance. Used for device sync, OPDS feed, and API endpoints.</p>
</div>
<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-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
required
/>
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm Password</label>
<input
type="password"
x-model="admin.confirmPassword"
id="confirm-password"
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>
<button
type="submit"
id="register-btn"
class="btn-primary w-full py-2 rounded opacity-50 cursor-not-allowed"
:disabled="loading"
x-text="loading ? 'Creating...' : 'Create Admin Account'"
></button>
</form>
</div>
<!-- ==================== STEP 2: Create Libraries ==================== -->
<div x-show="currentStep === 2" 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 Libraries</h2>
<p class="mb-6" style="color: var(--text-secondary)">Add one or more media libraries. You can always add more later.</p>
<!-- Created libraries list -->
<div x-show="libraries.length > 0" class="mb-6 space-y-2">
<template x-for="(lib, idx) in libraries" :key="lib.id">
<div class="p-3 rounded-lg border flex items-center justify-between" style="background-color: var(--bg-primary); border-color: var(--border)">
<div class="flex items-center gap-3">
<span class="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span>
<div>
<p class="font-medium" style="color: var(--text-primary)" x-text="lib.name"></p>
<p class="text-xs capitalize" style="color: var(--text-secondary)" x-text="lib.type"></p>
</div>
</div>
<button
type="button"
@click="removeLibrary(idx)"
class="text-xs text-red-500 hover:underline"
>Remove</button>
</div>
</template>
</div>
<!-- Add library form -->
<form @submit.prevent="addLibrary()" class="space-y-4 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Library Name</label>
<input
type="text"
x-model="newLibrary.name"
placeholder="My Ebook Library"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
required
/>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Library Type</label>
<select
x-model="newLibrary.type"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
>
<option value="ebooks">📚 Ebooks</option>
<option value="comics">📖 Comics</option>
<option value="manga">🗾 Manga</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Description (optional)</label>
<input
type="text"
x-model="newLibrary.description"
placeholder="Optional"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
/>
</div>
<button
type="submit"
class="btn-secondary w-full py-2 rounded"
:disabled="loading || !newLibrary.name.trim()"
x-text="loading ? 'Creating...' : '+ Add This Library'"
></button>
</form>
<div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 1" class="btn-secondary px-4 py-2 rounded">Back</button>
<button type="button" @click="currentStep = 3" class="btn-primary px-6 py-2 rounded">
<span x-text="libraries.length > 0 ? 'Next: Configure Folders' : 'Skip Continue'"></span>
</button>
</div>
</div>
<!-- ==================== STEP 3: Configure Folders ==================== -->
<div x-show="currentStep === 3" 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)">Configure Library Folders</h2>
<p class="mb-6" style="color: var(--text-secondary)">Add the filesystem folders where your media files are stored.</p>
<div x-show="libraries.length === 0" class="text-center py-8" style="color: var(--text-secondary)">
<p>No libraries created. You can add folders later from the admin panel.</p>
</div>
<div class="space-y-6">
<template x-for="lib in libraries" :key="lib.id">
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<div class="flex items-center gap-2 mb-3">
<span class="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span>
<h4 class="font-semibold" style="color: var(--text-primary)" x-text="lib.name"></h4>
</div>
<!-- Existing folders for this library -->
<div class="space-y-1 mb-3" x-show="lib.folders && lib.folders.length > 0">
<template x-for="(folder, fIdx) in (lib.folders || [])" :key="folder">
<div class="flex items-center justify-between p-2 rounded text-sm" style="background-color: var(--bg-secondary); border-color: var(--border)">
<span style="color: var(--text-primary)" x-text="folder"></span>
<button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="text-xs text-red-500 hover:underline">Remove</button>
</div>
</template>
</div>
<!-- Add folder -->
<div class="flex gap-2">
<input
type="text"
:placeholder="'/path/to/' + lib.type"
:id="'folderInput-' + lib.id"
class="flex-1 px-3 py-2 border rounded text-sm"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
/>
<button
type="button"
@click="showFolderBrowser('folderInput-' + lib.id)"
class="btn-secondary px-3 py-2 text-xs rounded"
>Browse</button>
<button
type="button"
@click="addFolderToLibrary(lib)"
class="btn-primary px-3 py-2 text-xs rounded"
>Add</button>
</div>
</div>
</template>
</div>
<div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 2" class="btn-secondary px-4 py-2 rounded">Back</button>
<button type="button" @click="currentStep = 4" class="btn-primary px-6 py-2 rounded">Next: Scan</button>
</div>
</div>
<!-- ==================== STEP 4: Scan & Finish ==================== -->
<div x-show="currentStep === 4" 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)">Scan & Finish</h2>
<p class="mb-6" style="color: var(--text-secondary)">Review your configuration and start the initial scan.</p>
<!-- Summary -->
<div class="space-y-3 mb-6">
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<p class="text-sm font-medium mb-1" style="color: var(--text-secondary)">Server URL</p>
<p style="color: var(--text-primary)" x-text="admin.baseUrl"></p>
</div>
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<p class="text-sm font-medium mb-1" style="color: var(--text-secondary)">Administrator</p>
<p style="color: var(--text-primary)" x-text="admin.username + ' (' + admin.email + ')'"></p>
</div>
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<p class="text-sm font-medium mb-2" style="color: var(--text-secondary)">
Libraries (<span x-text="libraries.length"></span>)
</p>
<template x-for="lib in libraries" :key="lib.id">
<div class="flex items-start justify-between text-sm py-1">
<div>
<span style="color: var(--text-primary)" x-text="lib.name"></span>
<span class="ml-2 text-xs px-2 py-0.5 rounded" style="background-color: var(--accent); color: var(--bg-primary)" x-text="lib.type"></span>
</div>
<div class="text-right" style="color: var(--text-secondary)">
<span x-text="(lib.folders || []).length + ' folder(s)'"></span>
</div>
</div>
</template>
</div>
</div>
<!-- Scan progress -->
<div x-show="scanStarted" class="mb-6 space-y-3">
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Progress</h3>
<div id="library-progress-list" class="space-y-3">
<template x-for="job in scanJobs" :key="job.jobId">
<div class="p-3 rounded border" style="background-color: var(--bg-primary); border-color: var(--border)">
<div class="flex justify-between items-center mb-2">
<span class="font-medium" style="color: var(--text-primary)" x-text="job.libraryName"></span>
<span class="text-sm" style="color: var(--text-secondary)" x-text="job.statusText"></span>
</div>
<div class="w-full rounded-full h-2" style="background-color: var(--bg-secondary)">
<div
class="h-2 rounded-full transition-all duration-500"
:style="'width: ' + job.progress + '%; background-color: var(--accent);'"
></div>
</div>
</div>
</template>
</div>
<div x-show="scanComplete" class="p-4 rounded-lg border border-green-500/50 bg-green-500/10 text-green-400 text-sm">
Initial scan complete! Your libraries are ready to use.
</div>
</div>
<div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 3" class="btn-secondary px-4 py-2 rounded" :disabled="loading">Back</button>
<div class="flex gap-3">
<button
x-show="!scanStarted"
type="button"
@click="startScan()"
class="btn-primary px-6 py-2 rounded"
:disabled="loading"
>Start Scan</button>
<button
x-show="scanStarted && !scanComplete"
type="button"
@click="finishSetup()"
class="btn-secondary px-6 py-2 rounded"
:disabled="loading"
>Skip to Dashboard</button>
<button
x-show="scanComplete"
type="button"
@click="finishSetup()"
class="btn-primary px-6 py-2 rounded"
:disabled="loading"
x-text="loading ? 'Finishing...' : 'Go to Dashboard'"
></button>
</div>
</div>
</div>
</div>
<!-- Folder Browser Modal (reused from admin library) -->
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7)">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
<button type="button" @click="hideFolderBrowser()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button>
</div>
<div id="folder-browser-content">
<!-- Directory listings rendered by Alpine -->
</div>
</div>
</div>
</body>
</html>
}