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
Binary file removed docs/assets/image.png
Binary file not shown.
1 change: 1 addition & 0 deletions src/main/cdp/attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
50 changes: 49 additions & 1 deletion src/main/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
type ResponseBody,
type TabsState,
type BookmarksState,
type HistoryState
type HistoryState,
type StorageDetail,
type StorageKV
} from '../shared/events'
import {
listBookmarks,
Expand Down Expand Up @@ -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<StorageDetail> => {
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<StorageKV[]> => {
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()
Expand Down
6 changes: 5 additions & 1 deletion src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
type ResponseBody,
type TabsState,
type BookmarksState,
type HistoryState
type HistoryState,
type StorageDetail
} from '../shared/events'

// ============================================================================
Expand Down Expand Up @@ -95,6 +96,9 @@ const api = {
getResponseBody(requestId: string): Promise<ResponseBody> {
return ipcRenderer.invoke(IPC.getResponseBody, requestId)
},
getStorageDetail(): Promise<StorageDetail> {
return ipcRenderer.invoke(IPC.getStorageDetail)
},
chromeExpand(open: boolean): void {
ipcRenderer.send(IPC.chromeExpand, open)
},
Expand Down
84 changes: 81 additions & 3 deletions src/renderer/overlay/StoragePanel.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
cookies: 'Cookies',
localstorage: 'localStorage',
Expand All @@ -17,6 +20,19 @@ const LABELS: Record<string, string> = {

export function StoragePanel({ st }: { st: StorageView }): JSX.Element {
const { latest } = st
const [detail, setDetail] = useState<StorageDetail | null>(null)
const [loading, setLoading] = useState(false)

const refresh = useCallback(async (): Promise<void> => {
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 <div style={{ padding: 20, fontSize: 11, color: 'var(--mute)', fontFamily: 'var(--font-mono)' }}>Sondeando storage…</div>
Expand All @@ -31,6 +47,10 @@ export function StoragePanel({ st }: { st: StorageView }): JSX.Element {
<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>
<span style={{ flex: 1 }} />
<button onClick={refresh} title="refrescar" style={{ background: 'transparent', border: 'none', color: loading ? 'var(--cyan)' : 'var(--mute)', cursor: 'pointer', display: 'flex' }}>
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M13 3 v3 h-3 M13 6 A5.5 5.5 0 1 0 13.5 10" stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
</button>
</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 }} />
Expand All @@ -39,7 +59,7 @@ export function StoragePanel({ st }: { st: StorageView }): JSX.Element {
</div>

{/* desglose por tipo */}
<div style={{ padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 10, borderBottom: '1px solid #22262e' }}>
<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>
Expand All @@ -48,7 +68,7 @@ export function StoragePanel({ st }: { st: StorageView }): JSX.Element {
const pct = latest.usage ? (b.bytes / latest.usage) * 100 : 0
return (
<div key={b.type}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 5 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontSize: 11, color: '#cfd3d9' }}>{LABELS[b.type] ?? b.type}</span>
<span style={{ fontSize: 10, color: 'var(--dim)' }}>{fmt(b.bytes)}</span>
</div>
Expand All @@ -60,10 +80,68 @@ export function StoragePanel({ st }: { st: StorageView }): JSX.Element {
})
)}
</div>

{/* entradas reales */}
<Section title="COOKIES" count={detail?.cookies.length ?? 0}>
{(detail?.cookies ?? []).map((c) => <CookieRow key={`${c.name}@${c.domain}${c.path}`} c={c} />)}
</Section>
<Section title="LOCALSTORAGE" count={detail?.local.length ?? 0}>
{(detail?.local ?? []).map((kv) => <KVRow key={kv.key} kv={kv} />)}
</Section>
<Section title="SESSIONSTORAGE" count={detail?.session.length ?? 0}>
{(detail?.session ?? []).map((kv) => <KVRow key={kv.key} kv={kv} />)}
</Section>
</div>
)
}

function Section({ title, count, children }: { title: string; count: number; children: ReactNode }): JSX.Element {
const [open, setOpen] = useState(false)
return (
<div style={{ borderBottom: '1px solid #1c2027' }}>
<div onClick={() => setOpen((o) => !o)}
style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', cursor: 'pointer' }}>
<svg width="10" height="10" viewBox="0 0 12 12" style={{ color: 'var(--mute)', transform: open ? 'rotate(90deg)' : 'none', transition: 'transform 0.1s' }}><path d="M4 3 L8 6 L4 9" stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
<span style={{ fontSize: 9.5, letterSpacing: '0.14em', color: 'var(--mute)' }}>{title}</span>
<span style={{ fontSize: 10, color: count ? 'var(--cyan-bright)' : 'var(--mute)' }}>{count}</span>
</div>
{open && (
<div style={{ padding: '0 14px 10px' }}>
{count === 0 ? <span style={{ fontSize: 10.5, color: 'var(--mute)' }}>—</span> : children}
</div>
)}
</div>
)
}

function CookieRow({ c }: { c: CookieInfo }): JSX.Element {
return (
<div style={{ padding: '5px 0', borderBottom: '1px solid #16181d' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 11, color: '#cfd3d9', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.name}</span>
{c.secure && <Flag>S</Flag>}
{c.httpOnly && <Flag>H</Flag>}
<span style={{ flex: 1 }} />
<span style={{ fontSize: 9, color: 'var(--mute)' }}>{c.domain}</span>
</div>
<div style={{ fontSize: 10, color: 'var(--dim)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', userSelect: 'text' }}>{c.value}</div>
</div>
)
}

function KVRow({ kv }: { kv: StorageKV }): JSX.Element {
return (
<div style={{ padding: '5px 0', borderBottom: '1px solid #16181d' }}>
<div style={{ fontSize: 11, color: 'var(--cyan-bright)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{kv.key}</div>
<div style={{ fontSize: 10, color: 'var(--dim)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', userSelect: 'text' }}>{kv.value}</div>
</div>
)
}

function Flag({ children }: { children: ReactNode }): JSX.Element {
return <span style={{ fontSize: 8, color: 'var(--mute)', border: '1px solid var(--line)', borderRadius: 3, padding: '0 3px', flexShrink: 0 }}>{children}</span>
}

function fmt(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`
Expand Down
26 changes: 26 additions & 0 deletions src/shared/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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',
Expand Down
Loading