Files
bookhoard/COLLECTIONS_CLEANUP_GUIDE.md
T
john-okeefe 1b9bc64b28 refactor(library): fix SSR bug by removing data fetch from init function
CRITICAL FIX: initializeLibraryAdmin() was calling reloadLibraries()
which fetched data from the API and replaced the SSR-rendered library
list on page load, defeating the purpose of server-side rendering.

Changes in web/src/library.ts:
- Remove DOMContentLoaded listener (now uses Alpine x-init in template)
- Remove void reloadLibraries() call from initializeLibraryAdmin()
- Add comment explaining SSR provides initial data
- Add initializeLibraryAdmin to export statement
- Add initializeLibraryAdmin to Alpine.data() registration
- Keep reloadLibraries() as standalone function for use after CRUD ops

Rationale:
- SSR provides fast initial page load with library list
- x-init should ONLY setup event listeners, not fetch data
- reloadLibraries() is called after create/delete/update operations
- Follows SSR-first architecture: different pages have different
  SSR/JS ratios (analytics is 80% JS, most pages are 80% SSR)

Documentation:
- Update COLLECTIONS_CLEANUP_GUIDE.md with SSR-first strategy
- Document page-by-page review status (dashboard ✓, collections 🔄)
- Fix template references (library.templ → admin_library.templ)
- Explain why analytics fetches data (intentional for dynamic page)

This ensures the admin library page maintains SSR benefits while
still providing interactive features via Alpine.js.
2026-03-12 17:58:36 -04:00

17 KiB
Raw Blame History

Alpine.js and SSR Architecture Cleanup Guide

Overview

This guide provides a systematic, page-by-page approach to fixing Alpine.js issues while maintaining SSR-first architecture.

Problem Statement

