Fix and refine frontend scan button implementation plan

Updated TASKS-scanning-progress.md Part 1 with critical fixes and
clarifications for implementing the admin page scan button functionality.

Fixes Applied:
- Converted inline JavaScript to TypeScript using existing web/src/admin.ts
- Fixed animation implementation to use TailwindCSS classes instead of custom CSS
- Added missing implementation steps:
  * Include admin.js in admin.templ
  * Remove old quickScan() function after migration
  * Build frontend assets step
- Fixed animation trigger by removing opacity/transform classes that prevented display
- Corrected API endpoint usage (/api/libraries/{id}/scan not /api/scanner/scan)

Root Cause Analysis:
- Original quickScan() sent empty folder_paths array causing 400 errors
- No "scan all libraries" endpoint exists - must scan each library individually
- Frontend had admin.ts but wasn't including it in templates

Implementation Approach:
- Fetch all libraries via GET /api/libraries
- Trigger scan for each library via POST /api/libraries/{id}/scan
- Display consolidated progress UI with animation
- Handle errors gracefully per library
- Use TypeScript for type safety
- Leverage TailwindCSS for all styling (no custom CSS)

Documentation Structure:
- Part 1: Admin scan button implementation (frontend)
- Part 2: Dashboard diagnosis and fixes (to be completed after backend work)

