diff --git a/src/main/cdp/attach.ts b/src/main/cdp/attach.ts index f1766aa..b32a40f 100644 --- a/src/main/cdp/attach.ts +++ b/src/main/cdp/attach.ts @@ -3,6 +3,7 @@ import { handleCdpNetwork, resetNetwork } from '../events/network' import { handleCdpConsole } from '../events/console' import { createMemoryPoller } from '../events/memory' import { createPerformancePoller } from '../events/performance' +import { createStoragePoller } from '../events/storage' // ============================================================================ // Capa CDP (D-002) — DESACOPLADA de la base (REQ-062): si mañana se migra a @@ -38,11 +39,13 @@ export function attachCdp(page: WebContents): () => void { const memory = createMemoryPoller(dbg) const performance = createPerformancePoller(dbg) + const storage = createStoragePoller(dbg, page) const onDetach = (_event: unknown, reason: string): void => { console.warn('[cdp] debugger detached:', reason) memory.stop() performance.stop() + storage.stop() } dbg.on('detach', onDetach) @@ -52,9 +55,10 @@ export function attachCdp(page: WebContents): () => void { dbg.sendCommand('Log.enable').catch((e) => console.error('[cdp] Log.enable', e)) dbg.sendCommand('Performance.enable').catch((e) => console.error('[cdp] Performance.enable', e)) - // Sondeos periódicos (REQ-022 / REQ-023). + // Sondeos periódicos (REQ-022 / REQ-023 / REQ-024). memory.start() performance.start() + storage.start() // Al navegar a otra página, limpiamos el estado de red acumulado. const onNavigation = (_e: unknown, _url: string, isInPlace: boolean, isMainFrame: boolean): void => { @@ -65,6 +69,7 @@ export function attachCdp(page: WebContents): () => void { return () => { memory.stop() performance.stop() + storage.stop() dbg.off('message', onMessage) dbg.off('detach', onDetach) if (!page.isDestroyed()) page.off('did-start-navigation', onNavigation) diff --git a/src/main/events/storage.ts b/src/main/events/storage.ts new file mode 100644 index 0000000..6c4e07b --- /dev/null +++ b/src/main/events/storage.ts @@ -0,0 +1,59 @@ +import type { Debugger, WebContents } from 'electron' +import { bus } from './bus' +import type { StorageBreakdown } from '../../shared/events' + +// ============================================================================ +// Sondeo del dominio Storage (REQ-024). `Storage.getUsageAndQuota` da el uso por +// tipo de almacenamiento del origen actual (cookies, localStorage, IndexedDB, +// cache, service workers…). Es un COMANDO → el main lo sondea y emite snapshots. +// ============================================================================ + +function originOf(url: string): string | null { + try { + const o = new URL(url).origin + return /^https?:/.test(o) ? o : null + } catch { + return null + } +} + +export function createStoragePoller(dbg: Debugger, page: WebContents, intervalMs = 2000): { start: () => void; stop: () => void } { + let timer: ReturnType | null = null + + const tick = async (): Promise => { + if (!dbg.isAttached()) return + const origin = originOf(page.getURL()) + if (!origin) return // about:blank u orígenes sin storage + try { + const res = (await dbg.sendCommand('Storage.getUsageAndQuota', { origin })) as { + usage: number + quota: number + usageBreakdown: { storageType: string; usage: number }[] + } + const breakdown: StorageBreakdown[] = (res.usageBreakdown ?? []) + .filter((b) => b.usage > 0) + .map((b) => ({ type: b.storageType, bytes: b.usage })) + .sort((a, b) => b.bytes - a.bytes) + bus.emitEvent({ + domain: 'storage', + record: { origin, usage: res.usage, quota: res.quota, breakdown } + }) + } catch { + // debugger despegándose / navegando: se reintenta al próximo tick. + } + } + + return { + start(): void { + if (timer) return + timer = setInterval(tick, intervalMs) + void tick() + }, + stop(): void { + if (timer) { + clearInterval(timer) + timer = null + } + } + } +} diff --git a/src/renderer/overlay/Overlay.tsx b/src/renderer/overlay/Overlay.tsx index 0ac3a15..07ce9d4 100644 --- a/src/renderer/overlay/Overlay.tsx +++ b/src/renderer/overlay/Overlay.tsx @@ -3,14 +3,16 @@ import { NetworkPanel } from './NetworkPanel' import { ConsolePanel } from './ConsolePanel' import { MemoryPanel } from './MemoryPanel' import { PerformancePanel } from './PerformancePanel' +import { StoragePanel } from './StoragePanel' import { useNetwork } from './useNetwork' import { useConsole } from './useConsole' import { useMemory } from './useMemory' import { usePerformance } from './usePerformance' +import { useStorage } from './useStorage' type Tab = 'Network' | 'Console' | 'Memory' | 'CPU' | 'Storage' const TABS: Tab[] = ['Network', 'Console', 'Memory', 'CPU', 'Storage'] -const ENABLED: Tab[] = ['Network', 'Console', 'Memory', 'CPU'] // v1 en progreso +const ENABLED: Tab[] = ['Network', 'Console', 'Memory', 'CPU', 'Storage'] // v1 // Gesto de arrastre genérico: captura el puntero y manda deltas de pantalla al // callback (mover o redimensionar). `moved` distingue drag de click (para el pill). @@ -66,6 +68,7 @@ export function Overlay(): JSX.Element { const con = useConsole() const mem = useMemory() const perf = usePerformance() + const st = useStorage() useEffect(() => window.overrun.onOverlayState(setCollapsed), []) @@ -107,7 +110,7 @@ export function Overlay(): JSX.Element { })} - {tab === 'Network' ? : tab === 'Console' ? : tab === 'Memory' ? : tab === 'CPU' ? : } + {tab === 'Network' ? : tab === 'Console' ? : tab === 'Memory' ? : tab === 'CPU' ? : tab === 'Storage' ? : } {/* footer */}
diff --git a/src/renderer/overlay/StoragePanel.tsx b/src/renderer/overlay/StoragePanel.tsx new file mode 100644 index 0000000..02dfecd --- /dev/null +++ b/src/renderer/overlay/StoragePanel.tsx @@ -0,0 +1,72 @@ +import type { StorageView } from './useStorage' + +// Panel Storage (REQ-024): uso/quota del origen + desglose por tipo de almacenamiento. +const LABELS: Record = { + cookies: 'Cookies', + localstorage: 'localStorage', + indexeddb: 'IndexedDB', + cache_storage: 'Cache Storage', + service_workers: 'Service Workers', + database: 'Database', + websql: 'WebSQL', + filesystem: 'File System', + shared_storage: 'Shared Storage', + interest_groups: 'Interest Groups', + other: 'Otro' +} + +export function StoragePanel({ st }: { st: StorageView }): JSX.Element { + const { latest } = st + + if (!latest) { + return
Sondeando storage…
+ } + + const usedPct = latest.quota ? (latest.usage / latest.quota) * 100 : 0 + + return ( +
+ {/* total usage / quota */} +
+
+ {fmt(latest.usage)} + / {fmt(latest.quota)} usados +
+
+
+
+
{latest.origin}
+
+ + {/* desglose por tipo */} +
+
POR TIPO
+ {latest.breakdown.length === 0 ? ( + Sin datos almacenados en este origen. + ) : ( + latest.breakdown.map((b) => { + const pct = latest.usage ? (b.bytes / latest.usage) * 100 : 0 + return ( +
+
+ {LABELS[b.type] ?? b.type} + {fmt(b.bytes)} +
+
+
+
+
+ ) + }) + )} +
+
+ ) +} + +function fmt(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB` + if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB` + return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB` +} diff --git a/src/renderer/overlay/useStorage.ts b/src/renderer/overlay/useStorage.ts new file mode 100644 index 0000000..38e2758 --- /dev/null +++ b/src/renderer/overlay/useStorage.ts @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react' +import type { OverrunEvent, StorageRecord } from '../../shared/events' + +export interface StorageView { + latest: StorageRecord | null +} + +/** Último snapshot de storage (REQ-024). Llega ~1 cada 2s. */ +export function useStorage(): StorageView { + const [view, setView] = useState({ latest: null }) + useEffect( + () => + window.overrun.onEvent((evt: OverrunEvent) => { + if (evt.domain === 'storage') setView({ latest: evt.record }) + }), + [] + ) + return view +} diff --git a/src/shared/events.ts b/src/shared/events.ts index 5ba97e4..b3d6d0f 100644 --- a/src/shared/events.ts +++ b/src/shared/events.ts @@ -152,12 +152,37 @@ export interface PerformanceEvent extends OverrunEventBase { record: PerformanceRecord } +// --------------------------------------------------------------------------- +// Dominio: storage (v1 — REQ-024) +// +// Uso por tipo de almacenamiento del origen de la página. Se obtiene por COMANDO +// (`Storage.getUsageAndQuota`) — el main lo sondea y emite snapshots. +// --------------------------------------------------------------------------- + +export interface StorageBreakdown { + /** cookies, indexeddb, localstorage, cache_storage, service_workers… */ + type: string + bytes: number +} + +export interface StorageRecord { + origin: string + usage: number // bytes usados en total + quota: number // bytes disponibles + breakdown: StorageBreakdown[] +} + +export interface StorageEvent extends OverrunEventBase { + domain: 'storage' + record: StorageRecord +} + // --------------------------------------------------------------------------- // Unión de todos los eventos del bus. Otros dominios se suman aquí sin rediseño. // --------------------------------------------------------------------------- -export type OverrunEvent = NetworkEvent | ConsoleEvent | MemoryEvent | PerformanceEvent -// | StorageEvent | SecurityEvent (próximas fases) +export type OverrunEvent = NetworkEvent | ConsoleEvent | MemoryEvent | PerformanceEvent | StorageEvent +// | SecurityEvent (v2) /** Canales IPC — un único punto para no tipear strings sueltos. */ export const IPC = {