@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.
Files changed (46) hide show
  1. package/README.md +345 -0
  2. package/dist/chunk-ESIQODSI.mjs +115 -0
  3. package/dist/chunk-ESIQODSI.mjs.map +1 -0
  4. package/dist/chunk-PAWJFY3S.mjs +6 -0
  5. package/dist/chunk-PAWJFY3S.mjs.map +1 -0
  6. package/dist/chunk-YQE3KTBG.mjs +64 -0
  7. package/dist/chunk-YQE3KTBG.mjs.map +1 -0
  8. package/dist/customEmitter-BO-1IWxm.d.ts +57 -0
  9. package/dist/emitters/index.cjs +74 -0
  10. package/dist/emitters/index.cjs.map +1 -0
  11. package/dist/emitters/index.d.ts +33 -0
  12. package/dist/emitters/index.mjs +4 -0
  13. package/dist/emitters/index.mjs.map +1 -0
  14. package/dist/index.cjs +887 -0
  15. package/dist/index.cjs.map +1 -0
  16. package/dist/index.d.ts +162 -0
  17. package/dist/index.mjs +694 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/logger/index.cjs +123 -0
  20. package/dist/logger/index.cjs.map +1 -0
  21. package/dist/logger/index.d.ts +50 -0
  22. package/dist/logger/index.mjs +4 -0
  23. package/dist/logger/index.mjs.map +1 -0
  24. package/package.json +67 -0
  25. package/src/DebugButton.tsx +143 -0
  26. package/src/DebugPanel.tsx +171 -0
  27. package/src/bridges/index.ts +1 -0
  28. package/src/bridges/monitorBridge.ts +63 -0
  29. package/src/emitters/Emitter.ts +51 -0
  30. package/src/emitters/audioEmitter.ts +74 -0
  31. package/src/emitters/customEmitter.ts +42 -0
  32. package/src/emitters/index.ts +10 -0
  33. package/src/hooks/useAudioEventLog.ts +147 -0
  34. package/src/hooks/useCustomEventLog.ts +39 -0
  35. package/src/hooks/useDebugShortcut.ts +27 -0
  36. package/src/hooks/useStoreSnapshot.ts +70 -0
  37. package/src/index.ts +62 -0
  38. package/src/logger/index.ts +3 -0
  39. package/src/logger/logStore.ts +89 -0
  40. package/src/logger/logger.ts +95 -0
  41. package/src/logger/types.ts +39 -0
  42. package/src/panels/AudioDebugPanel.tsx +157 -0
  43. package/src/panels/LogsPanel.tsx +267 -0
  44. package/src/panels/StorePanel.tsx +71 -0
  45. package/src/store/debugStore.ts +42 -0
  46. package/src/styles/index.css +5 -0
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+
5
+ // ============================================================================
6
+ // Shallow equal (avoids unnecessary re-renders)
7
+ // ============================================================================
8
+
9
+ function shallowEqual(a: unknown, b: unknown): boolean {
10
+ if (a === b) return true;
11
+ if (typeof a !== 'object' || typeof b !== 'object') return false;
12
+ if (a === null || b === null) return false;
13
+ const keysA = Object.keys(a as object);
14
+ const keysB = Object.keys(b as object);
15
+ if (keysA.length !== keysB.length) return false;
16
+ for (const key of keysA) {
17
+ if ((a as Record<string, unknown>)[key] !== (b as Record<string, unknown>)[key]) return false;
18
+ }
19
+ return true;
20
+ }
21
+
22
+ // ============================================================================
23
+ // Hook
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Polling-based external Zustand store snapshot.
28
+ *
29
+ * Reads the store state via `getState()` on an interval — NOT via reactive
30
+ * subscription. This prevents debug panel renders from being triggered by
31
+ * high-frequency store updates and avoids Zustand v5 infinite loop issues
32
+ * (returning new object references from selectors without useShallow).
33
+ *
34
+ * Only polls while `active` is true (panel tab is visible).
35
+ *
36
+ * @param getState A stable function that returns the current store state slice.
37
+ * Use `() => useMyStore.getState()` — the hook ref-stabilizes it.
38
+ * @param intervalMs Poll interval in ms. Default 200ms.
39
+ * @param active Whether to poll. Pass `isActive` from panel props.
40
+ *
41
+ * @example
42
+ * const snap = useStoreSnapshot(
43
+ * () => useTimelineStore.getState(),
44
+ * 200,
45
+ * isActive
46
+ * );
47
+ */
48
+ export function useStoreSnapshot<T>(
49
+ getState: () => T,
50
+ intervalMs = 200,
51
+ active = true,
52
+ ): T {
53
+ const getStateRef = useRef(getState);
54
+ getStateRef.current = getState;
55
+
56
+ const [snapshot, setSnapshot] = useState<T>(() => getState());
57
+
58
+ useEffect(() => {
59
+ if (!active) return;
60
+
61
+ const id = setInterval(() => {
62
+ const next = getStateRef.current();
63
+ setSnapshot((prev) => (shallowEqual(prev, next) ? prev : next));
64
+ }, intervalMs);
65
+
66
+ return () => clearInterval(id);
67
+ }, [active, intervalMs]);
68
+
69
+ return snapshot;
70
+ }
package/src/index.ts ADDED
@@ -0,0 +1,62 @@
1
+ // ============================================================================
2
+ // Components
3
+ // ============================================================================
4
+
5
+ export { DebugButton } from './DebugButton';
6
+ export type { DebugButtonProps } from './DebugButton';
7
+
8
+ export { DebugPanel } from './DebugPanel';
9
+ export type { DebugPanelProps, CustomDebugTab } from './DebugPanel';
10
+
11
+ // ============================================================================
12
+ // Built-in panels
13
+ // ============================================================================
14
+
15
+ export { LogsPanel } from './panels/LogsPanel';
16
+ export { AudioDebugPanel } from './panels/AudioDebugPanel';
17
+ export { StorePanel } from './panels/StorePanel';
18
+ export type { StorePanelProps } from './panels/StorePanel';
19
+
20
+ // ============================================================================
21
+ // Store
22
+ // ============================================================================
23
+
24
+ export { useDebugStore } from './store/debugStore';
25
+ export type { DebugTab, DebugStore } from './store/debugStore';
26
+
27
+ // ============================================================================
28
+ // Logger (own, no ui-core dependency)
29
+ // ============================================================================
30
+
31
+ export { createDebugLogger, debugLog, useDebugLogStore, useDebugFilteredLogs, useDebugLogCount, useDebugErrorCount } from './logger';
32
+ export type { LogEntry, LogLevel, LogFilter, Logger } from './logger';
33
+
34
+ // ============================================================================
35
+ // Hooks
36
+ // ============================================================================
37
+
38
+ export { useDebugShortcut } from './hooks/useDebugShortcut';
39
+
40
+ export { useAudioEventLog } from './hooks/useAudioEventLog';
41
+ export type { AudioLogEntry, UseAudioEventLogResult } from './hooks/useAudioEventLog';
42
+
43
+ export { useCustomEventLog } from './hooks/useCustomEventLog';
44
+ export type { CustomLogEntry } from './hooks/useCustomEventLog';
45
+
46
+ export { useStoreSnapshot } from './hooks/useStoreSnapshot';
47
+
48
+ // ============================================================================
49
+ // Bridges (optional integrations with other packages)
50
+ // ============================================================================
51
+
52
+ export { installMonitorBridge } from './bridges';
53
+
54
+ // ============================================================================
55
+ // Emitters (also available from '@djangocfg/debuger/emitters')
56
+ // ============================================================================
57
+
58
+ export { subscribeAudioEvents, emitAudioEvent, hasAudioListeners, clearAudioEventBuffer } from './emitters/audioEmitter';
59
+ export type { AudioDebugEvent, AudioEventKind } from './emitters/audioEmitter';
60
+
61
+ export { subscribeCustomEvents, emitDebugEvent, hasCustomListeners } from './emitters/customEmitter';
62
+ export type { CustomDebugEvent } from './emitters/customEmitter';
@@ -0,0 +1,3 @@
1
+ export { createDebugLogger, debugLog } from './logger';
2
+ export { useDebugLogStore, useDebugFilteredLogs, useDebugLogCount, useDebugErrorCount } from './logStore';
3
+ export type { LogEntry, LogLevel, LogFilter, LogStore, Logger } from './types';
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+
3
+ import { create } from 'zustand';
4
+ import type { LogStore, LogEntry, LogFilter } from './types';
5
+
6
+ // ============================================================================
7
+ // Constants
8
+ // ============================================================================
9
+
10
+ const MAX_LOGS = 1000;
11
+
12
+ const DEFAULT_FILTER: LogFilter = {
13
+ levels: ['debug', 'info', 'warn', 'error', 'success'],
14
+ };
15
+
16
+ // ============================================================================
17
+ // Helpers
18
+ // ============================================================================
19
+
20
+ let _counter = 0;
21
+
22
+ function generateId(): string {
23
+ return `log-${Date.now()}-${++_counter}`;
24
+ }
25
+
26
+ function matchesFilter(entry: LogEntry, filter: LogFilter): boolean {
27
+ if (!filter.levels.includes(entry.level)) return false;
28
+
29
+ if (filter.component) {
30
+ if (!entry.component.toLowerCase().includes(filter.component.toLowerCase())) return false;
31
+ }
32
+
33
+ if (filter.search) {
34
+ const s = filter.search.toLowerCase();
35
+ const inMsg = entry.message.toLowerCase().includes(s);
36
+ const inData = entry.data ? JSON.stringify(entry.data).toLowerCase().includes(s) : false;
37
+ if (!inMsg && !inData) return false;
38
+ }
39
+
40
+ return true;
41
+ }
42
+
43
+ // ============================================================================
44
+ // Store
45
+ // ============================================================================
46
+
47
+ export const useDebugLogStore = create<LogStore>((set, get) => ({
48
+ logs: [],
49
+ filter: DEFAULT_FILTER,
50
+
51
+ addLog: (entry) => {
52
+ const newEntry: LogEntry = {
53
+ ...entry,
54
+ id: generateId(),
55
+ timestamp: new Date(),
56
+ };
57
+ set((state) => {
58
+ const next = [...state.logs, newEntry];
59
+ return { logs: next.length > MAX_LOGS ? next.slice(-MAX_LOGS) : next };
60
+ });
61
+ },
62
+
63
+ clearLogs: () => set({ logs: [] }),
64
+
65
+ setFilter: (filter) =>
66
+ set((state) => ({ filter: { ...state.filter, ...filter } })),
67
+
68
+ getFilteredLogs: () => {
69
+ const { logs, filter } = get();
70
+ return logs.filter((e) => matchesFilter(e, filter));
71
+ },
72
+
73
+ exportLogs: () => JSON.stringify(get().logs, null, 2),
74
+ }));
75
+
76
+ // ============================================================================
77
+ // Selector hooks
78
+ // ============================================================================
79
+
80
+ export const useDebugFilteredLogs = () => {
81
+ const logs = useDebugLogStore((s) => s.logs);
82
+ const filter = useDebugLogStore((s) => s.filter);
83
+ return logs.filter((e) => matchesFilter(e, filter));
84
+ };
85
+
86
+ export const useDebugLogCount = () => useDebugLogStore((s) => s.logs.length);
87
+
88
+ export const useDebugErrorCount = () =>
89
+ useDebugLogStore((s) => s.logs.filter((l) => l.level === 'error').length);
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Debug Logger
3
+ *
4
+ * Standalone logger for @djangocfg/debuger — no ui-core dependency.
5
+ * Writes to useDebugLogStore (shown in LogsPanel) and to console in dev.
6
+ */
7
+
8
+ import { useDebugLogStore } from './logStore';
9
+ import type { Logger, LogLevel } from './types';
10
+
11
+ // ============================================================================
12
+ // Helpers
13
+ // ============================================================================
14
+
15
+ const isDev = process.env.NODE_ENV !== 'production';
16
+ const isBrowser = typeof window !== 'undefined';
17
+
18
+ function extractStack(data?: Record<string, unknown>): {
19
+ cleanData: Record<string, unknown> | undefined;
20
+ stack: string | undefined;
21
+ } {
22
+ if (!data) return { cleanData: undefined, stack: undefined };
23
+
24
+ const cleanData = { ...data };
25
+ let stack: string | undefined;
26
+
27
+ if (data.error instanceof Error) {
28
+ stack = data.error.stack;
29
+ cleanData.error = { name: data.error.name, message: data.error.message };
30
+ } else if (typeof data.error === 'object' && data.error !== null) {
31
+ const e = data.error as Record<string, unknown>;
32
+ if (typeof e.stack === 'string') stack = e.stack;
33
+ if (typeof e.message === 'string') cleanData.error = e.message;
34
+ }
35
+
36
+ return { cleanData, stack };
37
+ }
38
+
39
+ // ============================================================================
40
+ // createLogger
41
+ // ============================================================================
42
+
43
+ export function createDebugLogger(component: string): Logger {
44
+ const write = (level: LogLevel, message: string, data?: Record<string, unknown>) => {
45
+ const { cleanData, stack } = extractStack(data);
46
+
47
+ if (isBrowser) {
48
+ useDebugLogStore.getState().addLog({ level, component, message, data: cleanData, stack });
49
+ }
50
+
51
+ if (isDev) {
52
+ const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log';
53
+ const prefix = `[${component}]`;
54
+ if (cleanData) {
55
+ console[method](prefix, message, cleanData);
56
+ } else {
57
+ console[method](prefix, message);
58
+ }
59
+ }
60
+ };
61
+
62
+ return {
63
+ debug: (msg, data) => write('debug', msg, data),
64
+ info: (msg, data) => write('info', msg, data),
65
+ warn: (msg, data) => write('warn', msg, data),
66
+ error: (msg, data) => write('error', msg, data),
67
+ success: (msg, data) => write('success', msg, data),
68
+ };
69
+ }
70
+
71
+ // ============================================================================
72
+ // debugLog — one-shot helper (creates logger on each call, for non-hot paths)
73
+ // ============================================================================
74
+
75
+ export function debugLog(
76
+ component: string,
77
+ level: LogLevel,
78
+ message: string,
79
+ data?: Record<string, unknown>,
80
+ ): void {
81
+ const { cleanData, stack } = extractStack(data);
82
+
83
+ if (isBrowser) {
84
+ useDebugLogStore.getState().addLog({ level, component, message, data: cleanData, stack });
85
+ }
86
+
87
+ if (isDev) {
88
+ const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log';
89
+ if (cleanData) {
90
+ console[method](`[${component}]`, message, cleanData);
91
+ } else {
92
+ console[method](`[${component}]`, message);
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,39 @@
1
+ // ============================================================================
2
+ // Types
3
+ // ============================================================================
4
+
5
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success';
6
+
7
+ export interface LogEntry {
8
+ id: string;
9
+ timestamp: Date;
10
+ level: LogLevel;
11
+ component: string;
12
+ message: string;
13
+ data?: Record<string, unknown>;
14
+ stack?: string;
15
+ }
16
+
17
+ export interface LogFilter {
18
+ levels: LogLevel[];
19
+ component?: string;
20
+ search?: string;
21
+ }
22
+
23
+ export interface LogStore {
24
+ logs: LogEntry[];
25
+ filter: LogFilter;
26
+ addLog: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
27
+ clearLogs: () => void;
28
+ setFilter: (filter: Partial<LogFilter>) => void;
29
+ getFilteredLogs: () => LogEntry[];
30
+ exportLogs: () => string;
31
+ }
32
+
33
+ export interface Logger {
34
+ debug: (message: string, data?: Record<string, unknown>) => void;
35
+ info: (message: string, data?: Record<string, unknown>) => void;
36
+ warn: (message: string, data?: Record<string, unknown>) => void;
37
+ error: (message: string, data?: Record<string, unknown>) => void;
38
+ success: (message: string, data?: Record<string, unknown>) => void;
39
+ }
@@ -0,0 +1,157 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback, useState } from 'react';
4
+ import {
5
+ Button,
6
+ Badge,
7
+ ScrollArea,
8
+ } from '@djangocfg/ui-core/components';
9
+ import { cn } from '@djangocfg/ui-core/lib';
10
+ import { Trash2, Radio } from 'lucide-react';
11
+ import { useAudioEventLog } from '../hooks/useAudioEventLog';
12
+ import type { AudioEventKind } from '../emitters/audioEmitter';
13
+
14
+ // ============================================================================
15
+ // Types / constants
16
+ // ============================================================================
17
+
18
+ type DebugAudioMode = 'off' | 'on' | 'verbose';
19
+
20
+ const DEBUG_MODES: DebugAudioMode[] = ['off', 'on', 'verbose'];
21
+
22
+ const KIND_STYLES: Record<AudioEventKind, { label: string; className: string }> = {
23
+ play: { label: 'play', className: 'text-green-400' },
24
+ pause: { label: 'pause', className: 'text-yellow-400' },
25
+ seek: { label: 'seek', className: 'text-blue-400' },
26
+ ended: { label: 'ended', className: 'text-muted-foreground' },
27
+ sync: { label: 'sync', className: 'text-muted-foreground/50' },
28
+ load: { label: 'load', className: 'text-purple-400' },
29
+ error: { label: 'error', className: 'text-red-400' },
30
+ engine: { label: 'eng', className: 'text-cyan-400' },
31
+ custom: { label: 'custom', className: 'text-orange-400' },
32
+ };
33
+
34
+ // ============================================================================
35
+ // Helpers
36
+ // ============================================================================
37
+
38
+ function getDebugAudioMode(): DebugAudioMode {
39
+ try {
40
+ const v = localStorage.getItem('DEBUG_AUDIO');
41
+ if (v === 'verbose') return 'verbose';
42
+ if (v) return 'on';
43
+ } catch { /* ssr */ }
44
+ return 'off';
45
+ }
46
+
47
+ function formatTs(ts: number): string {
48
+ const d = new Date(ts);
49
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
50
+ }
51
+
52
+ // ============================================================================
53
+ // Component
54
+ // ============================================================================
55
+
56
+ interface AudioDebugPanelProps {
57
+ isActive: boolean;
58
+ }
59
+
60
+ export function AudioDebugPanel({ isActive }: AudioDebugPanelProps) {
61
+ const { events, clear, seekRate, syncIntervalMs, kindCounts } = useAudioEventLog(isActive);
62
+ const [debugMode, setDebugMode] = useState<DebugAudioMode>(getDebugAudioMode);
63
+
64
+ const handleSetDebugAudio = useCallback((mode: DebugAudioMode) => {
65
+ if (mode === 'off') {
66
+ localStorage.removeItem('DEBUG_AUDIO');
67
+ } else {
68
+ localStorage.setItem('DEBUG_AUDIO', mode === 'verbose' ? 'verbose' : '1');
69
+ }
70
+ setDebugMode(mode);
71
+ }, []);
72
+
73
+ const kindEntries = Object.entries(kindCounts) as [AudioEventKind, number][];
74
+ const activeKinds = kindEntries.filter(([, count]) => count > 0);
75
+ const reversedEvents = [...events].reverse();
76
+
77
+ return (
78
+ <div className="flex flex-col h-full">
79
+ {/* Metrics bar */}
80
+ <div className="flex flex-wrap items-center gap-2 border-b border-border px-3 py-2 text-xs">
81
+ <span className="text-muted-foreground">
82
+ Sync interval:{' '}
83
+ <span className="text-foreground font-mono">
84
+ {syncIntervalMs !== null ? `${syncIntervalMs.toFixed(1)}ms` : '—'}
85
+ </span>
86
+ </span>
87
+ <span className="text-muted-foreground">
88
+ Seeks/s: <span className="text-foreground font-mono">{seekRate}</span>
89
+ </span>
90
+ <span className="text-muted-foreground">
91
+ Total: <span className="text-foreground font-mono">{events.length}</span>
92
+ </span>
93
+
94
+ <div className="flex gap-1 flex-wrap ml-auto">
95
+ {activeKinds.map(([kind, count]) => (
96
+ <Badge key={kind} variant="outline" className={cn('text-[10px] px-1 py-0', KIND_STYLES[kind]?.className)}>
97
+ {KIND_STYLES[kind]?.label ?? kind} {count}
98
+ </Badge>
99
+ ))}
100
+ </div>
101
+
102
+ <Button variant="ghost" size="icon" className="h-6 w-6 ml-1" onClick={clear}>
103
+ <Trash2 className="h-3.5 w-3.5" />
104
+ </Button>
105
+ </div>
106
+
107
+ {/* DEBUG_AUDIO toggles */}
108
+ <div className="flex items-center gap-1.5 border-b border-border px-3 py-1.5">
109
+ <span className="text-[10px] text-muted-foreground mr-1">DEBUG_AUDIO:</span>
110
+ {DEBUG_MODES.map((mode) => (
111
+ <button
112
+ key={mode}
113
+ type="button"
114
+ className={cn(
115
+ 'text-[10px] px-2 py-0.5 rounded border transition-colors',
116
+ debugMode === mode
117
+ ? 'border-primary bg-primary/10 text-primary'
118
+ : 'border-border hover:bg-muted text-muted-foreground'
119
+ )}
120
+ onClick={() => handleSetDebugAudio(mode)}
121
+ >
122
+ {mode}
123
+ </button>
124
+ ))}
125
+ </div>
126
+
127
+ {/* Event log */}
128
+ <ScrollArea className="flex-1">
129
+ {reversedEvents.length === 0 ? (
130
+ <div className="flex flex-col items-center justify-center py-10 text-muted-foreground">
131
+ <Radio className="h-8 w-8 mb-2 opacity-40" />
132
+ <p className="text-sm">No audio events yet</p>
133
+ <p className="text-xs mt-1">
134
+ Wire <code className="text-xs">emitAudioEvent()</code> into your audio engine
135
+ </p>
136
+ </div>
137
+ ) : (
138
+ <div className="divide-y divide-border/30">
139
+ {reversedEvents.map((entry) => {
140
+ const style = KIND_STYLES[entry.kind] ?? { label: entry.kind, className: 'text-foreground' };
141
+ return (
142
+ <div key={entry.id} className="flex items-start gap-2 px-3 py-1 text-xs">
143
+ <span className="font-mono text-muted-foreground shrink-0 w-[88px]">{formatTs(entry.ts)}</span>
144
+ <span className={cn('shrink-0 w-12 font-medium', style.className)}>{style.label}</span>
145
+ {entry.trackId && (
146
+ <span className="shrink-0 font-mono text-muted-foreground">{entry.trackId.slice(0, 6)}</span>
147
+ )}
148
+ <span className="truncate text-foreground/80">{entry.msg}</span>
149
+ </div>
150
+ );
151
+ })}
152
+ </div>
153
+ )}
154
+ </ScrollArea>
155
+ </div>
156
+ );
157
+ }