diff --git a/CLAUDE.md b/CLAUDE.md
index 44c2e9e..801421a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -23,7 +23,13 @@ Grimoire is a Nostr protocol explorer and developer tool. It's a tiling window m
- Reactive: components subscribe via hooks, auto-update on new events
- Handles replaceable events automatically (profiles, contact lists, etc.)
-**Critical**: Don't create new EventStore or RelayPool instances - use the singletons in `src/services/`
+**Relay State** (`src/services/relay-liveness.ts`):
+- Singleton `RelayLiveness` tracks relay health across sessions
+- Persisted to Dexie `relayLiveness` table
+- Maintains failure counts, backoff states, last success/failure times
+- Prevents repeated connection attempts to dead relays
+
+**Critical**: Don't create new EventStore, RelayPool, or RelayLiveness instances - use the singletons in `src/services/`
### Window System
@@ -82,11 +88,23 @@ Use hooks like `useProfile()`, `useNostrEvent()`, `useTimeline()` - they handle
- Active account stored in Jotai state, synced via `useAccountSync` hook
- Use inbox/outbox relay pattern for user relay lists
+**Event Rendering**:
+- Feed renderers: `KindRenderer` component with `renderers` registry in `src/components/nostr/kinds/index.tsx`
+- Detail renderers: `DetailKindRenderer` component with `detailRenderers` registry
+- Registry pattern allows adding new kind renderers without modifying parent components
+- Falls back to `DefaultKindRenderer` or feed renderer for unregistered kinds
+
**Mosaic Layout**:
- Layout mutations via `updateLayout()` callback only
- Don't traverse or modify layout tree manually
- Adding/removing windows handled by `logic.ts` functions
+**Error Boundaries**:
+- All event renderers wrapped in `EventErrorBoundary` component
+- Prevents one broken event from crashing entire feed or detail view
+- Provides diagnostic UI with retry capability and error details
+- Error boundaries auto-reset when event changes
+
## Testing
**Test Framework**: Vitest with node environment
diff --git a/TODO.md b/TODO.md
index 01c0472..9ba23c9 100644
--- a/TODO.md
+++ b/TODO.md
@@ -24,24 +24,11 @@ Current RTL implementation is partial and has limitations:
**Test case**: Arabic text with hashtags on same line should display properly with right-alignment.
-### Live Mode Reliability
-**Priority**: High
-**File**: `src/components/ReqViewer.tsx`
-
-**Issues**:
-- Live mode sometimes stops updating (gets stuck)
-- May be related to reconnection on errors
-- Compact live indicator needed for better UX
-
-**Investigation needed**: Check relay reconnection logic and subscription lifecycle.
-
### Rendering Issues
**Priority**: Medium
-- **Window crashes on unsupported kind event** - Need graceful error handling for unknown kinds
- **Nested lists in Markdown should be padded** - Markdown renderer spacing issue
- **Text rendering**: Avoid inserting ` ` tags, investigate noStrudel's EOL metadata approach
-- **JSON viewer scrolling**: Expandable JSON in event details cannot be scrolled when content exceeds available height - needs overflow handling
## Command Palette / UX Improvements
@@ -88,9 +75,41 @@ When an action is entered, show the list of available options below and provide
**Priority**: Low
**Description**: Allow setting background color or theme for individual columns, helping visually organize workspace.
+## Recent Improvements ✅
+
+### Relay Liveness Persistence
+**Completed**: 2024-12-17
+**Files**: `src/services/db.ts`, `src/services/relay-liveness.ts`
+
+Relay health tracking now persists across sessions:
+- Added Dexie v8 migration with `relayLiveness` table
+- Created storage adapter implementing `LivenessStorage` interface
+- Relay failure counts, backoff states persist across app restarts
+- Prevents repeated connection attempts to dead relays
+
+### Detail Renderer Registry
+**Completed**: 2024-12-17
+**Files**: `src/components/nostr/kinds/index.tsx`, `src/components/EventDetailViewer.tsx`
+
+Refactored detail view rendering to use registry pattern:
+- Removed 25-line hardcoded if-else chain from EventDetailViewer
+- Created `detailRenderers` map with 11 specialized detail renderers
+- New detail renderers can be added without modifying EventDetailViewer
+- Falls back to feed renderer for kinds without custom detail views
+
+### Event Error Boundaries
+**Completed**: 2024-12-17
+**Files**: `src/components/EventErrorBoundary.tsx`, `src/components/nostr/Feed.tsx`, `src/components/EventDetailViewer.tsx`
+
+All event renderers now protected with error boundaries:
+- One broken event no longer crashes entire feed
+- Diagnostic UI shows kind, ID, error message, component stack
+- Retry button and collapsible details for debugging
+- Auto-resets when event changes
+
## Planned Improvements
-- **App-wide error boundary** - Splash crash screen for unhandled errors
+- **App-wide error boundary** - Splash crash screen for unhandled errors (separate from event-level boundaries)
- **Collapsible relay list** - Show user relay links without inbox/outbox icons initially
- **NIP badges everywhere** - Use consistent NIP badge components for linking to NIP documentation
- **External spec event kind support** - Add references and documentation links for commented-out event kinds from external specs (Blossom, Marmot Protocol, NKBIP, nostrocket, Corny Chat, NUD, etc.) in `src/constants/kinds.ts`. Consider adding a separate registry or documentation for non-official-NIP event kinds.
@@ -258,20 +277,7 @@ When an action is entered, show the list of available options below and provide
### Phase 1: Foundation Fixes (1-2 weeks)
**Goal:** Fix critical architectural issues and quick wins
-#### 1.1 Unified Detail Renderer Registry
-**Priority**: High | **Effort**: Low
-**Files**: `src/components/nostr/kinds/index.tsx`, `src/components/EventDetailViewer.tsx`
-
-**Problem**: EventDetailViewer uses hardcoded switch statement instead of registry pattern
-```tsx
-// Current anti-pattern:
-event.kind === kinds.Metadata ?
-: event.kind === kinds.Contacts ?
-```
-
-**Solution**: Create `detailRenderers` map parallel to `kindRenderers` with fallback logic
-
-#### 1.2 Systematic Depth Tracking
+#### 1.1 Systematic Depth Tracking
**Priority**: High | **Effort**: Medium
**Files**: All `*Renderer.tsx` files
@@ -283,15 +289,7 @@ event.kind === kinds.Metadata ?
- Audit all renderers using `EmbeddedEvent`
- Implement `CollapsedPreview` component for max depth exceeded
-#### 1.3 Error Boundaries
-**Priority**: High | **Effort**: Low
-**File**: `src/components/EventErrorBoundary.tsx`
-
-**Problem**: One broken event crashes entire feed
-
-**Solution**: Create `EventErrorBoundary` component wrapping all events with diagnostic error cards
-
-#### 1.4 Renderer Memoization
+#### 1.2 Renderer Memoization
**Priority**: Medium | **Effort**: Low
**Files**: All `*Renderer.tsx` files
diff --git a/src/components/ConnViewer.tsx b/src/components/ConnViewer.tsx
index e27cd29..111fcd2 100644
--- a/src/components/ConnViewer.tsx
+++ b/src/components/ConnViewer.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { toast } from "sonner";
import {
Wifi,
@@ -11,6 +11,10 @@ import {
Shield,
XCircle,
Settings,
+ Activity,
+ Clock,
+ AlertCircle,
+ Skull,
} from "lucide-react";
import { useRelayState } from "@/hooks/useRelayState";
import type { RelayState } from "@/types/relay-state";
@@ -26,6 +30,7 @@ import {
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { isAuthPreference } from "@/lib/type-guards";
+import liveness from "@/services/relay-liveness";
/**
* CONN viewer - displays connection and auth status for all relays in the pool
@@ -44,6 +49,9 @@ function ConnViewer() {
.filter((r) => r.connectionState !== "connected")
.sort((a, b) => a.url.localeCompare(b.url));
+ // Get all seen relays for liveness section
+ const seenRelays = liveness.getSeenRelays().sort();
+
return (