@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,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,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
|
+
}
|