diff --git a/docs/assets/image.png b/docs/assets/image.png deleted file mode 100644 index 7611621..0000000 Binary files a/docs/assets/image.png and /dev/null differ diff --git a/src/main/cdp/attach.ts b/src/main/cdp/attach.ts index b32a40f..92216b1 100644 --- a/src/main/cdp/attach.ts +++ b/src/main/cdp/attach.ts @@ -54,6 +54,7 @@ export function attachCdp(page: WebContents): () => void { dbg.sendCommand('Runtime.enable').catch((e) => console.error('[cdp] Runtime.enable', e)) dbg.sendCommand('Log.enable').catch((e) => console.error('[cdp] Log.enable', e)) dbg.sendCommand('Performance.enable').catch((e) => console.error('[cdp] Performance.enable', e)) + dbg.sendCommand('DOMStorage.enable').catch((e) => console.error('[cdp] DOMStorage.enable', e)) // Sondeos periódicos (REQ-022 / REQ-023 / REQ-024). memory.start() diff --git a/src/main/window.ts b/src/main/window.ts index cba0967..9a1c7ad 100644 --- a/src/main/window.ts +++ b/src/main/window.ts @@ -11,7 +11,9 @@ import { type ResponseBody, type TabsState, type BookmarksState, - type HistoryState + type HistoryState, + type StorageDetail, + type StorageKV } from '../shared/events' import { listBookmarks, @@ -419,6 +421,52 @@ function registerIpc(): void { } }) + // Detalle de storage del origen activo (cookies + items de local/session storage). + ipcMain.handle(IPC.getStorageDetail, async (): Promise => { + const empty: StorageDetail = { origin: '', cookies: [], local: [], session: [] } + const wc = activeTab()?.view.webContents + if (!wc) return empty + const url = wc.getURL() + let origin = '' + try { + origin = new URL(url).origin + } catch { + return empty + } + if (!/^https?:/.test(origin)) return { ...empty, origin } + const dbg = wc.debugger + + const getDom = async (isLocal: boolean): Promise => { + try { + const r = (await dbg.sendCommand('DOMStorage.getDOMStorageItems', { + storageId: { securityOrigin: origin, isLocalStorage: isLocal } + })) as { entries: [string, string][] } + return (r.entries ?? []).map(([key, value]) => ({ key, value })) + } catch { + return [] + } + } + + const cookies = await dbg + .sendCommand('Network.getCookies', { urls: [url] }) + .then((r) => + ((r as { cookies: any[] }).cookies ?? []).map((c) => ({ + name: c.name, + value: c.value, + domain: c.domain, + path: c.path, + size: c.size ?? c.name.length + String(c.value).length, + httpOnly: !!c.httpOnly, + secure: !!c.secure, + expires: c.expires ?? -1 + })) + ) + .catch(() => []) + + const [local, session] = await Promise.all([getDom(true), getDom(false)]) + return { origin, cookies, local, session } + }) + // Arrastre del overlay: acumula deltas de pantalla sobre la posición actual. ipcMain.on(IPC.overlayMove, (_e, dx: number, dy: number) => { const { width, height } = win.getContentBounds() diff --git a/src/preload/index.ts b/src/preload/index.ts index deac4a2..6304631 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -8,7 +8,8 @@ import { type ResponseBody, type TabsState, type BookmarksState, - type HistoryState + type HistoryState, + type StorageDetail } from '../shared/events' // ============================================================================ @@ -95,6 +96,9 @@ const api = { getResponseBody(requestId: string): Promise { return ipcRenderer.invoke(IPC.getResponseBody, requestId) }, + getStorageDetail(): Promise { + return ipcRenderer.invoke(IPC.getStorageDetail) + }, chromeExpand(open: boolean): void { ipcRenderer.send(IPC.chromeExpand, open) }, diff --git a/src/renderer/overlay/StoragePanel.tsx b/src/renderer/overlay/StoragePanel.tsx index 02dfecd..ad20b39 100644 --- a/src/renderer/overlay/StoragePanel.tsx +++ b/src/renderer/overlay/StoragePanel.tsx @@ -1,6 +1,9 @@ +import { useCallback, useEffect, useState, type ReactNode } from 'react' +import type { CookieInfo, StorageDetail, StorageKV } from '../../shared/events' import type { StorageView } from './useStorage' -// Panel Storage (REQ-024): uso/quota del origen + desglose por tipo de almacenamiento. +// Panel Storage (REQ-024): uso/quota + desglose por tipo + entradas reales +// (cookies, localStorage, sessionStorage) del origen activo. const LABELS: Record = { cookies: 'Cookies', localstorage: 'localStorage', @@ -17,6 +20,19 @@ const LABELS: Record = { export function StoragePanel({ st }: { st: StorageView }): JSX.Element { const { latest } = st + const [detail, setDetail] = useState(null) + const [loading, setLoading] = useState(false) + + const refresh = useCallback(async (): Promise => { + setLoading(true) + setDetail(await window.overrun.getStorageDetail()) + setLoading(false) + }, []) + + // Carga el detalle al montar y cuando cambia el origen. + useEffect(() => { + void refresh() + }, [refresh, latest?.origin]) if (!latest) { return
Sondeando storage…
@@ -31,6 +47,10 @@ export function StoragePanel({ st }: { st: StorageView }): JSX.Element {
{fmt(latest.usage)} / {fmt(latest.quota)} usados + +
@@ -39,7 +59,7 @@ export function StoragePanel({ st }: { st: StorageView }): JSX.Element {
{/* desglose por tipo */} -
+
POR TIPO
{latest.breakdown.length === 0 ? ( Sin datos almacenados en este origen. @@ -48,7 +68,7 @@ export function StoragePanel({ st }: { st: StorageView }): JSX.Element { const pct = latest.usage ? (b.bytes / latest.usage) * 100 : 0 return (
-
+
{LABELS[b.type] ?? b.type} {fmt(b.bytes)}
@@ -60,10 +80,68 @@ export function StoragePanel({ st }: { st: StorageView }): JSX.Element { }) )}
+ + {/* entradas reales */} +
+ {(detail?.cookies ?? []).map((c) => )} +
+
+ {(detail?.local ?? []).map((kv) => )} +
+
+ {(detail?.session ?? []).map((kv) => )} +
+
+ ) +} + +function Section({ title, count, children }: { title: string; count: number; children: ReactNode }): JSX.Element { + const [open, setOpen] = useState(false) + return ( +
+
setOpen((o) => !o)} + style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', cursor: 'pointer' }}> + + {title} + {count} +
+ {open && ( +
+ {count === 0 ? : children} +
+ )}
) } +function CookieRow({ c }: { c: CookieInfo }): JSX.Element { + return ( +
+
+ {c.name} + {c.secure && S} + {c.httpOnly && H} + + {c.domain} +
+
{c.value}
+
+ ) +} + +function KVRow({ kv }: { kv: StorageKV }): JSX.Element { + return ( +
+
{kv.key}
+
{kv.value}
+
+ ) +} + +function Flag({ children }: { children: ReactNode }): JSX.Element { + return {children} +} + function fmt(bytes: number): string { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB` diff --git a/src/shared/events.ts b/src/shared/events.ts index 1776c88..ff0a208 100644 --- a/src/shared/events.ts +++ b/src/shared/events.ts @@ -177,6 +177,30 @@ export interface StorageEvent extends OverrunEventBase { record: StorageRecord } +// Detalle on-demand del storage del origen activo (entradas reales, no solo tamaño). +export interface CookieInfo { + name: string + value: string + domain: string + path: string + size: number + httpOnly: boolean + secure: boolean + expires: number // -1 = sesión +} + +export interface StorageKV { + key: string + value: string +} + +export interface StorageDetail { + origin: string + cookies: CookieInfo[] + local: StorageKV[] + session: StorageKV[] +} + // --------------------------------------------------------------------------- // Unión de todos los eventos del bus. Otros dominios se suman aquí sin rediseño. // --------------------------------------------------------------------------- @@ -204,6 +228,8 @@ export const IPC = { overlayResize: 'overrun:overlay-resize', /** overlay → main (invoke): pide el body de una respuesta on-demand. */ getResponseBody: 'overrun:get-response-body', + /** overlay → main (invoke): detalle de storage del origen activo (cookies, items). */ + getStorageDetail: 'overrun:get-storage-detail', /** chrome → main: expandir/contraer la vista del chrome para que un popup * (dropdown de historial) pueda dibujarse sobre la página sin recortarse. */ chromeExpand: 'overrun:chrome-expand',