Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/main/cdp/attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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 => {
Expand All @@ -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)
Expand Down
59 changes: 59 additions & 0 deletions src/main/events/storage.ts
Original file line number Diff line number Diff line change
@@ -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<typeof setInterval> | null = null

const tick = async (): Promise<void> => {
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
}
}
}
}
7 changes: 5 additions & 2 deletions src/renderer/overlay/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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), [])

Expand Down Expand Up @@ -107,7 +110,7 @@ export function Overlay(): JSX.Element {
})}
</div>

{tab === 'Network' ? <NetworkPanel net={net} /> : tab === 'Console' ? <ConsolePanel con={con} /> : tab === 'Memory' ? <MemoryPanel mem={mem} /> : tab === 'CPU' ? <PerformancePanel perf={perf} /> : <Placeholder tab={tab} />}
{tab === 'Network' ? <NetworkPanel net={net} /> : tab === 'Console' ? <ConsolePanel con={con} /> : tab === 'Memory' ? <MemoryPanel mem={mem} /> : tab === 'CPU' ? <PerformancePanel perf={perf} /> : tab === 'Storage' ? <StoragePanel st={st} /> : <Placeholder tab={tab} />}

{/* footer */}
<div style={{ height: 34, display: 'flex', alignItems: 'center', gap: 8, padding: '0 14px', borderTop: '1px solid #22262e', fontFamily: 'var(--font-mono)' }}>
Expand Down
72 changes: 72 additions & 0 deletions src/renderer/overlay/StoragePanel.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
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 <div style={{ padding: 20, fontSize: 11, color: 'var(--mute)', fontFamily: 'var(--font-mono)' }}>Sondeando storage…</div>
}

const usedPct = latest.quota ? (latest.usage / latest.quota) * 100 : 0

return (
<div style={{ display: 'flex', flexDirection: 'column', minHeight: 0, flex: 1, fontFamily: 'var(--font-mono)', overflowY: 'auto' }}>
{/* total usage / quota */}
<div style={{ padding: '16px 14px 12px', borderBottom: '1px solid #22262e' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
<span style={{ fontSize: 24, color: 'var(--text)', fontWeight: 600 }}>{fmt(latest.usage)}</span>
<span style={{ fontSize: 12, color: 'var(--dim)' }}>/ {fmt(latest.quota)} usados</span>
</div>
<div style={{ marginTop: 10, height: 6, borderRadius: 3, background: '#20242c', overflow: 'hidden' }}>
<div style={{ width: `${Math.max(0.5, Math.min(100, usedPct))}%`, height: '100%', background: 'var(--cyan)', borderRadius: 3 }} />
</div>
<div style={{ marginTop: 8, fontSize: 10, color: 'var(--mute)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{latest.origin}</div>
</div>

{/* desglose por tipo */}
<div style={{ padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ fontSize: 9, letterSpacing: '0.14em', color: 'var(--mute)' }}>POR TIPO</div>
{latest.breakdown.length === 0 ? (
<span style={{ fontSize: 11, color: 'var(--mute)' }}>Sin datos almacenados en este origen.</span>
) : (
latest.breakdown.map((b) => {
const pct = latest.usage ? (b.bytes / latest.usage) * 100 : 0
return (
<div key={b.type}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 5 }}>
<span style={{ fontSize: 11, color: '#cfd3d9' }}>{LABELS[b.type] ?? b.type}</span>
<span style={{ fontSize: 10, color: 'var(--dim)' }}>{fmt(b.bytes)}</span>
</div>
<div style={{ height: 4, borderRadius: 2, background: '#20242c', overflow: 'hidden' }}>
<div style={{ width: `${Math.max(2, pct)}%`, height: '100%', borderRadius: 2, background: 'var(--cyan)' }} />
</div>
</div>
)
})
)}
</div>
</div>
)
}

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`
}
19 changes: 19 additions & 0 deletions src/renderer/overlay/useStorage.ts
Original file line number Diff line number Diff line change
@@ -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<StorageView>({ latest: null })
useEffect(
() =>
window.overrun.onEvent((evt: OverrunEvent) => {
if (evt.domain === 'storage') setView({ latest: evt.record })
}),
[]
)
return view
}
29 changes: 27 additions & 2 deletions src/shared/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading