mirror of
https://github.com/hoornet/vega.git
synced 2026-05-07 12:49:13 -07:00
Add subscription debug panel (Ctrl+Shift+D)
Hidden dev tool showing NDK uptime, live subscription status, per-relay connection state, per-tab last-updated timestamps, and recent feed diagnostics log. Polls every 2s while visible. Closes with Ctrl+Shift+D, Escape, or X button.
This commit is contained in:
@@ -21,6 +21,7 @@ import { HashtagFeed } from "./components/feed/HashtagFeed";
|
||||
import { PodcastsView } from "./components/podcast/PodcastsView";
|
||||
import { PodcastPlayerBar } from "./components/podcast/PodcastPlayerBar";
|
||||
import { ToastContainer } from "./components/shared/ToastContainer";
|
||||
import { DebugPanel } from "./components/shared/DebugPanel";
|
||||
import { HelpModal } from "./components/shared/HelpModal";
|
||||
import { useUIStore } from "./stores/ui";
|
||||
import { useUpdater } from "./hooks/useUpdater";
|
||||
@@ -53,6 +54,8 @@ function App() {
|
||||
const currentView = useUIStore((s) => s.currentView);
|
||||
const showHelp = useUIStore((s) => s.showHelp);
|
||||
const toggleHelp = useUIStore((s) => s.toggleHelp);
|
||||
const showDebugPanel = useUIStore((s) => s.showDebugPanel);
|
||||
const toggleDebugPanel = useUIStore((s) => s.toggleDebugPanel);
|
||||
const [onboardingDone, setOnboardingDone] = useState(
|
||||
() => !!localStorage.getItem("wrystr_pubkey")
|
||||
);
|
||||
@@ -110,6 +113,7 @@ function App() {
|
||||
</div>
|
||||
<PodcastPlayerBar />
|
||||
<ToastContainer />
|
||||
{showDebugPanel && <DebugPanel onClose={toggleDebugPanel} />}
|
||||
{showHelp && <HelpModal onClose={toggleHelp} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
140
src/components/shared/DebugPanel.tsx
Normal file
140
src/components/shared/DebugPanel.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { getNDK, getNDKUptimeMs } from "../../lib/nostr";
|
||||
import { isLiveSubActive, useFeedStore } from "../../stores/feed";
|
||||
import { getRecentDiagEntries, type DiagEntry } from "../../lib/feedDiagnostics";
|
||||
|
||||
interface RelayInfo {
|
||||
url: string;
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
interface DebugState {
|
||||
uptimeMs: number | null;
|
||||
liveSubActive: boolean;
|
||||
relays: RelayInfo[];
|
||||
lastUpdated: Record<string, number>;
|
||||
recentDiag: DiagEntry[];
|
||||
}
|
||||
|
||||
function formatUptime(ms: number): string {
|
||||
const sec = Math.floor(ms / 1000);
|
||||
if (sec < 60) return `${sec}s`;
|
||||
const min = Math.floor(sec / 60);
|
||||
const remSec = sec % 60;
|
||||
if (min < 60) return `${min}m ${remSec}s`;
|
||||
const hr = Math.floor(min / 60);
|
||||
const remMin = min % 60;
|
||||
return `${hr}h ${remMin}m`;
|
||||
}
|
||||
|
||||
function shortenUrl(url: string): string {
|
||||
return url.replace(/^wss?:\/\//, "").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
function timeAgo(ts: number): string {
|
||||
const sec = Math.floor((Date.now() - ts) / 1000);
|
||||
if (sec < 5) return "just now";
|
||||
if (sec < 60) return `${sec}s ago`;
|
||||
const min = Math.floor(sec / 60);
|
||||
if (min < 60) return `${min}m ago`;
|
||||
const hr = Math.floor(min / 60);
|
||||
return `${hr}h ago`;
|
||||
}
|
||||
|
||||
function readState(): DebugState {
|
||||
const ndk = getNDK();
|
||||
const relays = Array.from(ndk.pool?.relays?.values() ?? []).map((r) => ({
|
||||
url: r.url,
|
||||
connected: r.connected,
|
||||
}));
|
||||
return {
|
||||
uptimeMs: getNDKUptimeMs(),
|
||||
liveSubActive: isLiveSubActive(),
|
||||
relays,
|
||||
lastUpdated: useFeedStore.getState().lastUpdated,
|
||||
recentDiag: getRecentDiagEntries(5),
|
||||
};
|
||||
}
|
||||
|
||||
export function DebugPanel({ onClose }: { onClose: () => void }) {
|
||||
const [state, setState] = useState<DebugState>(readState);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setState(readState()), 2000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const connectedCount = state.relays.filter((r) => r.connected).length;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-4 z-50 w-80 bg-bg-raised/95 backdrop-blur-sm border border-border shadow-xl text-[11px] font-mono">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-border/50">
|
||||
<span className="text-text-muted uppercase tracking-widest text-[10px]">Debug</span>
|
||||
<button onClick={onClose} className="text-text-dim hover:text-text transition-colors">×</button>
|
||||
</div>
|
||||
|
||||
{/* Uptime + Live Sub */}
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-border/50">
|
||||
<span className="text-text-dim">
|
||||
NDK uptime: <span className="text-text">{state.uptimeMs !== null ? formatUptime(state.uptimeMs) : "—"}</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${state.liveSubActive ? "bg-success" : "bg-danger"}`} />
|
||||
<span className="text-text-dim">live sub {state.liveSubActive ? "on" : "off"}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Relays */}
|
||||
<div className="px-3 py-2 border-b border-border/50">
|
||||
<div className="text-text-dim mb-1">Relays ({connectedCount}/{state.relays.length})</div>
|
||||
<div className="space-y-0.5 max-h-32 overflow-y-auto">
|
||||
{state.relays.map((r) => (
|
||||
<div key={r.url} className="flex items-center gap-2">
|
||||
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${r.connected ? "bg-success" : "bg-danger"}`} />
|
||||
<span className="text-text-dim truncate">{shortenUrl(r.url)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feed Timestamps */}
|
||||
<div className="px-3 py-2 border-b border-border/50">
|
||||
<div className="text-text-dim mb-1">Last updated</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{(["global", "following", "trending"] as const).map((tab) => (
|
||||
<div key={tab}>
|
||||
<span className="text-text-dim">{tab}: </span>
|
||||
<span className="text-text">{state.lastUpdated[tab] ? timeAgo(state.lastUpdated[tab]) : "—"}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Diagnostics */}
|
||||
<div className="px-3 py-2">
|
||||
<div className="text-text-dim mb-1">Recent log</div>
|
||||
{state.recentDiag.length === 0 ? (
|
||||
<span className="text-text-dim">No entries</span>
|
||||
) : (
|
||||
<div className="space-y-0.5 max-h-28 overflow-y-auto">
|
||||
{state.recentDiag.map((d, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-[10px]">
|
||||
<span className="text-text-dim shrink-0">
|
||||
{new Date(d.ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
|
||||
</span>
|
||||
<span className="text-text truncate">{d.action}</span>
|
||||
{d.durationMs !== undefined && (
|
||||
<span className="text-text-dim shrink-0">{d.durationMs}ms</span>
|
||||
)}
|
||||
{d.eventsReturned !== undefined && (
|
||||
<span className="text-text-dim shrink-0">{d.eventsReturned}ev</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,11 +3,18 @@ import { useUIStore } from "../stores/ui";
|
||||
import { useFeedStore } from "../stores/feed";
|
||||
|
||||
export function useKeyboardShortcuts() {
|
||||
const { currentView, setView, goBack, toggleHelp } = useUIStore();
|
||||
const { currentView, setView, goBack, toggleHelp, showDebugPanel, toggleDebugPanel } = useUIStore();
|
||||
const { focusedNoteIndex, setFocusedNoteIndex, notes } = useFeedStore();
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
// Ctrl+Shift+D works everywhere, even in text fields
|
||||
if (e.ctrlKey && e.shiftKey && e.key === "D") {
|
||||
e.preventDefault();
|
||||
toggleDebugPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = (e.target as HTMLElement).tagName;
|
||||
if (tag === "INPUT" || tag === "TEXTAREA" || (e.target as HTMLElement).isContentEditable) return;
|
||||
switch (e.key) {
|
||||
@@ -21,6 +28,7 @@ export function useKeyboardShortcuts() {
|
||||
setTimeout(() => (document.querySelector("[data-search-input]") as HTMLInputElement)?.focus(), 50);
|
||||
break;
|
||||
case "Escape":
|
||||
if (showDebugPanel) { toggleDebugPanel(); break; }
|
||||
goBack();
|
||||
break;
|
||||
case "?":
|
||||
@@ -38,5 +46,5 @@ export function useKeyboardShortcuts() {
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [currentView, focusedNoteIndex, notes.length]);
|
||||
}, [currentView, focusedNoteIndex, notes.length, showDebugPanel]);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ function getLog(): DiagEntry[] {
|
||||
}
|
||||
}
|
||||
|
||||
export function getRecentDiagEntries(count = 5): DiagEntry[] {
|
||||
return getLog().slice(-count).reverse();
|
||||
}
|
||||
|
||||
function saveLog(entries: DiagEntry[]) {
|
||||
localStorage.setItem(DIAG_KEY, JSON.stringify(entries.slice(-MAX_ENTRIES)));
|
||||
}
|
||||
|
||||
@@ -57,16 +57,22 @@ export function saveRelayUrls(urls: string[]) {
|
||||
}
|
||||
|
||||
let ndk: NDK | null = null;
|
||||
let ndkCreatedAt: number | null = null;
|
||||
|
||||
export function getNDK(): NDK {
|
||||
if (!ndk) {
|
||||
ndk = new NDK({
|
||||
explicitRelayUrls: getStoredRelayUrls(),
|
||||
});
|
||||
ndkCreatedAt = Date.now();
|
||||
}
|
||||
return ndk;
|
||||
}
|
||||
|
||||
export function getNDKUptimeMs(): number | null {
|
||||
return ndkCreatedAt ? Date.now() - ndkCreatedAt : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the current NDK instance and create a fresh one.
|
||||
* Preserves the signer (login state) but resets all relay connections.
|
||||
@@ -87,6 +93,7 @@ export async function resetNDK(): Promise<void> {
|
||||
ndk = new NDK({
|
||||
explicitRelayUrls: getStoredRelayUrls(),
|
||||
});
|
||||
ndkCreatedAt = Date.now();
|
||||
|
||||
// Restore signer so user stays logged in
|
||||
if (oldSigner) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { getNDK, connectToRelays, ensureConnected, resetNDK, getStoredRelayUrls, addRelay, removeRelay, fetchWithTimeout, withTimeout, FEED_TIMEOUT, THREAD_TIMEOUT, SINGLE_TIMEOUT } from "./core";
|
||||
export { getNDK, getNDKUptimeMs, connectToRelays, ensureConnected, resetNDK, getStoredRelayUrls, addRelay, removeRelay, fetchWithTimeout, withTimeout, FEED_TIMEOUT, THREAD_TIMEOUT, SINGLE_TIMEOUT } from "./core";
|
||||
export { fetchGlobalFeed, fetchFollowFeed, fetchUserNotes, fetchUserNotesNIP65, fetchNoteById, fetchReplies, publishNote, publishReply, publishRepost, publishQuote, fetchHashtagFeed, fetchThreadEvents, fetchAncestors } from "./notes";
|
||||
export { publishProfile, publishContactList, fetchProfile, fetchFollowSuggestions, fetchMentions, fetchNewFollowers } from "./social";
|
||||
export { publishArticle, fetchArticle, fetchAuthorArticles, fetchArticleFeed, searchArticles, fetchByAddr } from "./articles";
|
||||
|
||||
@@ -11,6 +11,10 @@ const MAX_FEED_SIZE = 200;
|
||||
|
||||
// Live subscription handle — persists across store calls
|
||||
let liveSub: NDKSubscription | null = null;
|
||||
|
||||
export function isLiveSubActive(): boolean {
|
||||
return liveSub !== null;
|
||||
}
|
||||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
interface FeedState {
|
||||
|
||||
@@ -27,6 +27,7 @@ interface UIState {
|
||||
pendingArticleEvent: NDKEvent | null;
|
||||
pendingHashtag: string | null;
|
||||
showHelp: boolean;
|
||||
showDebugPanel: boolean;
|
||||
feedLanguageFilter: string | null;
|
||||
setView: (view: View) => void;
|
||||
setFeedTab: (tab: FeedTab) => void;
|
||||
@@ -40,6 +41,7 @@ interface UIState {
|
||||
setFeedLanguageFilter: (filter: string | null) => void;
|
||||
toggleSidebar: () => void;
|
||||
toggleHelp: () => void;
|
||||
toggleDebugPanel: () => void;
|
||||
}
|
||||
|
||||
const SIDEBAR_KEY = "wrystr_sidebar_collapsed";
|
||||
@@ -58,6 +60,7 @@ export const useUIStore = create<UIState>((set, _get) => ({
|
||||
pendingArticleEvent: null,
|
||||
pendingHashtag: null,
|
||||
showHelp: false,
|
||||
showDebugPanel: false,
|
||||
feedLanguageFilter: null,
|
||||
setView: (currentView) => set({ currentView }),
|
||||
setFeedTab: (feedTab) => set({ feedTab }),
|
||||
@@ -94,4 +97,5 @@ export const useUIStore = create<UIState>((set, _get) => ({
|
||||
return { sidebarCollapsed: next };
|
||||
}),
|
||||
toggleHelp: () => set((s) => ({ showHelp: !s.showHelp })),
|
||||
toggleDebugPanel: () => set((s) => ({ showDebugPanel: !s.showDebugPanel })),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user