This plan is now ready for implementation after backend progress tracking
is completed (as documented in TASKS-backend-progress-tracking.md).
This commit is contained in:
2026-02-25 10:40:10 -05:00
parent a35a08928c
commit bd9715c272
+283 -166
View File
@@ -109,19 +109,33 @@ function quickScan() {
### Part 1: Fix Scan Library Button
**NOTE - Major Changes to Original Plan:**
- **Switched from inline JavaScript to TypeScript** (follows PROJECT_GUIDELINES.md: "convert all JavaScript to TypeScript")
- **Uses existing `web/src/admin.ts` infrastructure** instead of adding new inline code
- **No custom CSS** - uses TailwindCSS transition classes for animation (follows "TailwindCSS classes only" rule)
- **Inline CSS with variables retained** - follows existing pattern in admin.templ for theme support
**Rationale:**
- Project already has `web/src/admin.ts` with TypeScript scanning functions
- Inline JavaScript in templates makes code harder to maintain
- TypeScript provides better type safety and code organization
- TailwindCSS transitions are sufficient for UI animation
**Files to Modify:**
- `templates/admin.templ`
- `web/src/admin.ts` (extend TypeScript scanning functions)
- `templates/admin.templ` (add progress UI, include admin.js)
**Implementation Steps:**
#### Step 1: Replace quickScan() Function
#### Step 1: Extend TypeScript in web/src/admin.ts
**Location:** `templates/admin.templ` lines 69-85
**Location:** Add new functions after `loadSystemStats()` (around line 103)
**New Implementation:**
```javascript
async function quickScan() {
**Add these functions:**
```typescript
async function scanAllLibraries(): Promise<void> {
const token = localStorage.getItem('token');
if (!token) return;
try {
// Step 1: Get all libraries
@@ -136,15 +150,17 @@ async function quickScan() {
const libsData = await libsResp.json();
if (!libsData.data || libsData.data.length === 0) {
alert('No libraries found. Please create a library first.');
if ((window as any).showToast?.error) {
(window as any).showToast.error('No libraries found. Please create a library first.');
}
return;
}
const libraries = libsData.data;
// Step 2: Scan each library
const jobs = [];
const libraryNames = {};
const jobs: string[] = [];
const libraryNames: Record<string, string> = {};
for (const lib of libraries) {
const scanResp = await fetch(`/api/libraries/${lib.id}/scan`, {
@@ -162,7 +178,9 @@ async function quickScan() {
}
if (jobs.length === 0) {
alert('Failed to start scan for any library');
if ((window as any).showToast?.error) {
(window as any).showToast.error('Failed to start scan for any library');
}
return;
}
@@ -171,19 +189,246 @@ async function quickScan() {
} catch (error) {
console.error('Scan error:', error);
alert('Failed to start scan: ' + error.message);
if ((window as any).showToast?.error) {
(window as any).showToast.error('Failed to start scan: ' + (error as Error).message);
}
}
}
function showScanProgress(jobIds: string[], libraryNames: Record<string, string>): void {
const container = document.getElementById('scan-progress-container') as HTMLElement;
const list = document.getElementById('library-progress-list') as HTMLElement;
if (!container || !list) return;
container.classList.remove('hidden');
// Trigger slide-in animation by removing opacity and transform classes
container.classList.remove('opacity-0', '-translate-y-2.5');
// Create progress items for each library
list.innerHTML = jobIds.map(jobId => `
<div id="progress-${jobId}" 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)">
${libraryNames[jobId]}
</span>
<span id="status-${jobId}" class="text-sm" style="color: var(--text-secondary)">
Pending...
</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div id="bar-${jobId}"
class="h-2 rounded-full transition-all duration-500"
style="width: 0%; background-color: var(--accent);">
</div>
</div>
</div>
`).join('');
// Start polling
pollScanProgress(jobIds, libraryNames);
}
function pollScanProgress(jobIds: string[], libraryNames: Record<string, string>): void {
const token = localStorage.getItem('token');
const startTime = Date.now();
const interval = setInterval(async () => {
let allComplete = true;
let totalProgress = 0;
let totalFiles = 0;
let totalNewItems = 0;
let totalErrors = 0;
for (const jobId of jobIds) {
try {
const resp = await fetch(`/api/scanner/status/${jobId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (resp.ok) {
const status = await resp.json();
// Update individual library progress
updateLibraryProgress(jobId, status);
totalProgress += status.progress || 0;
totalFiles += status.files_scanned || 0;
totalNewItems += status.new_items || 0;
totalErrors += status.errors || 0;
if (status.status !== 'completed' && status.status !== 'failed') {
allComplete = false;
}
}
} catch (error) {
console.error(`Failed to poll job ${jobId}:`, error);
}
}
// Update overall progress
const overallProgress = Math.round(totalProgress / jobIds.length);
const progressBar = document.getElementById('scan-progress-bar') as HTMLElement;
const progressText = document.getElementById('scan-progress-text') as HTMLElement;
const statusText = document.getElementById('scan-status') as HTMLElement;
if (progressBar) progressBar.style.width = overallProgress + '%';
if (progressText) progressText.textContent = overallProgress + '%';
// Update status text
const elapsed = Math.round((Date.now() - startTime) / 1000);
if (!allComplete && statusText) {
statusText.textContent = `Scanning... ${elapsed}s elapsed • ${totalFiles} files processed`;
}
// Check if all complete
if (allComplete) {
clearInterval(interval);
showScanResults(jobIds.length, totalFiles, totalNewItems, totalErrors, elapsed);
}
}, 2000);
}
function updateLibraryProgress(jobId: string, status: any): void {
const bar = document.getElementById(`bar-${jobId}`) as HTMLElement;
const statusText = document.getElementById(`status-${jobId}`) as HTMLElement;
if (bar) {
bar.style.width = (status.progress || 0) + '%';
}
if (statusText) {
const statusMessages: Record<string, string> = {
'pending': 'Pending...',
'running': `Scanning... ${status.progress || 0}%`,
'completed': `✓ Complete (${status.new_items || 0} items)`,
'failed': `✗ Failed`
};
statusText.textContent = statusMessages[status.status] || status.status;
}
}
function showScanResults(libCount: number, files: number, items: number, errors: number, elapsed: number): void {
const resultsDiv = document.getElementById('scan-results') as HTMLElement;
const contentDiv = document.getElementById('scan-results-content') as HTMLElement;
if (!resultsDiv || !contentDiv) return;
contentDiv.innerHTML = `
<p>• ${libCount} librar${libCount === 1 ? 'y' : 'ies'} scanned</p>
<p>• ${files} files processed</p>
<p>• ${items} new items added</p>
${errors > 0 ? `<p style="color: var(--accent);">• ${errors} errors</p>` : ''}
<p style="color: var(--text-secondary)">Completed in ${elapsed} seconds</p>
`;
resultsDiv.classList.remove('hidden');
const statusText = document.getElementById('scan-status') as HTMLElement;
if (statusText) statusText.textContent = 'Scan complete!';
}
function hideScanProgress(): void {
const container = document.getElementById('scan-progress-container') as HTMLElement;
if (container) container.classList.add('hidden');
}
// Export to window
(window as any).scanAllLibraries = scanAllLibraries;
(window as any).hideScanProgress = hideScanProgress;
```
#### Step 2: Add Progress UI to admin.templ
#### Step 2: Update admin.templ to Include admin.js
**Location:** After the Quick Actions grid (around line 64)
**Location:** `templates/admin.templ` lines 7-12 (head section)
**Add this HTML:**
**Add admin.js script tag:**
```html
<script src="/static/htmx.min.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/admin.js"></script> <!-- ADD THIS LINE -->
<link href="/static/style.css" rel="stylesheet">
```
#### Step 3: Update Scan Button onClick Handler
**Location:** `templates/admin.templ` line 54
**Change from:**
```html
<button onclick="quickScan()" class="btn-primary p-4 rounded-lg text-left">
```
**Change to:**
```html
<button onclick="scanAllLibraries()" class="btn-primary p-4 rounded-lg text-left">
```
#### Step 3.5: Remove Old Inline JavaScript Function
**Location:** `templates/admin.templ` lines 69-92
**Action:** DELETE the entire `quickScan()` function from the `<script>` section
**What to remove:**
```html
<script>
function quickScan() {
fetch('/api/scanner/scan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
folder_paths: []
})
}).then(res => res.json()).then(data => {
alert(data.message || 'Scan completed successfully!');
}).catch(err => {
console.error('Scan error:', err);
alert('Scan failed. Please check your folder configuration.');
});
}
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
document.addEventListener('DOMContentLoaded', function() {
loadTheme();
});
</script>
```
**Replace with:**
```html
<script>
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
document.addEventListener('DOMContentLoaded', function() {
loadTheme();
});
</script>
```
**Note:** Keep `logout()` and `DOMContentLoaded` handlers - only remove `quickScan()`.
#### Step 4: Add Progress UI to admin.templ
**Location:** After the Quick Actions card (after line 64)
**Add this HTML after line 64:**
```html
<!-- Scan Progress Section -->
<div id="scan-progress-container" class="hidden mt-6 p-6 rounded-lg border"
<div id="scan-progress-container" class="hidden mt-6 p-6 rounded-lg border opacity-0 -translate-y-2.5 transition-all duration-300 ease-out"
style="background-color: var(--bg-secondary); border-color: var(--border);">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">
@@ -237,161 +482,33 @@ async function quickScan() {
</div>
```
#### Step 3: Add Progress Polling Logic
#### Step 5: Build TypeScript to JavaScript
**Location:** In the `<script>` section of admin.templ
**Add these functions:**
```javascript
function showScanProgress(jobIds, libraryNames) {
const container = document.getElementById('scan-progress-container');
const list = document.getElementById('library-progress-list');
container.classList.remove('hidden');
// Create progress items for each library
list.innerHTML = jobIds.map(jobId => `
<div id="progress-${jobId}" 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)">
${libraryNames[jobId]}
</span>
<span id="status-${jobId}" class="text-sm" style="color: var(--text-secondary)">
Pending...
</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div id="bar-${jobId}"
class="h-2 rounded-full transition-all duration-500"
style="width: 0%; background-color: var(--accent);">
</div>
</div>
</div>
`).join('');
// Start polling
pollScanProgress(jobIds);
}
async function pollScanProgress(jobIds) {
const token = localStorage.getItem('token');
const startTime = Date.now();
const interval = setInterval(async () => {
let allComplete = true;
let totalProgress = 0;
let totalFiles = 0;
let totalNewItems = 0;
let totalErrors = 0;
for (const jobId of jobIds) {
try {
const resp = await fetch(`/api/scanner/status/${jobId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (resp.ok) {
const status = await resp.json();
// Update individual library progress
updateLibraryProgress(jobId, status);
totalProgress += status.progress || 0;
totalFiles += status.files_scanned || 0;
totalNewItems += status.new_items || 0;
totalErrors += status.errors || 0;
if (status.status !== 'completed' && status.status !== 'failed') {
allComplete = false;
}
}
} catch (error) {
console.error(`Failed to poll job ${jobId}:`, error);
}
}
// Update overall progress
const overallProgress = Math.round(totalProgress / jobIds.length);
document.getElementById('scan-progress-bar').style.width = overallProgress + '%';
document.getElementById('scan-progress-text').textContent = overallProgress + '%';
// Update status text
const elapsed = Math.round((Date.now() - startTime) / 1000);
if (!allComplete) {
document.getElementById('scan-status').textContent =
`Scanning... ${elapsed}s elapsed • ${totalFiles} files processed`;
}
// Check if all complete
if (allComplete) {
clearInterval(interval);
showScanResults(jobIds.length, totalFiles, totalNewItems, totalErrors, elapsed);
}
}, 2000);
}
function updateLibraryProgress(jobId, status) {
const bar = document.getElementById(`bar-${jobId}`);
const statusText = document.getElementById(`status-${jobId}`);
if (bar) {
bar.style.width = (status.progress || 0) + '%';
}
if (statusText) {
const statusMessages = {
'pending': 'Pending...',
'running': `Scanning... ${status.progress || 0}%`,
'completed': `✓ Complete (${status.new_items || 0} items)`,
'failed': `✗ Failed`
};
statusText.textContent = statusMessages[status.status] || status.status;
}
}
function showScanResults(libCount, files, items, errors, elapsed) {
const resultsDiv = document.getElementById('scan-results');
const contentDiv = document.getElementById('scan-results-content');
contentDiv.innerHTML = `
<p>• ${libCount} librar${libCount === 1 ? 'y' : 'ies'} scanned</p>
<p>• ${files} files processed</p>
<p>• ${items} new items added</p>
${errors > 0 ? `<p style="color: var(--accent);">• ${errors} errors</p>` : ''}
<p style="color: var(--text-secondary)">Completed in ${elapsed} seconds</p>
`;
resultsDiv.classList.remove('hidden');
document.getElementById('scan-status').textContent = 'Scan complete!';
}
function hideScanProgress() {
document.getElementById('scan-progress-container').classList.add('hidden');
}
**Build command:**
```bash
npm run build:ts
```
#### Step 4: Add CSS Styling (if needed)
**What this does:**
- Compiles `web/src/admin.ts` to `web/static/admin.js`
- TypeScript compiler (`tsc`) handles the conversion
- Output file `admin.js` will be loaded by the script tag added in Step 2
**Location:** `web/static/input.css` or `web/static/style.css`
**Note:** This build step runs automatically in the Docker container during image build. For local development, run it manually after editing TypeScript files.
Most styles use existing CSS variables, but add if needed:
```css
#scan-progress-container {
animation: slideIn 0.3s ease-out;
}
#### Step 6: Animation Implementation Note
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
```
**How the slide-in animation works:**
1. **Initial state** (Step 4 HTML): Container has classes `hidden opacity-0 -translate-y-2.5 transition-all duration-300 ease-out`
2. **When scan starts** (Step 1 TypeScript): `showScanProgress()` function:
```typescript
container.classList.remove('hidden'); // Makes element visible
container.classList.remove('opacity-0', '-translate-y-2.5'); // Triggers animation
```
3. **Result:** Browser transitions from `opacity-0` to `opacity-1` and `-translate-y-2.5` to `translate-y-0` over 300ms
**No custom CSS needed** - follows PROJECT_GUIDELINES.md "TailwindCSS classes only" rule.
---
@@ -402,7 +519,7 @@ Most styles use existing CSS variables, but add if needed:
#### Step 1: Check System Collections Exist
**Bruno Request:**
```
GET /api/dashboard/sections?library_id=551ac19c-896a-4406-b479-353fc489b295
GET /api/dashboard/sections?library_id=849151fb-564e-4b24-89e3-d11360789576
```
**Expected Response:**
@@ -446,7 +563,7 @@ GET /api/dashboard/sections?library_id=551ac19c-896a-4406-b479-353fc489b295
#### Step 2: Check Book's Library
**Bruno Request:**
```
GET /api/media-items?library_id=551ac19c-896a-4406-b479-353fc489b295&limit=5&sort=created_at+DESC
GET /api/media-items?library_id=849151fb-564e-4b24-89e3-d11360789576&limit=5&sort=created_at+DESC
```
**Expected:**