Files
client/plans/event-management-page.md
2026-04-17 16:52:51 -04:00

11 KiB

Event Management Page (event-management.html)

Overview

A unified page for managing all of the user's Nostr events across every kind. Three-column drill-down layout: Kinds → Events → Relay Status. Supports deletion (kind 5) with per-relay verification, and blocking (via mute-list.mjs kind 10000) as a fallback when deletion fails.

Existing Infrastructure

Component Location Role
Mute/block list mute-list.mjs NIP-51 kind:10000 — addMute('e', eventId, isPrivate) blocks an event across all pages
Block UI block.html Manages mute list entries (pubkeys, hashtags, words, event IDs)
DB browser db.html Reads IndexedDB ndk-shared store, grouped by kind
Event editor event.html Loads/edits/reposts individual events by kind
Publish flow init-ndk.mjs publishEvent()ndk-worker.js handlePublish() Returns relayResults.successful[] and relayResults.failed[]
Per-relay fetch init-ndk.mjs fetchEventsFromAllRelays() Currently wraps ndkFetchEvents — returns combined, not per-relay
NDK subscribe init-ndk.mjs subscribe() Live subscription with ndkEvent window events

Architecture

Three-Column Layout

graph LR
    A[Left Column: Kind List] -->|click kind| B[Center Column: Events for Kind]
    B -->|click event| C[Right Column: Relay Detail + Actions]

Left Column — Kind Summary

  • On page load, reads IndexedDB cache to build a list of kinds the user has published
  • Each row: kind number, human-readable label, event count
  • Clicking a kind populates the center column
  • Optional "Refresh from Relays" button to hydrate from live relay data

Center Column — Events for Selected Kind

  • Lists all events of the selected kind, sorted by created_at descending
  • Each row shows: truncated event ID, d-tag or content preview, timestamp
  • For addressable events (kinds 30000+): shows the d-tag as primary identifier
  • Clicking an event populates the right column
  • Batch actions: "Select All" checkbox for bulk delete

Right Column — Event Detail + Relay Status + Actions

  • Shows full event JSON (collapsible, like db.html raw modal)
  • Relay presence table: which relays have this event
  • Action buttons: Delete, Block, Copy JSON
  • After delete: per-relay verification results

Startup Flow

flowchart TD
    A[Page Load] --> B[Read IndexedDB ndk-shared]
    B --> C[Group cached events by kind]
    C --> D[Render Kind List in left column]
    D --> E[User clicks a kind]
    E --> F[Show cached events for that kind]
    F --> G[Optional: Subscribe for live updates]
    G --> H[User clicks an event]
    H --> I[Show event detail + relay info]

The page starts fast by reading only from IndexedDB. No relay queries on startup. Relay hydration happens lazily:

  1. When the user clicks a kind, a background subscription fetches fresh data for that kind
  2. When the user clicks an event and wants relay detail, we query individual relays

Repost / Copy to All Relays Flow

