Files
client/plans/db-grouped-by-kind.md
2026-04-17 16:52:51 -04:00

6.2 KiB

DB Page: Grouped-by-Kind with Pagination

Problem

The www/db.html page loads all events from IndexedDB at once via store.getAll(), parses every event, and renders every row into a single <table>. With thousands of events, this causes:

  • Slow initial render (thousands of DOM rows created at once)
  • Unresponsive filtering/sorting (re-renders entire table on every keystroke)
  • No way to get a quick overview of what kinds of events exist

Solution Overview

Replace the flat table with a two-level grouped view:

  1. Kind Summary View — initial load shows one row per kind with count, newest, and oldest timestamps
  2. Expanded Kind View — clicking a kind row expands it to show paginated event rows for that kind
  3. Pagination — each expanded kind has its own pagination controls
flowchart TD
    A[Page Load] --> B[getAllEvents from IndexedDB]
    B --> C[Parse events into currentEvents]
    C --> D[groupEventsByKind]
    D --> E[renderKindSummary]
    
    E --> F{User clicks kind row}
    F -->|Expand| G[renderExpandedKind with pagination]
    F -->|Collapse| E
    
    G --> H[Per-kind filter/sort/paginate]
    H --> G

Detailed Design

New State Variables

Add to the GLOBAL VARIABLES section of db.html:

// Grouped view state
let kindGroups = new Map();        // Map<number, Event[]>
let expandedKinds = new Set();     // Set<number> - which kinds are open
let kindPageState = new Map();     // Map<number, {page, pageSize, sortCol, sortDir, filters}>

const DEFAULT_PAGE_SIZE = 50;
const PAGE_SIZE_OPTIONS = [25, 50, 100];

Function: groupEventsByKind

Groups the parsed currentEvents array into a Map<kind, Event[]>:

function groupEventsByKind(events) {
    const groups = new Map();
    for (const evt of events) {
        const kind = evt.kind ?? 'unknown';
        if (!groups.has(kind)) groups.set(kind, []);
        groups.get(kind).push(evt);
    }
    return groups;
}

Function: renderKindSummary

Replaces the current renderTable() as the initial view. Renders a compact summary table:

Kind Count Newest Oldest
0 42 2024-03-19 2024-01-15
1 1,203 2024-03-19 2023-06-01
3 87 2024-03-18 2024-02-10
  • The ▶/▼ indicator uses the existing HamburgerMorphing component (arrow_right / arrow_down)
  • Clicking a row toggles that kind in expandedKinds
  • When expanded, the events table for that kind renders directly below the summary row
  • divStats shows total: "12,847 events across 23 kinds"

Function: renderExpandedKind

When a kind is expanded, renders:

  1. A sub-table with the existing column layout: commands | kind | created_at | id | pubkey | content | tags | relay
  2. Filter row (same wildcard inputs as current)
  3. Paginated rows (only renders current page worth of <tr> elements)
  4. Pagination controls bar below the sub-table

Pagination Controls

Per expanded kind, rendered below the event sub-table:

[◀ Prev] Page 1 of 25 [Next ▶]  |  Show: [25] [50] [100]  |  Showing 1-50 of 1,203
  • Each kind maintains independent page state in kindPageState
  • Changing page size resets to page 1
  • Filtering resets to page 1

Per-Kind Filter and Sort

  • Each expanded kind has its own filter inputs and sort state stored in kindPageState
  • Filtering operates only on events of that kind
  • Sorting operates only on events of that kind
  • Both reset pagination to page 1

CSS Additions

New styles needed:

  • .kind-summary-row — styled like a header row, clickable, with hover effect
  • .kind-summary-row .expand-icon — container for the HamburgerMorphing expand/collapse indicator
  • .kind-expanded-section — container for the expanded event table + pagination
  • .pagination-bar — flex row for pagination controls
  • .pagination-btn — styled like existing .btnCmd
  • .pagination-info — muted text showing current range
  • .page-size-btn / .page-size-btn.active — page size selector buttons

Changes to Existing Functions

loadEvents

  • After parsing events into currentEvents, call groupEventsByKind(currentEvents) to populate kindGroups
  • Call renderKindSummary() instead of renderTable()

renderTable

  • Kept as-is but renamed to renderKindEventTable(kind, events, pageState) — only renders events for a single kind
  • Receives a subset of events (already filtered/sorted/paginated)

getAllEvents

  • No changes needed — still loads all events from IndexedDB
  • Future optimization: could use IndexedDB indexes to count by kind without loading all data, but that is out of scope for this change

Event Flow

  1. Page loads → initDatabase()loadEvents()
  2. loadEvents() calls getAllEvents(), parses into currentEvents
  3. groupEventsByKind(currentEvents)kindGroups Map
  4. renderKindSummary() renders the summary table (fast — only N rows where N = number of distinct kinds)
  5. User clicks kind row → toggleKind(kind) adds/removes from expandedKinds
  6. If expanding: initializes kindPageState for that kind, calls renderExpandedKind(kind)
  7. renderExpandedKind(kind) gets events from kindGroups.get(kind), applies filters/sort from kindPageState, slices for current page, renders sub-table + pagination
  8. User interacts with pagination/filter/sort → updates kindPageState, re-renders only that kind section

Preserved Functionality

  • Raw JSON modal (view/copy) — works exactly as before, using currentDisplayedEvents index
  • Wildcard filtering — same wildcardToRegex logic, scoped per-kind
  • Column sorting — same sortEvents logic, scoped per-kind
  • HamburgerMorphing sort icons — same pattern, scoped per-kind table
  • Refresh button — reloads all events, re-groups, re-renders summary

Implementation Order

  1. Add new state variables
  2. Create groupEventsByKind() function
  3. Create renderKindSummary() function with expand/collapse
  4. Create renderExpandedKind() function with pagination
  5. Add pagination controls and handlers
  6. Wire up per-kind filtering
  7. Wire up per-kind sorting
  8. Add CSS styles
  9. Update loadEvents() to use new pipeline
  10. Update divStats for summary counts
  11. Test with existing raw JSON modal functionality