@djangocfg/debuger 2.1.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +345 -0
- package/dist/chunk-ESIQODSI.mjs +115 -0
- package/dist/chunk-ESIQODSI.mjs.map +1 -0
- package/dist/chunk-PAWJFY3S.mjs +6 -0
- package/dist/chunk-PAWJFY3S.mjs.map +1 -0
- package/dist/chunk-YQE3KTBG.mjs +64 -0
- package/dist/chunk-YQE3KTBG.mjs.map +1 -0
- package/dist/customEmitter-BO-1IWxm.d.ts +57 -0
- package/dist/emitters/index.cjs +74 -0
- package/dist/emitters/index.cjs.map +1 -0
- package/dist/emitters/index.d.ts +33 -0
- package/dist/emitters/index.mjs +4 -0
- package/dist/emitters/index.mjs.map +1 -0
- package/dist/index.cjs +887 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.mjs +694 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger/index.cjs +123 -0
- package/dist/logger/index.cjs.map +1 -0
- package/dist/logger/index.d.ts +50 -0
- package/dist/logger/index.mjs +4 -0
- package/dist/logger/index.mjs.map +1 -0
- package/package.json +67 -0
- package/src/DebugButton.tsx +143 -0
- package/src/DebugPanel.tsx +171 -0
- package/src/bridges/index.ts +1 -0
- package/src/bridges/monitorBridge.ts +63 -0
- package/src/emitters/Emitter.ts +51 -0
- package/src/emitters/audioEmitter.ts +74 -0
- package/src/emitters/customEmitter.ts +42 -0
- package/src/emitters/index.ts +10 -0
- package/src/hooks/useAudioEventLog.ts +147 -0
- package/src/hooks/useCustomEventLog.ts +39 -0
- package/src/hooks/useDebugShortcut.ts +27 -0
- package/src/hooks/useStoreSnapshot.ts +70 -0
- package/src/index.ts +62 -0
- package/src/logger/index.ts +3 -0
- package/src/logger/logStore.ts +89 -0
- package/src/logger/logger.ts +95 -0
- package/src/logger/types.ts +39 -0
- package/src/panels/AudioDebugPanel.tsx +157 -0
- package/src/panels/LogsPanel.tsx +267 -0
- package/src/panels/StorePanel.tsx +71 -0
- package/src/store/debugStore.ts +42 -0
- package/src/styles/index.css +5 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
useDebugLogStore,
|
|
6
|
+
useDebugFilteredLogs,
|
|
7
|
+
useDebugLogCount,
|
|
8
|
+
useDebugErrorCount,
|
|
9
|
+
type LogLevel,
|
|
10
|
+
type LogEntry,
|
|
11
|
+
} from '../logger';
|
|
12
|
+
import {
|
|
13
|
+
Button,
|
|
14
|
+
CopyButton,
|
|
15
|
+
Badge,
|
|
16
|
+
Input,
|
|
17
|
+
Tooltip,
|
|
18
|
+
TooltipContent,
|
|
19
|
+
TooltipTrigger,
|
|
20
|
+
} from '@djangocfg/ui-core/components';
|
|
21
|
+
import { LazyJsonTree } from '@djangocfg/ui-tools';
|
|
22
|
+
import { useVirtualizer, type VirtualItem } from '@tanstack/react-virtual';
|
|
23
|
+
import {
|
|
24
|
+
Bug, Trash2, Download, ChevronDown, ChevronUp,
|
|
25
|
+
AlertCircle, Info, AlertTriangle, CheckCircle, Search,
|
|
26
|
+
} from 'lucide-react';
|
|
27
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Constants
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const ROW_ESTIMATE_PX = 32;
|
|
34
|
+
const OVERSCAN = 5;
|
|
35
|
+
|
|
36
|
+
const LOG_LEVEL_CONFIG: Record<LogLevel, { icon: React.ElementType; color: string }> = {
|
|
37
|
+
debug: { icon: Bug, color: 'text-muted-foreground' },
|
|
38
|
+
info: { icon: Info, color: 'text-blue-500' },
|
|
39
|
+
warn: { icon: AlertTriangle, color: 'text-yellow-500' },
|
|
40
|
+
error: { icon: AlertCircle, color: 'text-red-500' },
|
|
41
|
+
success: { icon: CheckCircle, color: 'text-green-500' },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const LOG_LEVELS = Object.keys(LOG_LEVEL_CONFIG) as LogLevel[];
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// LogEntryRow
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
interface LogEntryRowProps {
|
|
51
|
+
entry: LogEntry;
|
|
52
|
+
expanded: boolean;
|
|
53
|
+
onToggle: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function LogEntryRow({ entry, expanded, onToggle }: LogEntryRowProps) {
|
|
57
|
+
const config = LOG_LEVEL_CONFIG[entry.level];
|
|
58
|
+
const Icon = config.icon;
|
|
59
|
+
const pad = (n: number, len = 2) => String(n).padStart(len, '0');
|
|
60
|
+
const d = entry.timestamp;
|
|
61
|
+
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
|
|
62
|
+
const hasData = entry.data && Object.keys(entry.data).length > 0;
|
|
63
|
+
const hasStack = !!entry.stack;
|
|
64
|
+
const isExpandable = hasData || hasStack;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="border-b border-border/50 last:border-0">
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={isExpandable ? onToggle : undefined}
|
|
71
|
+
disabled={!isExpandable}
|
|
72
|
+
className={cn(
|
|
73
|
+
'flex w-full items-start gap-2 px-3 py-1.5 text-left text-xs',
|
|
74
|
+
isExpandable ? 'hover:bg-muted/50 cursor-pointer' : 'cursor-default'
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<Icon className={cn('h-3.5 w-3.5 mt-0.5 shrink-0', config.color)} />
|
|
78
|
+
<span className="text-muted-foreground font-mono shrink-0">{time}</span>
|
|
79
|
+
<Badge variant="outline" className="shrink-0 text-[10px] px-1 py-0">{entry.component}</Badge>
|
|
80
|
+
<span className="flex-1 truncate">{entry.message}</span>
|
|
81
|
+
{isExpandable && (
|
|
82
|
+
<span className="shrink-0 text-muted-foreground">
|
|
83
|
+
{expanded ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
|
84
|
+
</span>
|
|
85
|
+
)}
|
|
86
|
+
</button>
|
|
87
|
+
{expanded && isExpandable && (
|
|
88
|
+
<div className="px-3 pb-2 pl-8">
|
|
89
|
+
{hasData && <div className="mt-1"><LazyJsonTree data={entry.data} mode="compact" /></div>}
|
|
90
|
+
{hasStack && (
|
|
91
|
+
<pre className="mt-2 text-[10px] text-red-400 whitespace-pre-wrap font-mono bg-red-950/20 p-2 rounded">
|
|
92
|
+
{entry.stack}
|
|
93
|
+
</pre>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// LogsPanel
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
export function LogsPanel({ isActive }: { isActive: boolean }) {
|
|
106
|
+
const [expandedLogs, setExpandedLogs] = useState<Set<string>>(new Set());
|
|
107
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
108
|
+
const [selectedLevels, setSelectedLevels] = useState<Set<LogLevel>>(
|
|
109
|
+
new Set(['debug', 'info', 'warn', 'error', 'success'])
|
|
110
|
+
);
|
|
111
|
+
const [componentFilter, setComponentFilter] = useState('');
|
|
112
|
+
|
|
113
|
+
const logs = useDebugFilteredLogs();
|
|
114
|
+
const logCount = useDebugLogCount();
|
|
115
|
+
const errorCount = useDebugErrorCount();
|
|
116
|
+
const clearLogs = useDebugLogStore((s) => s.clearLogs);
|
|
117
|
+
const setFilter = useDebugLogStore((s) => s.setFilter);
|
|
118
|
+
const exportLogs = useDebugLogStore((s) => s.exportLogs);
|
|
119
|
+
|
|
120
|
+
// Virtualizer
|
|
121
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
122
|
+
const virtualizer = useVirtualizer({
|
|
123
|
+
count: logs.length,
|
|
124
|
+
getScrollElement: () => scrollRef.current,
|
|
125
|
+
estimateSize: () => ROW_ESTIMATE_PX,
|
|
126
|
+
overscan: OVERSCAN,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!isActive) return;
|
|
131
|
+
setFilter({
|
|
132
|
+
levels: Array.from(selectedLevels),
|
|
133
|
+
component: componentFilter || undefined,
|
|
134
|
+
search: searchQuery || undefined,
|
|
135
|
+
});
|
|
136
|
+
}, [isActive, selectedLevels, componentFilter, searchQuery, setFilter]);
|
|
137
|
+
|
|
138
|
+
const handleToggleExpand = useCallback((id: string) => {
|
|
139
|
+
setExpandedLogs((prev) => {
|
|
140
|
+
const next = new Set(prev);
|
|
141
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
142
|
+
return next;
|
|
143
|
+
});
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
const handleToggleLevel = useCallback((level: LogLevel) => {
|
|
147
|
+
setSelectedLevels((prev) => {
|
|
148
|
+
const next = new Set(prev);
|
|
149
|
+
next.has(level) ? next.delete(level) : next.add(level);
|
|
150
|
+
return next;
|
|
151
|
+
});
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
const handleExport = useCallback(() => {
|
|
155
|
+
const json = exportLogs();
|
|
156
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
157
|
+
const url = URL.createObjectURL(blob);
|
|
158
|
+
const a = document.createElement('a');
|
|
159
|
+
a.href = url;
|
|
160
|
+
a.download = `debug-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
|
|
161
|
+
a.click();
|
|
162
|
+
URL.revokeObjectURL(url);
|
|
163
|
+
}, [exportLogs]);
|
|
164
|
+
|
|
165
|
+
const handleClear = useCallback(() => {
|
|
166
|
+
clearLogs();
|
|
167
|
+
setExpandedLogs(new Set());
|
|
168
|
+
}, [clearLogs]);
|
|
169
|
+
|
|
170
|
+
const logsJson = exportLogs();
|
|
171
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
172
|
+
const totalSize = virtualizer.getTotalSize();
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className="flex flex-col h-full">
|
|
176
|
+
{/* Stats */}
|
|
177
|
+
<div className="flex items-center gap-2 border-b border-border px-3 py-1.5 shrink-0">
|
|
178
|
+
<Badge variant="secondary" className="text-[10px]">{logCount} logs</Badge>
|
|
179
|
+
{errorCount > 0 && <Badge variant="destructive" className="text-[10px]">{errorCount} errors</Badge>}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Filters */}
|
|
183
|
+
<div className="flex flex-wrap items-center gap-2 border-b border-border px-3 py-2 shrink-0">
|
|
184
|
+
<div className="relative flex-1 min-w-[140px]">
|
|
185
|
+
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
186
|
+
<Input
|
|
187
|
+
placeholder="Search..."
|
|
188
|
+
value={searchQuery}
|
|
189
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
190
|
+
className="h-7 pl-7 text-xs"
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
<Input
|
|
194
|
+
placeholder="Component..."
|
|
195
|
+
value={componentFilter}
|
|
196
|
+
onChange={(e) => setComponentFilter(e.target.value)}
|
|
197
|
+
className="h-7 w-24 text-xs"
|
|
198
|
+
/>
|
|
199
|
+
<div className="flex items-center gap-0.5">
|
|
200
|
+
{LOG_LEVELS.map((level) => {
|
|
201
|
+
const { icon: Icon, color } = LOG_LEVEL_CONFIG[level];
|
|
202
|
+
const active = selectedLevels.has(level);
|
|
203
|
+
return (
|
|
204
|
+
<Tooltip key={level}>
|
|
205
|
+
<TooltipTrigger asChild>
|
|
206
|
+
<Button variant={active ? 'secondary' : 'ghost'} size="icon" className="h-6 w-6" onClick={() => handleToggleLevel(level)}>
|
|
207
|
+
<Icon className={cn('h-3.5 w-3.5', active && color)} />
|
|
208
|
+
</Button>
|
|
209
|
+
</TooltipTrigger>
|
|
210
|
+
<TooltipContent>{level}</TooltipContent>
|
|
211
|
+
</Tooltip>
|
|
212
|
+
);
|
|
213
|
+
})}
|
|
214
|
+
</div>
|
|
215
|
+
<div className="flex items-center gap-0.5 ml-auto">
|
|
216
|
+
<CopyButton value={logsJson} size="icon" className="h-6 w-6" />
|
|
217
|
+
<Tooltip>
|
|
218
|
+
<TooltipTrigger asChild>
|
|
219
|
+
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={handleExport}>
|
|
220
|
+
<Download className="h-3.5 w-3.5" />
|
|
221
|
+
</Button>
|
|
222
|
+
</TooltipTrigger>
|
|
223
|
+
<TooltipContent>Export JSON</TooltipContent>
|
|
224
|
+
</Tooltip>
|
|
225
|
+
<Tooltip>
|
|
226
|
+
<TooltipTrigger asChild>
|
|
227
|
+
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={handleClear}>
|
|
228
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
229
|
+
</Button>
|
|
230
|
+
</TooltipTrigger>
|
|
231
|
+
<TooltipContent>Clear</TooltipContent>
|
|
232
|
+
</Tooltip>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Virtualized log list */}
|
|
237
|
+
<div ref={scrollRef} className="flex-1 overflow-auto">
|
|
238
|
+
{logs.length === 0 ? (
|
|
239
|
+
<div className="flex flex-col items-center justify-center py-10 text-muted-foreground">
|
|
240
|
+
<Bug className="h-8 w-8 mb-2 opacity-40" />
|
|
241
|
+
<p className="text-sm">No logs yet</p>
|
|
242
|
+
</div>
|
|
243
|
+
) : (
|
|
244
|
+
<div style={{ height: `${totalSize}px`, position: 'relative' }}>
|
|
245
|
+
{virtualItems.map((vItem: VirtualItem) => {
|
|
246
|
+
const entry = logs[vItem.index];
|
|
247
|
+
return (
|
|
248
|
+
<div
|
|
249
|
+
key={entry.id}
|
|
250
|
+
data-index={vItem.index}
|
|
251
|
+
ref={virtualizer.measureElement}
|
|
252
|
+
style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${vItem.start}px)` }}
|
|
253
|
+
>
|
|
254
|
+
<LogEntryRow
|
|
255
|
+
entry={entry}
|
|
256
|
+
expanded={expandedLogs.has(entry.id)}
|
|
257
|
+
onToggle={() => handleToggleExpand(entry.id)}
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
})}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ScrollArea } from '@djangocfg/ui-core/components';
|
|
5
|
+
import { LazyJsonTree } from '@djangocfg/ui-tools';
|
|
6
|
+
import { Database } from 'lucide-react';
|
|
7
|
+
import { useStoreSnapshot } from '../hooks/useStoreSnapshot';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface StorePanelProps {
|
|
14
|
+
/** Display label shown above the JSON tree */
|
|
15
|
+
label: string;
|
|
16
|
+
/**
|
|
17
|
+
* Getter for the store state slice to display.
|
|
18
|
+
* Use `() => useMyStore.getState()` — stable function ref is not required,
|
|
19
|
+
* the hook handles stabilization internally.
|
|
20
|
+
*/
|
|
21
|
+
getState: () => Record<string, unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Poll interval in ms. Default 200ms.
|
|
24
|
+
* Use higher values for stores that update very frequently.
|
|
25
|
+
*/
|
|
26
|
+
intervalMs?: number;
|
|
27
|
+
/** Only polls when active (panel tab is visible) */
|
|
28
|
+
isActive: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Component
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generic Zustand store viewer for the debug panel.
|
|
37
|
+
*
|
|
38
|
+
* Uses polling (not reactive subscription) to read external store state.
|
|
39
|
+
* This avoids Zustand v5 infinite loop issues and prevents the debug panel
|
|
40
|
+
* from re-rendering on every store change.
|
|
41
|
+
*
|
|
42
|
+
* @example — in consuming app's custom tab:
|
|
43
|
+
* import { StorePanel } from '@org/debuger';
|
|
44
|
+
* import { useTimelineStore } from '@stores/timelineStore';
|
|
45
|
+
*
|
|
46
|
+
* <StorePanel
|
|
47
|
+
* label="Timeline Store"
|
|
48
|
+
* getState={() => useTimelineStore.getState()}
|
|
49
|
+
* isActive={isActive}
|
|
50
|
+
* />
|
|
51
|
+
*/
|
|
52
|
+
export function StorePanel({ label, getState, intervalMs = 200, isActive }: StorePanelProps) {
|
|
53
|
+
const snapshot = useStoreSnapshot(getState, intervalMs, isActive);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex flex-col h-full">
|
|
57
|
+
<div className="flex items-center gap-2 border-b border-border px-3 py-2 shrink-0">
|
|
58
|
+
<Database className="h-3.5 w-3.5 text-muted-foreground" />
|
|
59
|
+
<span className="text-xs font-medium text-muted-foreground">{label}</span>
|
|
60
|
+
<span className="ml-auto text-[10px] text-muted-foreground/60">
|
|
61
|
+
polling {intervalMs}ms
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
<ScrollArea className="flex-1">
|
|
65
|
+
<div className="p-2">
|
|
66
|
+
<LazyJsonTree data={snapshot} mode="full" />
|
|
67
|
+
</div>
|
|
68
|
+
</ScrollArea>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { create } from 'zustand';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
export type DebugTab = 'logs' | 'audio' | 'pipeline' | 'custom';
|
|
10
|
+
|
|
11
|
+
export interface DebugStore {
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
tab: DebugTab;
|
|
14
|
+
isUnlocked: boolean;
|
|
15
|
+
open: () => void;
|
|
16
|
+
close: () => void;
|
|
17
|
+
toggle: () => void;
|
|
18
|
+
setTab: (t: DebugTab) => void;
|
|
19
|
+
unlock: (key: string) => boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Store
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
export const useDebugStore = create<DebugStore>((set) => ({
|
|
27
|
+
isOpen: false,
|
|
28
|
+
tab: 'logs',
|
|
29
|
+
isUnlocked: false,
|
|
30
|
+
|
|
31
|
+
open: () => set({ isOpen: true }),
|
|
32
|
+
close: () => set({ isOpen: false }),
|
|
33
|
+
toggle: () => set((s) => ({ isOpen: !s.isOpen })),
|
|
34
|
+
setTab: (tab) => set({ tab }),
|
|
35
|
+
|
|
36
|
+
unlock: (_key) => {
|
|
37
|
+
// Any non-empty value unlocks — no secret key required
|
|
38
|
+
const valid = true;
|
|
39
|
+
if (valid) set({ isUnlocked: true });
|
|
40
|
+
return valid;
|
|
41
|
+
},
|
|
42
|
+
}));
|