ui: improve relay tooltip, update docs

This commit is contained in:
Alejandro Gómez
2025-12-22 19:43:00 +01:00
parent 3f1c66ec01
commit c4bc3ab445
4 changed files with 87 additions and 41 deletions

View File

@@ -29,6 +29,12 @@ Grimoire is a Nostr protocol explorer and developer tool. It's a tiling window m
- Maintains failure counts, backoff states, last success/failure times
- Prevents repeated connection attempts to dead relays
**Nostr Query State Machine** (`src/lib/req-state-machine.ts` + `src/hooks/useReqTimelineEnhanced.ts`):
- Accurate tracking of REQ subscriptions across multiple relays
- Distinguishes between `LIVE`, `LOADING`, `PARTIAL`, `OFFLINE`, `CLOSED`, and `FAILED` states
- Solves "LIVE with 0 relays" bug by tracking per-relay connection state and event counts
- Pattern: Subscribe to relays individually to detect per-relay EOSE and errors
**Critical**: Don't create new EventStore, RelayPool, or RelayLiveness instances - use the singletons in `src/services/`
### Window System

View File

@@ -19,9 +19,32 @@ We'll combine two sources of truth:
This hybrid approach avoids duplicate subscriptions while providing accurate status tracking.
## Implementation Tasks
## Implementation Progress
### Phase 1: Core Infrastructure
### COMPLETED: Phase 1: Core Infrastructure
- [x] Task 1.1: Create Per-Relay State Tracking Types (`src/types/req-state.ts`)
- [x] Task 1.2: Create State Derivation Logic (`src/lib/req-state-machine.ts`)
- [x] Task 1.3: Create Enhanced Timeline Hook (`src/hooks/useReqTimelineEnhanced.ts`)
- [x] Unit tests for state machine (`src/lib/req-state-machine.test.ts`)
### COMPLETED: Phase 2: UI Integration
- [x] Task 2.1: Update ReqViewer Status Indicator with 8-state machine
- [x] Task 2.2: Enhance Relay Dropdown with Per-Relay Status and 2-column grid tooltip
- [x] Task 2.3: Add Empty/Error States (Failed, Offline, Partial)
### PENDING: Phase 3: Testing & Polish
- [ ] Task 3.1: Add Unit Tests for `useReqTimelineEnhanced` hook
- [ ] Task 3.2: Add Integration Tests for `ReqViewer` UI
- [ ] Task 3.3: Complete Manual Testing Checklist
### FUTURE: Phase 4: Future Enhancements
- [ ] Task 4.1: Relay Performance Metrics (Latency tracking)
- [ ] Task 4.2: Smart Relay Selection (Integrate with RelayLiveness)
- [ ] Task 4.3: Query Optimization Suggestions
---
## Original Implementation Tasks (Reference)
#### Task 1.1: Create Per-Relay State Tracking Types

View File

@@ -1057,59 +1057,76 @@ export default function ReqViewer({
// Build comprehensive tooltip content
const tooltipContent = (
<div className="space-y-1.5 text-xs">
<div className="font-semibold border-b border-border pb-1 mb-1">
<div className="space-y-3 text-xs p-1">
<div className="font-mono font-bold border-b border-border pb-2 mb-2 break-all text-primary">
{url}
</div>
<div className="space-y-1 text-muted-foreground">
<div className="flex items-center gap-2">
<span className="w-20">Connection:</span>
<span className="text-foreground">
{connIcon.label}
</span>
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
<div className="space-y-0.5">
<div className="text-[10px] uppercase font-bold text-muted-foreground tracking-tight">
Connection
</div>
<div className="flex items-center gap-1.5 font-medium">
<span className="shrink-0">{connIcon.icon}</span>
<span>{connIcon.label}</span>
</div>
</div>
<div className="flex items-center gap-2">
<span className="w-20">Auth:</span>
<span className="text-foreground">
{authIcon.label}
</span>
<div className="space-y-0.5">
<div className="text-[10px] uppercase font-bold text-muted-foreground tracking-tight">
Authentication
</div>
<div className="flex items-center gap-1.5 font-medium">
<span className="shrink-0">{authIcon.icon}</span>
<span>{authIcon.label}</span>
</div>
</div>
{reqState && (
<>
<div className="flex items-center gap-2">
<span className="w-20">Subscription:</span>
<span className="text-foreground capitalize">
{reqState.subscriptionState}
</span>
</div>
{reqState.eventCount > 0 && (
<div className="flex items-center gap-2">
<span className="w-20">Events:</span>
<span className="text-foreground">
{reqState.eventCount} received
</span>
<div className="space-y-0.5">
<div className="text-[10px] uppercase font-bold text-muted-foreground tracking-tight">
Subscription
</div>
)}
<div className="font-medium capitalize">
{reqState.subscriptionState}
</div>
</div>
<div className="space-y-0.5">
<div className="text-[10px] uppercase font-bold text-muted-foreground tracking-tight">
Events
</div>
<div className="flex items-center gap-1.5 font-medium">
<FileText className="size-3 text-muted-foreground" />
<span>{reqState.eventCount} received</span>
</div>
</div>
</>
)}
{nip65Info && (
<>
{nip65Info.readers.length > 0 && (
<div className="flex items-center gap-2">
<span className="w-20">Inbox:</span>
<span className="text-foreground">
<div className="space-y-0.5">
<div className="text-[10px] uppercase font-bold text-muted-foreground tracking-tight">
Inbox (Read)
</div>
<div className="font-medium">
{nip65Info.readers.length} author
{nip65Info.readers.length !== 1 ? "s" : ""}
</span>
</div>
</div>
)}
{nip65Info.writers.length > 0 && (
<div className="flex items-center gap-2">
<span className="w-20">Outbox:</span>
<span className="text-foreground">
<div className="space-y-0.5">
<div className="text-[10px] uppercase font-bold text-muted-foreground tracking-tight">
Outbox (Write)
</div>
<div className="font-medium">
{nip65Info.writers.length} author
{nip65Info.writers.length !== 1 ? "s" : ""}
</span>
</div>
</div>
)}
</>
@@ -1133,9 +1150,8 @@ export default function ReqViewer({
<div className="flex items-center gap-1.5 flex-shrink-0">
{/* Event count badge */}
{reqState && reqState.eventCount > 0 && (
<div className="flex items-center gap-1 text-[10px] text-muted-foreground font-medium">
<FileText className="size-2.5" />
<span>{reqState.eventCount}</span>
<div className="text-[10px] text-muted-foreground font-medium">
[{reqState.eventCount}]
</div>
)}
@@ -1146,7 +1162,8 @@ export default function ReqViewer({
<Check className="size-3 text-green-600/70" />
) : (
(reqState.subscriptionState === "receiving" ||
reqState.subscriptionState === "waiting") && (
reqState.subscriptionState ===
"waiting") && (
<Loader2 className="size-3 text-muted-foreground/40 animate-spin" />
)
)}

View File

@@ -78,7 +78,7 @@ export function getAuthIcon(relay: RelayState | undefined) {
},
none: {
icon: <Shield className="size-3 text-muted-foreground/40" />,
label: "No Authentication Required",
label: "Not required",
},
};
return iconMap[relay.authStatus] || iconMap.none;