From 17a7f1b5fb6c0eb5eabd22e0dfd50ce763d56686 Mon Sep 17 00:00:00 2001 From: ElVatoEste Date: Thu, 20 Aug 2026 18:53:51 -0600 Subject: [PATCH 1/2] feat(storage): show real cookies and local/session storage entries - Nuevo detalle on-demand (getStorageDetail): Network.getCookies + DOMStorage.getDOMStorageItems del origen activo. - DOMStorage.enable en attach. - StoragePanel: secciones colapsables Cookies / localStorage / sessionStorage con entradas reales (nombre/clave + valor, flags secure/httpOnly), boton de refrescar. Se mantiene el uso/quota + desglose por tipo. --- src/main/cdp/attach.ts | 1 + src/main/window.ts | 50 +++++++++++++++- src/preload/index.ts | 6 +- src/renderer/overlay/StoragePanel.tsx | 84 ++++++++++++++++++++++++++- src/shared/events.ts | 26 +++++++++ 5 files changed, 162 insertions(+), 5 deletions(-) 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', From dc527b2011eeac209e82fe2627a9790bdcb8c2aa Mon Sep 17 00:00:00 2001 From: ElVatoEste Date: Thu, 20 Aug 2026 18:53:51 -0600 Subject: [PATCH 2/2] docs: remove annotated screenshot --- docs/assets/image.png | Bin 19726 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/assets/image.png diff --git a/docs/assets/image.png b/docs/assets/image.png deleted file mode 100644 index 7611621eb7f00eaebeee1ebf4c512cf4ad0e94ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19726 zcmbTdby!s07x+tqbhn@&At2q2f^>)|Al)F{F*MR8Ae~Byba%thAR#qF$4Cy{!`$Qh zz4y7#@BVq$^8ja`IqbdH+H0-7_t~F`*3wWUz@^1SK|vuxDxg~zCzogb$y7+ZM&i>P( zEh@_J&%?vRlh4E|!6Wj4U8z4p(SWC4a&z-jQY4k&0EhGai zti|IL&9`+4xIga?@$&vmhe%bB1tNLs})8(zH92{&ht)3 z&vu-W1hr>jDzT>ri=2|e<+M-CqA4osTj5!X@(L!3zrW$E)DdWTlvCH(*mp~)LBjhYw^dan?PcQmH_Rc6EkP{ry*!5WbXF#&%QO~2`k2_ph|nmVtPye? zX=zKV61sYM`GNS+cpaDBm%OI<^rCO^Mc>kiCW`YW$hyR-xU9zAf`=yUz-tHVd$qs_ zdHx$A-b`{W;v|mRPwv2_;3qlx0o7GZinyn)nzrL-%>Lxr&7Jn)mbFJ{cID#G%ae7dZ=tRN^CPwJ_ICS%O^#k}1`dMl`uu;f~} ztBJh)er^p|eWcan9iCu}QtIu^4e8voFL*Ewx_a*Hpx`GdVmL??2svK}Q6dg%TR9b6jd(?Ku=TK**)B)|NYZC>Li`f5LY%WoU&|t7dd^`ipiH?? zL2y9PYq_pep#1MP5V8bx3Z{hk!ju#wWx}h8IxfdKbV0NdCtY1ldxX-(Pm$|ArnDJh zIQYC9CIsbsl;o6Ovz>RejR}`pmo%6uV;l-;Vh;O_a}~L$71^kRXc=RplsXHOfM|I7 z%f2X7j5nhI>p5VDZl+n12Xr;P*K|qldrbce&!n+kv7cJTvc0E^Vk>;Fdx*7ov&X*} z^qpY3aa$#&XRFG|g*bA&NxfZ%!xP1LzZPIo#sptf;BD)lL$N7iVhqu7jze*dXMk+8 z`c6wu{?{8%42p*bblXMt&~ZGQY9-m;UYQrm$Mfl@)2C>0-)zbF&!KojpxX zIGG4!H+lKbvMyo7b@lvJwz8-N&(>7DK4JlRGftn0DZTQu941r^RO6f=A(^3WT}5Qy z*V-Tev|#J+)!6pUGs)e~rbW8qiFL|SF@uxHf5Sz)&1l-UFqIF@Q3Y3UGKs)Qy8<6? zurvOva4EdN-1H=gpfri|dD5{$+@Kw1Cqmb71Ljd3ZX z08w&0A>vKS&$m(Q`f+xCE~pKd*62?dZUpE31eQHnkQA8D7@a z(hBa*?^b=Py05s@2HIAq`KK-5^7a{;z?du$|EfiIda#4~2RP<>nXDyOen{DUc*xIJ zF)2+GLMo^x6 zcXp*nyfpRQrv+`_qJVSEc1_Vznp9~X0bg{JAh;4wWw(7>*eqONZfctQIG>G4TWEu1 zVq{d(S&fpUj9k^0d1f|Hm=*L&^2e=}RCmeTAHQfNgeNcunwXrNeqVeflQB6ddm|UV z$;94(N$O9Jis>DKi7yv!z_3+aEi-A9#4Y8_-yM`b5@vJWsLCNr_1F<06u=Zx)J^I> zCE4to3EFkN4-4`fMb8+bQjDAdfBnRfNd&nti{SH}``neeRP4a}gEz4Oosrl1&Ox|^ zfK;-t9ozbM@i^IY_ywx8|DLinvbud;dG9=-%JEHpDg9pYrMJ4b%VU6@zf_|@VnzMV z`289Cb@S>&I$G*voZn1cA{>Y!$9=8*Rs~SWU+-isYw&Ou}_mZEof}5n_ zK;(CZkK^an)W?+Fv%bFKaf6m`zC06F<6%a2^y{Cvy#5M;C1)bR315P(2fPmSN(^(S zJD58eGbiw2*Oyw1Ow}drx(Y<5i$!*EnS$|2Vav4coO3PLTRbh$)r2j z)j;@oUi;U`K)Z2Z%B15UC=-=n-q8 z%PZ#4W3#5(f><>#o`&@J_SlgFheG^u$2)$%Z?zTW<3ZdUw^H8f%d(%JnD~%Q#>E^N zy{1&}>A0zRQ0{R3RO0vI_dNx!9tNA{KU^fd6^M>j zuampTX*V9(-u$vWctl4dPi<#gfvmEfrEVSZ;e|LRepT#eb)Y>KxbKa7J+*{6Jzm<} zQAHBZ*@lL?{2bfVLnjMauYBT@zVs*IR0+_w=4KD8D& z!HKgCQ5>V)=L?4Z-V|b*>V+-uGWgov@GnpA=9A;cuRL)R&ie5Z>`;&!m2=2t2CT@% zFbu5fW0>vp9-gQ@-7LwI^s7tK8EsCz`U3HXNP_p??&QH*RqfLmaQ%Ix^oY}aTlto`5PO&r#g(oUXu!@ejs_H}a~%R(YbmnTLX1_g>w62e^0z z=fPEmCK-LpuzbtY{58vS@ShIknVPi}l26ee;<6UjNP<&l_YO)qOPm0=9Fa+QZEGM7 zk~VC77QR(RV_6c|-@L<9qFtZ0Hd;ABMd|MI}des2>3J-@lh!m)M3{@$IFxRYiF-dr7n_S-*oE)1K!f|SvTy`?+OzWjt> zaMU75aCCFbCKMqu8(CG$diYGLT0*P3z}%hHe{C*2tg-&PLy_ zsxVc*vjKOmi`BSZ4D)eY+qk*Fy)5QGREdc+*~=})qthUioIfpgcQ*ay%bP|xzu@A< z2*RY02S#}mo8q(FD*AOwtJzauW=28lKV~N9`R|()lSIV9{12hruNOBZnUk^I993@yQEgX(I=@@6`%O z?dX>BaZ%MRKL5qFcolO9u8iL(-Ok;HtM9VWl7hxk=_CE4CW8Eu^@Hv}n0O8IHFFbO zK}XC9hPXfUr=rzp14W3`@|2o~pH0Gj!;0;RlC1nSq)ZbIHMPe3!P}&Ad_*SmQJ95u z>hEv=nz9NyzOk{`6@n9US|2^55J(aM9eXTzo?d&GG@3AAuLWeJzO3?%q@1)FQpRch zF41pPuW}e*M}7KKg72YN3S3bRoqFi*?wX{EbbsQo8WuSuGi-FNxwUaT@kDK{A2cSz z+A95+L_*T4GB;9yChHo8wJj-@6dM$j_>lL39`94R%EAX;#isuGKOOlQ)+oO82wlvB zmne?M%A1jJqh8wW-~ebaroB?jt(2nElJxH6KXREB(=;$hJ1_ZQp*J67C#TBwc89fQv9s$7i1V z{js<`U2*Z9J2Q93gXdg1^8W0^0f_-YFW*P{ws9c^V0FP!w&p7p*I)B<)ipi}Cy1&H z4W;wLGfA96*&O$faR>0$z(qrYrGkVDWZ^l;KXr&vutPC&v|;F}6piq~AKcY^^(~W? zzRR3Vh$D^5hI!IqrN&99wfrZOB zsL1VfLj1+g4}+*$&Z9ajVO+?41493s3O;n7w8X2RF~$10J&hk&C*%7@^f%XuFP_#p zVt0KSY~V*0zvX`@PF$*OZZMqb4RG+^lN&|K&((c1tcWNYUVF7&=yqMv0yyNQtX8S5suOS_?C|Z9mG;GLcCoJAu!IPs~m>v>`x! zP(OC^yc+47su|dVvvG9e+5XnWi>f@$X`O8=`Z*I*OesNrKpj#rtsDcD9TI!qaeW|QPPHkXb|+@Z#*lmzJ1_ch<_@)9p6v~%deH&g_KoSfC% z#vpElNee=PtgWKwk80LxMsY(U1cAL6nAqtF9H=c;_){9~>7sH>zt>@$$ zr_tjXGo_YNSP*wQ(rn)JcOpin@KWcxo80dD+b_{AAZvcRsLKBATLhtSKxeAD34Ei} zVIBJX{_$f|0Nk$eFMdW=rs9NI6V6LxqYIDY5DD#Pb!--;6V{ti)EaQTkvOKr*_njhE7_P7l;3QVATDm8pR|Uk!yyq_ms_u%>xDFew{e@=f8#^!r;&*c<1L zHnff{-N4m8;rpct(d`n$@9K2bAepWWDEyNQXwTd(IHa?Lf}48n4)r=S>@+)vW8x!+ zqiA~s%0*5$t5x#t2?+u69M;Kp7F|Aq7n+WSv66Z zh-Vqu(Eg~gS5-!0rV9~o?4}<&Y;6jsc?#*FHuzZHTeVjYOX%P0Mkh()ESbcnKzZmE z3-+D3zX@_iGImwD8S8EsM?HAbLGJL%6s4f6(;xOrHTivi9SB#44nFM-ypBvZkhiS6 zFUGi&{BQ+Mw_1chu77Q{VYo(mMPwUTe@sW7ECjKles4JV@HmHd`tgk)bT$)AbyZ`& zn!nU|O$^f}N^0=pu9p`55|j<1-adXE&EBG{e(QuSuV5 zKb&B;3kQx@#e+oOqgb*)u4Sx|L_2EjH;?MZIrYgCHfqpiM~oEf&e6?c&I~?B{9K;v zV~pMj9C6Y7@E}A+5mI#niaatq&(Aj-T*VJvUOAW>sd51IWUjdTy7p!}RQ*MSUv4(- zy6i?i&8cV0h1@gS*n%CUa>5keT`put9pMi+*N#a+ z5AgB(^Pv}rwZJY=5VtduGpt~kHRsk=814*xv)@};VRFH@wu4OuQdL+;KTkJ#_RnU+`-aarq!WyMyZCLS-)RcA^D4}cl zuI}mecKskOUq9;ZfHPI?2}kOaMbZB9JgcD!4SXLTwAuW?q*P>ZA`opCrKS9cJxhL6 z`(=jvN1Zf({NH(z5>37SXR0LxIioQIKrhV1X0fX_Oug7$r9M~PA3g<1^m4VWOO2TUII@ZrZR>XXFO@!ICz)dtCD zVTD(SJ)R0n9$v2tsfJy4MyVs)`}m3sp*SZV2be?Qmz8Dr{hMxYWeyCS9ja!(=BwtTW1;}PTuTU1d`Bl( zmq0UMNEnmpV!mq;9qyTY36--*Hdsw77c+{E{W8aR<#Kw*POB*HIPx;*MaH66)zjb1 z_cCBF9rd(akdk&NYTioA~MjIe&vs$-PPN+|m5S{veh!U6G?5VMe zY`NvPUaWLvj{H*)Dyv#fb_~(Mq!o>wbIqhZkEAL|A-|k>+OqV?!fK>E3c{b zC0C#K$$);;{zUR0cWQ~6c1Mo>Au{UUTgD|4X!8gN>4-j|e@EST8@+__N!} z-CIWxegU#a#VJ5S*N0HJb)w7>3z2s9N-YO^&)^;*HqS&YYe=56(%m$^jNy?D?Jd-q_3Q4L@q`x2bD_E#SF1(H#{JPlbEmz;+EoBYnNxzv ze)n#DKZ_Mnq=waN+b=LXDsIv}dV5y*4eHew>IBDLK5c>LXIMON+U>@-1Brggy+^Ny z5x#WgXqWls?; zzI`gcDwH=Acgui?Tr#1rQHMlz#10M7osaQjd-JjS!Ur6@7_maPqE5Vz&gNK_M(pF1 zoqP=@Hw@o4@r@{KttJXs&)}HH)8sWcYKRiw9w1_euOu}`u%?pHo0`q~1*{$`x zqh(B>`B+7!dGR)SN8{W3vHU!Z?2r~M0m@q@;Mgmx5Aqs=-7bOA2KbJ zo4>U(yf%n;Xb@K3(;W(bkH3n94;EycHmQ^Onhqpp2M+D`)xtaMcA3HXFssW*O{3w^ zbk0^OnS@m^6~E|J?R!W=XEY(O7h2z-(%-t#lOk8f`05*2xRHrcMz1=CA)`XY-G!f2 zsC%;W$awpg0Ct_Ebbn^U-{N^?zV}zw`>y*$nogtxtY;CycI9O|b5vG$^l;|vIJD&Q zP!SG3d1ux5oQZr~gr(&+_sZ*3KpN(j3;Vl~Vkp`X()`$#nWE4%mjX#Un=k1%6!rYt zUm8CJyz5=ozO7yu9OO<6sDV>ar8}kfUhKiX5OyW@h@`#Qu!2VPXfZP4d!Fk|F=t+t zgOZ?+1G4v2)uPtW8$5$RYuc;eS_8UAJG$4(@>Ff|>j*#GKkM;C-YHnb(2(!3oYSW! zcc|^NDB~V2*B_uV=h*y5e8Iy8iu}JBT%xl*>pSE5Z{cR8n>!iJY$L)L0&Tc!!=II} zq6sV!dV##`2SeS-0eveXu2cN(XCbDclK6ideC-uCnZ``Kdlk;6iqI7wy}42t=ytq7 zr%xddTUiux<#fw4+{@}K=bE(#rntYq4C6+Y3NklbBnOrK#u14XZ+rLth{Ti*RnCle zLGkVEsGU9iS0Z_mYkQDRuW@9ssfZ&gMrJv!ep!Fa@>b8fimA`s4`$%i@v#u$Z1)UeOozZ%& z$Sk~atGHC}Ox~MmDrh9VofJf%*Cmf7yM_r{3Lrv5I^}ct>iL}i;88%#hN0yiT&GJWymk_5a zP_qqm>!8m;<`+hiyIv%NE93Y4Bho*<6U7Qc;2Nh|IY$8{0sRrp@l&U!$1;cSh4!1` z!h$v*j+M8Ij|y9co9+x=*qJdBNrHfzN1S+wnSVMiQvjQ1L+z#!M@oGvx;R&l7Cq#=HW?=EG3T3$syaa_UX0G!V{M z7(&~A7RbA*ga)A!RAL2~w3Wo(cf@f4%WaON6A}5G4#;Cq2@>qzb#KchP?F%ilJ_71 zES-K|rByZ6-71Qu?0tWQCO*ptMr7e?i&zD82agA7uptk-*}3bq4J}!KtKkQjM_reR zDVOoY$7H{GN}cwpukI#DuW#MkyY#|=wK15QFBZL!ow@4HUBfLQV~g2#C7vLNUdUX4 zqL}InM>r3NEo?aRwBa&rjwO&gx%i z5{`0vXkk{lejWwtnM6K=^A88qzVb7^YUWEXpLJ)wiJb4*vy|rF_1(0MlqgAejAU@v zJ2zXBFf;6$phwWWcG}x&ST8l2dyy8@5^9GS>yQ9m&BT15*Ks*neI4wtJxEadF?de;Y|AI8Ed9{ajv)qRLk=FVub+Gfd*=UA>J@$to!Dk4iEh)LGp8^~@+lp5#R&kvdrQocO7h1nz? zt=H!Qkt8%U3R}ahPVjlt`h3mi#eF_tLksl_t8|dvYJ%SVc6i%J#0vXO*kn|bs|%_2*W^Pq;DBbnXlB8bPlq*TwN zh*a{U=Jj)K^2t0=Q_ibXl_tg!<~lfzTSlIl^M?SvQ z`4~rdK$?(yJ6SR|h9WOCR)|T3%U~SL?E7Uv(>&gyGB&$?HfWq)+D|NGJ}~nTv7X6) zVBqsnLtK|Ocl6;ut$ZK7@dW2k24<&cM zm^s=NmbV5W34!zK$MYvn#GA|ISEn!U7jH{l&o7G?A7^VDKhNakBQ5fEL70O7-=j+N zQah3a0-{2yAL6sHE1Yr5{wLuQiQFM#7fmrGbU=*3{Xfh5OQ0&g1%+8p88S0NggyUl-MI&6(8VuF11f z@tv>?;vC+AnAAuQM}Gs)8W?!4;W;Tixa<9>@jEQV7Gx_oB8aKDGS4j`2aEJucAov5 za(=F{8fe9$Xh_M^@o?qxC({gX==bgTklLs9=j*dJtg+Za7E_QdU!=*mCIw`ZAor6M z>y*38>Zb=GissNH=4jIcKj=wr@GN%dXLFtq7t z*;72WuZzSqR!5I|aT@X~a>C%Ug@Ne#BIDTNtAYor-PlA`X-<+GsyGtHf+G&0NN3@_~TJZLY z{ak<0D{8|Od~tCX(NvDaq{)#mnmoeqBvt!qAt*PgKbc!zjEOo1q$E3mPy}k-h?~v5clALgvjtt*@0a6FFV!d5a8fLYhh`dtWrO_sOpE$=@KW!Ymhmo`(vD+AU)0}%}hj^}M6Cf!tglb$~@{|^F8?UKo%kLNE za{@Qz5BNZ(hPt%f(~=k?xr7{ za7}!7d!Eq>e^&FG4^I~VHaDb%iusShD3b#SK()pTsYQd5o@W)lpM><}f2H386h{sk z&SbVln_Qkyq5#$8jOeBxt)4@<#={?PC-db zJ=c@rsepkcQ~(f$q5kE3s`A@)I>wlxt=?-}w*Uda77sar5r3()ELjR4_T;8E7)srT{ z0hu1l@n0VvQCUM@5sY-{c$b(*4gcu}Rz`#rm6hu^dmSN&k11M*@BPL`{$DH#4s#Bg z{>918^-N@Kw74TD*)&vD_WRG_kH1&gF*Rl40bZzRbvo-G5ykDTj*0DV$}30kdU#Ya zNa5`h0P*=3qOci-Gq?KqrGNC$nH(l;Xfgyt^B>iLZ6UU$=^d9FWKqyVqSYg;-RCIV z>$`T_I11tdh`$8DApy{e^JlPB&F^O~;tnhD-Q2lp>h0A`wa@`2T0Kqy#*#_fq9gxt zy{g{vut)+S@L#I;)PjiLMsBbs2%Sp~cbv%%YRh0d)Y# zDH2ouS>6Zn(9kyk7PRqxRiTjsaOi(z72q%g{ngApmaJR;S^wH89AzvE&L+V)eoINM z$Ux0_f76oi$tAGsv{Yg;1hs!EdHTr?CP^GB=GFoA>_5BAC}EP65&jX=?#=vv7(6n7 zhWtaT$Ii`N^1F+=P>2T>qt;BEWUmPjNXHCx=onzZ`z>?t9S(CGjGGobYHCKh*__$< z#FeA}(6a)7evHh*>_i(}#YZ7C#h^(NI>g zaB|K$_P5*Lue$%rg1uA+SjI|#s?$4niyzU2N~SO--n4slp|suIlI|V!r@cl5y|^6v z6R43rF1KxIBKVKHlpD0?5OrTb74cdIFxF8{ueOOI_fNjQ;00dks|M$7ZlKbltXs6O z2T5eQ1ORR(BcI0_>Wh~G@agxnAoz;sz>4Sk%Kw*ujhYK>_aCDfv4N@6B(?Qrpa03` z2Z9&*LHegGi}?QWC`}k@dVMR%$RaDL-R)ms1EOvB0gnR!8gtqY8bG)hqSRrS$HLx{ zJMF6OML<{Q>Z&{S2-bj#Ji|p`>QV(p+J)kzB z3LHI?zrI2bm*EONlN4nj4-KG_Fip!by7KB~qtzA09AEtk!`1muy%<1(VveBL5oqO| zVsT8$&%6#mH=L2*$IG2%V%oHLLZDl105q27adOVm!|-K_hkI96Oc$iEkN)q01>t`u z>iv$$@X5h0v2f}T0urDV0e^pj#1Ntg8;sI@%YVqn*2{c&$ zXO-a(YH~ohe+2p|h>yo6aqt7k8UI9A>|Y-~g@1_vaK_4)ME~7fK>2rb`R4A96#DE- zPL6{`+J7cA3@6tiPWntnJr~^3VYni!{jbe!+JD;W(7BsF19bcc2m@kR{J$rOO-l+2 zdNTf}k?za?ReO8;PDjTUVD11(D*sPQM}L2R4?5-l?$}5DAHz2+RA^N`51s#B$fy6G zj`F{The&CwYDeAv;NjU1d>NTV^>Li`=Qb2|=F%>0aQL&Fp;Ltin_7szo|CL&*|V9V zPAP8EkKnfm=s*zjFD)BZCYIxEdBV! zSO$;@wWsFB3i=A3KVgvzr?Q^lwO)9+;`c}yOg5D0wt4f;!Yg+dhg?*A>n#;`4h(|6 zRucfxWf>~s_FBwUd}-L?l2=pn*3++{uYzh9^7S7%sX^U4=9G z%lt$uV-~;O4H2(rqi#vx5F1g&?;nXEY{AurBLzR-JLEn-JBJP$ zDRNuQ_Pgp`cZ0v~}Sp3KW656^6Y{rUFAY|sVT^n!tEZ=Rh$I&uL=<=WkI?Lrhe=(ynW9nU1@ zuf!|A2&OBSsp}6aAmSdDecO8s=(vj#2P>cpbD$vDV4_cihbKVUzDW-oyK*$Rx#|(& zw)0$C`}jCmev~!M6q7+`5&hY0oP73Y0K7O)rJ?i_RFF`G25 z($W~n2`T?c>5x1u+JmsFJU$prR#d(4N3@#*U+n;NG2y3+MXPwlA5qHjY}xLF+G02} zN7hfAQk?$ApIbQhw_d>nxa>YwYgL^^mU<&9%I4Mo;?IrZr!FvA$(kYB8o^AS3FkQB zccgkPm2VWW2$b2E*18<=0DqantG7PstDxyTr0S`0mSTL=l#AAOgq(o17`FO)Wg}+WZf9x;lp8tg_w?TX+ zkQ*+)=8w<|LNfHI{tN|&zKICP2+%8Z>7!lZo)DYa&q}NOblwAh8yawf0BjuIFT-Zr6xo!@t8w_V>5EQX1#{&cv_A}6JN zt8v9*;dXhmBlRF%#kZ#nujw@9jedgucNIbC)VVsk!Ki8B$+7&>4DF2{c(&}lWTkUU zbP0ifO@#0cSfi;yBX@aP^ay2y>e(+i``fD{l~RpUD7ib?yZ8roE_}9qO2s9gAB(Cp z$`E_VsCR+uGO9*1SEXY4?^$}yNtd$7FqVBM(zjQI?TAZp`QFo_lx=D}aXoR~lWCHR z?jvzGLgcWWH_!e@9)|42NXjl{|HpAQdXMvtpON6xd%Aw5CH`zHkU-qhZ#(v_$cws8 z8+)FJ#J0rM75Mu6?(sgoX5Y>IUmDDNdIAhtLBv}b`85cg`&|!7TzBz@skP}#Di^l} zWKCnJzVt?J#M%Ax&cEi)?HMjpM{4r;h+{grHK$XLeR9>U*&(G#msfTYfwe9jFIO15 z6IUHWMIVf4MF?qzPC(+sNeJf16`>ZygG7MK^Xx~fagQC*)y{VB;;o~~mHEQYeS6Mu&y}l)x5nD5yMw=DRm-Q85ORINciU-SZrdo+LMk9dBHsVhPxt842$2 z(v=4ET}K!~V7#MAdD%ymmgu32gT z4U$&zag}bL6WbBTl9v4TDAhi5x_eOL_W*g<>QyyoqXn>e%9rNq;dk^}F1wQ2->a%> z*E6O}ru%~OD9JiVhuDCGjvo2qo5ZI+UxIKTM?M73F+%4VLA+M|ZH>AP6PQDMCtmSi zM$@X5h1yE`3ON^Zh*i&ybH8}+#hefO9&8Y0TrHj@x6{WN)uw)-E=9Altc4ecj6)A4 zyO-*>8>M_bGLLF2B@ZE4ukZ6*Gjc|Y;l{&%ZKOT*eO?_;Q*bFodA`J!UDlwKI-}2T zZMBtgdBn>5Yg<0MSNWb=Y++379^J%jWq>e)kh>K5jVP{FR&uISCQ*WUG!uDj3p+BX zfiA_C1QzShZxdw*H#Ov%(T_FZv#{Qx`z0C~fBI@Xj__g4T^#EG*?eoIHJFLGvRKax z4|M;?*Q&ZCMhe~{AinP5loV-0-_W?k^&zrTN(zG-TMe}&yty= z!=u)Be$sHu@?uV@Y;h`?zU2`M{S9>q1I=2`5xw1jW1ZO5bG>U1N$wy!e!yvdYUf_p zaY{0zTThDhE!!E-FTIh_-4y^{0xVRC%OEyBKVjD*DtRqh<%90PUYaZA2}b2MY%x{) zMUjw%wK$dHOmt{--}I`&q`7JQN8G;|*V!2|&s*~nRY^*Ut+u*E&(ct-G8oizd@`<^J6S!d`KmDE<&BD2Ft#$F5G-WJRu07^&yn|x z+qn4k5`XVc%JXQvTdu*Fm*B=+d!3`b9wE*EWWxCT_!9PSd#txQ-hMe?~ z`8V0(6Mds6BU1+@Y_*0=^5Fl@xTt(`0T)RRpdq#r>l;T6psBmU$=LiW4mIX7qs80p zHiz83MJN8V(B-_go8T%Q(P_h993)tBfnL$T2tPa6sI&!Z&??;HaK6mvvAlS8?|X6k zq#{i;LNkB84#&E5glunNRQ|5F*;m@twABigD^TX7V+&#MW8|)*!7;0;J6nQa$C2yB z@>KJ-krg^_@eeyj_b&^PmkrHBY%dJd2VSUI_?ORUH;MP}8C9Rk0t0d^^mSVGI{CP2 zGS$vzs5L2NAnRo@Z2Hb}G|kg?IwWJ(Szw|ySG^YB-{G68Rsn2wR%O|Dw+8sf)SmGV z*h(Tog|NaYV4K;VE>OSiJX*S=Nl|uji{D_YsB>n|0s|l1&piL>kjwdJGW8ZeCTduy zQUjZ!ssxxwjR?7*^BZ0@&7QvA3@#p{CI*6QRv@$aI8HYh_K*=fy<7+$6?aYG%BOT8 z@;G3rJEb|4SPQJ@0Dns(lX=SXB<;a*#{?>^xQ}(PaVgo|yE?4_>FXenF&&iGQL}?_ zBTi5WkDDFSD^NaiFI_tt?5#Yl(@s{IdHN>2(r(g<=xff$vl%be8Sg3U>5m3dGuM^7 zx0Q;)F6Mq-E*v(!mUXYpn?3S7C-vEG4vX0`L<4Zo9|H%<9LfUR^Vg>p#`I4OnaI*} zc&*!QXcYY8p3D%Qe2;9mz^9)HajAF#2eofo*{zY(ui3Z@Cn&IIs z$8U0R(e-LeY)}o9l+oSll+fyuXC03=Tf{rOFfy1J>*-pt*$dvsKN7UkWE>Y93oP554V1n5$r7t25`$$;ddVH)lE6&4reUKgVl)x%d_`uqIGO|upE@b(+2 zAbXMepgzJ8v@cscW#izPcd9(nT5ATzrXNRrAFC&x`mM}RLU;ea4Avk{r}KApOGMWQ z_+B2hUQ3t0yG0Ub-*nmIUMViO*|NkA_(-J=lh`37{8-AbHLmUF@V{+Etk@&qRHFnx zhf~u7-6HK@M>Ib0{1sw~{q|yEva9gT=O=Cq9R67qT+e=S;(?*+_jFHv_g=tddV2fw z-aao%>DWV`KNf%I3R|1xpP467*1Y=`09HeZm=T&p)0e5)F|sh$FHH>E&#Jk_C`C;v zORLgg&1ZUA1LglV@&ura>X61DScS)^oE`Bq&LQGp*SRHaubU>_nPk;4RsE*=@vI$M z7q_a&kts_&J!-a*>fF~zV=3SD)-l65KDIHJ@OzBdz_pasPH^3i7ZYDbh-y9&G;2+4 zDt)3Pe!b;Zisj4p*+bW0==xq$GRK+F3SwchECvZn{*K5y@Mr4(R5F?R*+vrvc7ojQ zTtP(xLZ^&K4^b)Kqg;*f?#$85YwW4Qjh{$%uYBEjrRm8H_vd~ObJrVtFuTyPxjr&1 zn<@Zy!n6AhTD?oDwAv;qXy{>3=`QWj>=`W+hqbE@BcE_qu_$vC``HV$Wc2V3hN+fb zTZBsF*SHyx>IY8PB71ZwJ8IJ=44Ufp`coxEI>HVf_Qyyk`g6@g$PWD%5D(sC{VP#; zp%rkM;!wOXEvn_)8%V{9A8t8IT418|RB|fa;=>NC0_6O13)Hr!qx>p{^}B_jOK7Kk zhatRwsjl|o_(Pk9a&*M$_+nk^goc5_L9P!cYwx9}h%-&h(4xxsKL@79x$iQ?L6FHi zAZJCT1L~Ol+jD%ZzitZCGl4E%?E>!>Z8Q*uFiMAta_^Fph;yb`iWOmgu8JfyT5T{{wp*BBZ z&5YGiN}YXyPO9s&%1G02{~fcgKs7`^Uyg+2R3n#w!zC4qG7dGoKN8qZx+#cinnXG2 zH-_T|<=%oj%}Jr1uxG7`-dUTXVYDa7M}axctcLN zBE=`Nug4GF*g}3CEUh&OE6yDMSv7fj9g{lCZJ+Fr=k9zvb#>W+Fy&u&C^mz z{&`P&`W1SC`DYl-K8L2&wf4s>X~3!YKd+Cor2fQZsT@h zFg|anP9%+g)kR3~;6edHOB=X5Gav}*^H#TrXlp2C*}%#|)3-aO8mMsE6Qe@Kd)GYB zTBJzOk-zZNLO zun6`PK;DrMIw3D<-Kt#9OB!Sai=@cbAnRnxmaJnLB1_#GgmIf0LncF(8Db1G2!oz8^juf{0nhXK@tp5D-|zSO ze$Mr|&inPb-jKKXS6${`*8_jgqe@#*MwdjTG(k~Omqfn>M9gN}NIMvUBYt3`Jz+!j zAroFOupp=y#S5;LYp$-HJFMXX+rP(9!l7J|HNUYJv?TXwC`%=?UthM&WT?KYBtJdv zQHd=$I4KqfYpxdVYh3ZM;*Px+a&4{ zRmN;}PgE0mfDqL^$Lk~7L}jrcqK1uyDi+ktu7#~&jqqU=aM-Z}Vh$kRK-ac=N^Z{_y43z$HLeGqb>9Gk)3b3LzPsW4Hp2hpZ^`# zPvx26khPC+#L7kj`BZwRzZ1-9keZB53j2%nZU+vT{h}!q?*tU98pE3e$Q#(&?J$(( zJy03q(c)eg@6m{p6ZFbsm^)y#=TBwz92^_18Gjj^|IpNT`*Y91rP-mV$P;&}{~ER$ z9v#{TY1CGnk(dwnXbD<++&5V^GSF6w@JYphH;7Zi; z0)$GrwT@(T^uX712(Se?*MSaiu3m>|LN#5-p;IsaI1QkX)6e_1Xq6ka<|k*#>CX zH8I6&#bL>#h=_y=&%>_vZR>x}*gLk1!t*_z;zq8trzr^AV>o=|1+U`j>zW!8eAZgw z#-Tk>9OS5pai2Z-(TuEfU5q#$-jjUx4y_thFlql}Mmy8!l7NO+=SVH=?(t2|Ku^wW zu{$%!zVmpfsM?{wA zkUNLFvp0ckLMW%oVIy2?kOouf8G5xfMD1brAHoig4cw?pQ)hP9ExZnb&_gsRf_$m$ zwb7*4$kbLEo^DvH{F-atK| zN6_hU(momFu9ec&`(d23Q`9qrHN-~1hSYthHA#n&djINzT2Rd8HA zNF6=|_k%Ic@2Du_W7REJ*)#&{r*7j&`L?kwZzE0C-9*^s7BON(H5m(6ovt4%TQ@VQ zRRg;hXmzLi+ef4hc#GG%?1#(3O6`^fm5MxU%EYm1g$(C7_Q_A4*3jhKE57W(Pxv6K_J2&^#+`ACfBT)3U4y)wZ+h`DTE&|8-bc z84%^|NbTwG&Z%Y-or+ccD{|bvB%vQJ@2V8Z9v>(NB?7GIaYGjqQ*d9yS_`Be8u$46 z$>nVMZGPrjl3~O@XGg*W3(Tt1y+xMenjoc%pIR@uNWUB#6IAA|fKu~s11 z(dH+91KB{-0+W-cVb#s%KmL%-O#!Rmuw{PqaykXB-DRLf{<)_mPX>WS(W+djY^Y zV;L>N4Z&Yl>7lNHPYqN{O^Hnu^n>=h$ZxZ?3iXT6|5 zyQ%>Dd~+H29Co5a6d;&8QDFWvYk+fC<{r&X{C@@`e~8?k&KeWHnU}Z=2(v4Y%N54Z G=>Gs4<{lFO