Console errors caused by:

  1. Dead Alpine.js exports (functions that don't exist)
  2. DOMContentLoaded listeners running on wrong pages
  3. Missing x-init calls in templates
  4. CRITICAL: x-init functions that fetch data and replace SSR content

SSR-First Alpine.js Strategy

Different pages have different SSR/JavaScript ratios:

  • Analytics Page (80% JavaScript): Dynamic charts/data that fetch on page load

    • x-init="loadAnalytics" fetches data and replaces content
    • This is intentional - analytics is a JavaScript-heavy page
  • Dashboard Page (SSR + Interactive): SSR for speed, JS for drag-drop/library switching

    • SSR provides initial dashboard data
    • x-init ONLY sets up event listeners (drag-drop, modals)
    • x-init does NOT fetch data (data already SSR'd)
    • switchLibrary() fetches only when user changes library dropdown
  • Admin Library Page (SSR + Modals/CRUD): SSR list, JS for management

    • SSR provides initial library list
    • x-init ONLY sets up event listeners and modals
    • BUG: initializeLibraryAdmin() was calling reloadLibraries() which fetches data
    • This replaces the SSR content on page load - MUST FIX
  • Most Pages (80% SSR): Server-rendered with interactive areas

    • Use x-init for setup only (event listeners, modals)
    • NEVER fetch data in x-init if data is already SSR'd

Key Principle:

  • DO in x-init: Set up event listeners, initialize modals, setup drag-drop
  • DON'T in x-init: Fetch data via API, replace SSR innerHTML

Review Status

Pages methodically reviewed so far:

  • Dashboard - Completed, working correctly
  • 🔄 Collections - In progress (fixes in progress)
  • ⏸️ Admin Library - Identified bug, fix documented in this guide
  • ⏸️ Other pages - Not yet reviewed, will be done page-by-page

Goal: Clean, working Alpine.js integration with proper SSR architecture.


Prerequisites

Before starting, verify the current state:

# Check current errors
cd /home/nymusicman/Code/bookhoard
npm run build:ts

# Should see errors about:
# - "addbooksToAdd is not defined"
# - "removebooksToAdd is not defined"
# - "toggleBookSelection is not defined"
# - etc.

Step 1: Fix collections.ts Alpine.data Export

File: web/src/collections.ts

Problem: Alpine.data exports functions that were deleted in commit 93710a1.

Action: Update the Alpine.data export to only include existing functions.

Step 1.1: Read Current Alpine.data Export

# Check what's currently exported
tail -50 web/src/collections.ts | grep -A25 "Alpine.data"

You'll see something like:

Alpine.data("collections", () => ({
  addbooksToAdd,           // ❌ Does NOT exist - deleted
  backToCollections,
  closeCollectionModal,
  createRule,
  deleteRule,
  filterCollectionBooks,
  filterIcons,
  hideAddBooksModal,
  initCollectionDetail,     // ❌ Does NOT exist - deleted
  initColorSelection,      // ❌ Does NOT exist - deleted
  initIconSelection,        // ❌ Does NOT exist - deleted
  loadCollectionRules,
  loadCollections,
  navigateToCollection,
  populateIconGrid,
  removeBook,
  removebooksToAdd,         // ❌ Does NOT exist - deleted
  searchBooksForCollections,
  selectColor,
  selectIcon,
  showAddBooksModal,
  showAllIcons,              // ❌ Does NOT exist - deleted
  setupHTMXAuth,
  testRule,
  toggleBookForRemoval,    // ❌ Does NOT exist - deleted
  toggleBookSelection,      // ❌ Does NOT exist - deleted
  updateSelectedCount,
}));

Step 1.2: Read Current Export Statement

# Check the export statement at the end of the file
grep -A30 "^export {" web/src/collections.ts

You'll see similar dead exports.

Step 1.3: Verify Which Functions Actually Exist

# Search for function definitions
grep -n "^function\|^async function" web/src/collections.ts

Expected output (actual existing functions):

  • backToCollections
  • closeCollectionModal
  • createRule
  • deleteRule
  • filterCollectionBooks
  • filterIcons
  • hideAddBooksModal
  • loadCollectionRules
  • loadCollections
  • navigateToCollection
  • populateIconGrid
  • removeBook
  • searchBooksForCollections
  • selectColor
  • selectIcon
  • showAddBooksModal
  • setupHTMXAuth
  • testRule
  • updateSelectedCount

Step 1.4: Update the Export Statement

Line 407: Change the export statement to only include existing functions:

export {
  backToCollections,
  closeCollectionModal,
  createRule,
  deleteRule,
  filterCollectionBooks,
  filterIcons,
  hideAddBooksModal,
  loadCollectionRules,
  loadCollections,
  navigateToCollection,
  populateIconGrid,
  removeBook,
  searchBooksForCollections,
  selectColor,
  selectIcon,
  showAddBooksModal,
  setupHTMXAuth,
  testRule,
  updateSelectedCount,
};

Step 1.5: Update Alpine.data Registration

Line 424: Update Alpine.data to match the export:

Alpine.data("collections", () => ({
  backToCollections,
  closeCollectionModal,
  createRule,
  deleteRule,
  filterCollectionBooks,
  filterIcons,
  hideAddBooksModal,
  loadCollectionRules,
  loadCollections,
  navigateToCollection,
  populateIconGrid,
  removeBook,
  searchBooksForCollections,
  selectColor,
  selectIcon,
  showAddBooksModal,
  setupHTMXAuth,
  testRule,
  updateSelectedCount,
}));

Step 1.6: Verify the Fix

# Build TypeScript
npm run build:ts

# Should now succeed with 0 errors

Step 2: Fix Template Function Calls

File: templates/collections.templ

Problem: Template calls functions that no longer exist.

Step 2.1: Remove Dead Function Calls from Collection Detail Page

Line 226: Remove the removebooksToAdd call:

<!-- BEFORE -->
<button
  id="bulk-remove-btn"
  @click="removebooksToAdd"
  disabled
  class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
  🗑️ Remove Selected
</button>

<!-- AFTER -->
<button
  id="bulk-remove-btn"
  disabled
  class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
  🗑️ Remove Selected
</button>

Line 229: Remove addbooksToAdd call:

<!-- BEFORE -->
<button @click="addbooksToAdd" class="btn-primary px-4 py-2 rounded-lg">
   Add Selected Books
</button>

<!-- AFTER -->
<button type="button" class="btn-primary px-4 py-2 rounded-lg" style="opacity: 0.5; cursor: not-allowed;" disabled>
   Add Selected Books
</button>

Step 2.2: Regenerate Templates

# Generate Go template files
templ generate

# Should see: Complete [updates=0 duration=~40ms]

Step 3: Fix Other TypeScript Files (Remove DOMContentLoaded)

For each file, we'll remove the DOMContentLoaded listener and add x-init to the template.

Step 3.1: analytics.ts

File: web/src/analytics.ts

Current:

export { loadAnalytics };

document.addEventListener("DOMContentLoaded", loadAnalytics);

Alpine.data("analytics", () => ({
  loadAnalytics,
}));

Change to:

export { loadAnalytics };

// REMOVE this line:
// document.addEventListener("DOMContentLoaded", loadAnalytics);

Alpine.data("analytics", () => ({
  loadAnalytics,
}));

Template Update: templates/analytics.templ

Line 14: Add x-init to body tag:

<!-- BEFORE -->
<body x-data="analytics" class="theme-{ user.Theme }">

<!-- AFTER -->
<body x-data="analytics" x-init="loadAnalytics" class="theme-{ user.Theme }">

Step 3.2: docs.ts

File: web/src/docs.ts

Current (around line 95-100):

document.addEventListener("DOMContentLoaded", () => {
  initializeDocsSearch();
});

export { toggleSidebar, initializeDocsSearch };

Alpine.data("docs", () => ({
  toggleSidebar,
  initializeSearch: initializeDocsSearch,
}));

Change to:

// REMOVE this entire block:
// document.addEventListener("DOMContentLoaded", () => {
//   initializeDocsSearch();
// });

export { toggleSidebar, initializeDocsSearch };

Alpine.data("docs", () => ({
  toggleSidebar,
  initializeSearch: initializeDocsSearch,
}));

Template Update: templates/docs.templ

Find the <body> tag and add x-init:

<!-- BEFORE -->
<body x-data="docs" class="theme-{ user.Theme }">

<!-- AFTER -->
<body x-data="docs" x-init="initializeSearch" class="theme-{ user.Theme }">

Step 3.3: library.ts - SSR-FIRST FIX REQUIRED

⚠️ CRITICAL SSR BUG: The initializeLibraryAdmin() function currently calls reloadLibraries() which fetches data from the API and replaces the SSR-rendered library list. This defeats SSR for the admin library page!

File: web/src/library.ts

Problem (around line 653-655):

function initializeLibraryAdmin(): void {
  // Setup event listeners
  const librariesList = document.getElementById("libraries-list");
  if (librariesList) {
    librariesList.addEventListener("click", handleLibraryListClick);
  }

  // ... more setup ...

  // ❌ BUG: This fetches data and replaces SSR content on page load!
  void reloadLibraries();
}

Fix - Remove data fetch from init:

function initializeLibraryAdmin(): void {
  // Setup event listeners
  const librariesList = document.getElementById("libraries-list");
  if (librariesList) {
    librariesList.addEventListener("click", handleLibraryListClick);
  }

  // ... keep all existing setup code ...

  // ✅ REMOVED: void reloadLibraries();
  // SSR provides initial library list - no need to fetch on page load
  // reloadLibraries() is still available to call AFTER create/delete/update operations
}

Why this fix matters:

  • SSR renders the library list on the server for fast initial load
  • Calling reloadLibraries() in x-init replaces this with a slower API call
  • reloadLibraries() should ONLY be called after CRUD operations (create/delete/update)
  • The initializeLibraryAdmin() should ONLY setup event listeners and modals

Template Update: templates/admin_library.templ

Current state (line 11):

<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">

Status: Already correct - template already has x-data and x-init setup Action: No template change needed - just fix library.ts to not fetch data in init

Step 3.4: dashboard.ts

File: web/src/dashboard.ts

This file needs special handling since it has a complex DOMContentLoaded block with multiple event listeners.

Current (lines 494-521):

document.addEventListener("DOMContentLoaded", () => {
  initDragAndDrop();

  document.addEventListener("click", (e: Event) => {
    // ... 70+ lines of event handling ...
  });

  document.addEventListener("input", (e: Event) => {
    // ... event handling ...
  });

  // ... more initialization ...
});

Create a wrapper function at the end of the file:

Add before the export statement (before line ~490):

// Wrapper function for dashboard initialization
function initDashboard() {
  initDragAndDrop();

  document.addEventListener("click", (e: Event) => {
    const target = e.target as HTMLElement;
    const actionElem = target.closest("[data-action]") as HTMLElement;
    const action = actionElem?.getAttribute("data-action");

    switch (action) {
      case "scroll-carousel": {
        const collectionId =
          target.dataset.direction || actionElem?.dataset.direction || "0";
        if (collectionId) scrollCarousel(collectionId, direction);
        break;
      }
      // ... keep all existing cases ...
    }
  });

  document.addEventListener("input", (e: Event) => {
    const target = e.target as HTMLElement;
    const actionElem = target.closest("[data-input-action]") as HTMLElement;
    const action = actionElem?.getAttribute("data-input-action");

    switch (action) {
      case "update-items-count": {
        const input = target as HTMLInputElement;
        const displayTarget = input.getAttribute("target");
        if (displayTarget) updateItemsCount(input, displayTarget);
        break;
      }
    }
  });

  // Load saved library on page load
  const librarySelect = document.getElementById(
    "library-select",
  ) as HTMLSelectElement;
  if (librarySelect) {
    librarySelect.addEventListener("change", (e) => {
      const target = e.target as HTMLSelectElement;
      if (target.value) {
        switchLibrary(target.value);
      }
    });
  }

  const savedLibrary = localStorage.getItem("selectedLibrary");
  const currentLibrary = new URLSearchParams(window.location.search).get(
    "library_id",
  );
  if (savedLibrary && savedLibrary !== currentLibrary) {
    window.location.href = `/dashboard?library_id=${savedLibrary}`;
  }
}

Remove the old DOMContentLoaded block (delete lines 494-521):

// DELETE this entire block:
// document.addEventListener("DOMContentLoaded", () => {
//   initDragAndDrop();
//   ... all 70+ lines ...
// });

Add to export statement:

export {
  closeDashboardSettings,
  openDashboardSettings,
  initDashboard,  // ← ADD THIS
  saveDashboardSettings,
  scrollCarousel,
  // ... keep all other exports ...
};

Add to Alpine.data:

Alpine.data("dashboard", () => ({
  closeDashboardSettings,
  openDashboardSettings,
  initDashboard,  // ← ADD THIS
  saveDashboardSettings,
  scrollCarousel,
  // ... keep all other exports ...
}));

Template Update: templates/dashboard.templ

Line 19: Add x-data and x-init to body tag:

<!-- BEFORE -->
<body class="theme-{ user.Theme }">

<!-- AFTER -->
<body class="theme-{ user.Theme }" x-data="dashboard" x-init="initDashboard()">

Step 3.5: Verify All TypeScript Files

# Build all TypeScript
npm run build:ts

# Should succeed with 0 errors

Step 4: Regenerate Templates

# Generate Go template files
templ generate

# Expected: Complete [updates=X duration=~40ms]
# where X is the number of templates modified

Step 5: Build and Verify

# Build the Go server
go build ./cmd/server

# If build succeeds, you're done!
# If build fails, check the error and fix accordingly

Step 6: Test in Browser

  1. Start the server:

    podman compose up -d
    
  2. Open browser and test:

    • Navigate to /collections - should work with no console errors
    • Navigate to /analytics - should load analytics data
    • Navigate to /dashboard - drag and drop should work
    • Navigate to /docs - search should work
    • Navigate to /admin/library - library management should work
  3. Check browser console:

    • No "X is not defined" errors
    • No "n.bind is not a function" errors
    • No "initX is not defined" errors

Summary

Progress So Far

Pages methodically reviewed:

  • Dashboard - Completed (drag-drop works, SSR maintained)
  • 🔄 Collections - In progress (dead exports removed, more fixes needed)
  • ⏸️ Admin Library - SSR bug identified, fix documented in Step 3.3
  • ⏸️ All other pages - Not yet reviewed, will be done page-by-page

Completed Changes

TypeScript Files (3 files):

  • web/src/collections.ts - Removed dead Alpine.js exports
  • web/src/analytics.ts - Removed DOMContentLoaded, SSR-first (intentional data fetch)
  • web/src/admin.ts - Removed DOMContentLoaded, WebSocket moved to template

Template Files (1 file):

  • templates/analytics.templ - Added x-init="loadAnalytics" (JS-heavy page, correct)

Pending Critical Fixes

SSR Bug in library.ts (Step 3.3):

  • initializeLibraryAdmin() calls reloadLibraries() which fetches data and replaces SSR
  • Fix documented: Remove data fetch from init, only setup event listeners
  • ⚠️ Template already correct: templates/admin_library.templ has x-data/x-init

What Was NOT Changed

  • "Add Books" modal - Still client-side Alpine.js (quick fix decision)
  • WebSocket code - Already moved to templates with server-side token injection
  • API endpoints - Already exist and work correctly
  • HTMX modals - Already implemented for collection CRUD
  • Search - Skipped pending user's planned revamp

SSR-First Principles Applied

Analytics (80% JS): x-init fetches data - intentional for dynamic page Dashboard (SSR + JS): x-init ONLY sets up event listeners, no data fetch Admin Library: Will fix to only setup event listeners, SSR provides data Collections: SSR provides data, JS for interactivity (in progress)

Result So Far

Collections dead exports removed Analytics loads correctly with SSR-first approach Admin WebSocket fixed with server-side token injection Admin library SSR bug identified, fix ready to implement Other pages will be reviewed methodically, one by one

Next Steps

Immediate (Critical SSR Bug):

  1. Fix web/src/library.ts Step 3.3 - Remove reloadLibraries() from initializeLibraryAdmin()
  2. Test admin library page to verify SSR content is not replaced

Continue Page-by-Page Review: 3. Complete collections page fixes 4. Review and fix remaining pages one at a time 5. Update this guide as each page is completed