When the relay status table shows an event exists on only some relays, the user can "Repost to All Relays" to spread it to every connected relay. This reuses the existing publishRawEvent worker message (same as event.html's "Repost as-is" feature) — the event is already signed, so it gets published without re-signing.

flowchart TD
    A[User clicks Repost to All Relays] --> B[Send already-signed event via publishRawEvent]
    B --> C[Show relayResults.successful and relayResults.failed]
    C --> D[Update relay status table with new presence]

Delete Flow

flowchart TD
    A[User clicks Delete] --> B[Confirm dialog]
    B --> C[Publish kind 5 deletion event]
    C --> D[Show which relays accepted the delete request]
    D --> E[Wait 2-3 seconds]
    E --> F[Re-query each relay individually for the event]
    F --> G{Event still present?}
    G -->|Yes| H[Show relay as: Delete failed - offer Block]
    G -->|No| I[Show relay as: Deleted successfully]

Block Flow

When deletion fails on some relays, the user can block the event:

  • Calls addMute('e', eventId, isPrivate) from mute-list.mjs
  • This adds the event ID to the kind:10000 mute list
  • All other pages that call isEventMuted() will filter it out
  • The block is immediate and works regardless of relay cooperation

Per-Relay Query Enhancement

The current fetchEventsFromAllRelays() returns combined results, not per-relay. We need a new worker message type to query a specific relay for a specific event:

New worker message: queryRelay

  • Input: { type: 'queryRelay', relayUrl, filter, requestId }
  • Opens a temporary connection to the specific relay (or uses existing pool connection)
  • Returns events found on that specific relay
  • Used for post-delete verification

Implementation Plan

Phase 1: Page Shell + Kind List from Cache

  • Create event-management.html with standard page template (header, footer, sidenav, hamburger)
  • Three-column responsive CSS layout (collapses to stacked on mobile)
  • On init: open IndexedDB ndk-shared, read all events, group by kind
  • Render left column with kind list (kind number, label, count)
  • Kind label lookup table for common kinds (0=Profile, 1=Note, 3=Contacts, 10002=Relay List, 30023=Article, 31123=Skill, etc.)

Phase 2: Event List for Selected Kind

  • Click a kind → populate center column with events of that kind
  • Show event ID (truncated), d-tag for addressable kinds, content preview, created_at date
  • Sort by created_at descending
  • Filter/search input for the event list
  • Pagination or virtual scroll for kinds with many events
  • Background subscription to hydrate fresh events for the selected kind

Phase 3: Event Detail + Relay Status

  • Click an event → populate right column with full detail
  • Collapsible raw JSON viewer
  • Parsed tag display (key tags shown as labeled fields)
  • "Check Relays" button that queries each connected relay for this event
  • Relay status table: relay URL, status icon (present/absent/checking)
  • "Repost to All Relays" button — republishes the already-signed event to all connected relays (uses publishRawEvent worker message, same as event.html's Repost feature)
  • After repost: update relay status table showing which relays now have the event

Phase 4: Delete with Per-Relay Verification

  • Delete button → confirmation dialog
  • Publish kind 5 deletion request via publishEvent()
  • Display which relays accepted the deletion request (from relayResults)
  • After short delay, re-query each relay to verify event removal
  • Update relay status table with verification results (deleted / still present / error)
  • Add queryRelay message type to ndk-worker.js for single-relay queries

Phase 5: Block Integration

  • Block button on event detail (always available)
  • "Block" button highlighted/suggested when delete verification shows failures
  • Calls addMute('e', eventId, isPrivate) from mute-list.mjs
  • Private/public toggle for the block (default: private)
  • Visual indicator if event is already blocked
  • Configure mute list on page init (same pattern as block.html)

Phase 6: Polish + Batch Operations

  • Batch select events in center column
  • Batch delete selected events
  • Batch block selected events
  • Export selected events as JSON
  • Kind list refresh button (re-subscribe to all user events)
  • Status bar showing operation progress

File Changes

File Change
www/event-management.html New — full page
www/ndk-worker.js Add queryRelay message handler for single-relay event queries
www/js/init-ndk.mjs Add queryRelay() export that sends the new message type

UI Wireframe

┌─────────────────────────────────────────────────────────────────────┐
│ ☰  Event Management                                        [avatar]│
├──────────────┬──────────────────────┬───────────────────────────────┤
│ Kinds        │ Events (Kind 1)      │ Event Detail                  │
│              │                      │                               │
│ [search]     │ [search/filter]      │ ID: abc123...                 │
│              │                      │ Created: 2025-01-15           │
│  0  Profile 1│ ☐ abc1.. 2025-01-15 │ Content: Hello world...       │
│  1  Notes  47│ ☑ def2.. 2025-01-14 │                               │
│  3  Contacts │ ☐ ghi3.. 2025-01-13 │ ┌─ Raw JSON ──────────────┐  │
│  7  Reactions│ ☐ jkl4.. 2025-01-12 │ │ { "kind": 1, ... }      │  │
│ 10002 Relays │                      │ └─────────────────────────┘  │
│ 30023 Article│                      │                               │
│ 31123 Skills │                      │ Relay Status:                 │
│              │                      │ ✅ wss://relay1  present      │
│              │                      │ ✅ wss://relay2  present      │
│              │                      │ ❌ wss://relay3  not found    │
│              │                      │                               │
│              │ [Delete Selected]    │ [Repost All] [Delete] [Block] │
│              │                      │ [Copy JSON]                   │
│ [Refresh]    │                      │ Delete Results:               │
│              │                      │ ✅ relay1: deleted             │
│              │                      │ ⚠️ relay2: still present      │
│              │                      │   → [Block this event]        │
├──────────────┴──────────────────────┴───────────────────────────────┤
│ footer: relay status                                        0 sats  │
└─────────────────────────────────────────────────────────────────────┘

Kind Label Reference

Kind Label
0 Profile
1 Note
3 Contacts
4 Encrypted DM
5 Deletion
6 Repost
7 Reaction
10000 Mute List
10002 Relay List
10123 Skill Adoption
30023 Long-form Article
30024 Draft Article
30078 App-specific Data
31123 Skill (Public)
31124 Skill (Private)

Responsive Behavior

  • Desktop (>1280px): Three columns side by side
  • Tablet (800-1280px): Two columns (kinds + events), detail as overlay/modal
  • Mobile (<800px): Single column, drill-down navigation with back buttons