From 1222baadeed229de56b7e2b41ecdf5b8ce0c21d6 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 9 Sep 2026 02:30:44 +0200 Subject: [PATCH 1/2] Fix lock icon getting blurred with its container, tune overlay look filter/opacity on the container recomposited its whole subtree (including the lock icon appended as a child) as one filtered unit, so the icon's own filter:none/opacity:1 couldn't opt it back out. Switch to backdrop-filter on the overlay instead, which only blurs what's painted behind it, leaving the icon crisp with no need to reparent the site's own React-owned DOM. Also lighten the blur (pixelated on small pills) in favor of grayscale/brightness doing the "disabled" signaling, and size the overlay 4px larger on every side than a flush inset:0. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013qK4zsF5ogU7cjRVL3AQZ2 --- .../interventions/blur-disable.ts | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/content-scripts/interventions/blur-disable.ts b/src/content-scripts/interventions/blur-disable.ts index 208f038..aa80a90 100644 --- a/src/content-scripts/interventions/blur-disable.ts +++ b/src/content-scripts/interventions/blur-disable.ts @@ -20,19 +20,38 @@ const INTERCEPTED_EVENT_TYPES = ['pointerdown', 'mousedown', 'mouseup', 'click'] const LOCK_ICON_SVG = ''; +// `backdrop-filter` (not `filter`) is deliberate: `filter` on the container +// would recomposite the container's entire subtree — including the lock +// icon overlay appended below as its child — as one filtered unit, blurring +// the icon along with the button (filter/opacity are compositing +// properties, so a child's own `filter:none` can't opt back out once an +// ancestor sets one). `backdrop-filter` on the overlay instead only blurs +// what's painted *behind* the overlay; the overlay's own children (the +// icon) paint on top of that blurred backdrop unaffected, no DOM +// restructuring of the site's own (React-owned) nodes required. const OVERLAY_STYLE = [ 'position:absolute', - 'inset:0', + // 4px on every side instead of a flush `inset:0`, so the overlay reads as + // slightly larger than the pill it's covering rather than exactly + // clipped to it. + 'top:-4px', + 'bottom:-4px', + 'left:-4px', + 'right:-4px', 'display:flex', 'align-items:center', 'justify-content:center', 'pointer-events:auto', 'cursor:not-allowed', 'z-index:2147483000', - 'filter:none', - 'opacity:1', + // A light blur (vs. the earlier 3px) avoids the pixelated look on small + // pill-sized targets; darkening + desaturating the backdrop is what + // actually communicates "disabled" here, the blur alone was carrying too + // much of that job. + 'backdrop-filter:blur(1px) grayscale(0.6) brightness(0.55)', + '-webkit-backdrop-filter:blur(1px) grayscale(0.6) brightness(0.55)', + 'background:rgba(0,0,0,0.25)', 'color:#fff', - 'background:transparent', ].join(';'); /** @@ -50,8 +69,6 @@ export const blurDisable: Intervention = { const originalStyleAttr = container.getAttribute('style'); const computedPosition = getComputedStyle(container).position; - container.style.filter = 'blur(3px)'; - container.style.opacity = '0.5'; container.style.cursor = 'not-allowed'; container.style.pointerEvents = 'none'; if (computedPosition === 'static') { From c87f86f82e1b3c1e84d5ec6bc7e45ac340015d9f Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 9 Sep 2026 03:03:51 +0200 Subject: [PATCH 2/2] Send a ping every 20s to keep the WS connection (and MV3 worker) alive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend now replies with {"type":"pong"}. See backend commit for the full rationale: a MV3 background service worker can be idle-killed after ~30s with no activity reaching its own message handlers, and a disconnect while a lockout is active used to reset the visible lock state on reconnect (fixed backend-side) — this keepalive addresses the other half by making that disconnect much less likely to happen in the first place. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013qK4zsF5ogU7cjRVL3AQZ2 --- src/background/ws-client.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/background/ws-client.ts b/src/background/ws-client.ts index 5443108..988ec71 100644 --- a/src/background/ws-client.ts +++ b/src/background/ws-client.ts @@ -32,7 +32,22 @@ interface WireError { message: string; } -type WireMessage = WireLockState | WireAck | WireError; +interface WirePong { + type: 'pong'; +} + +type WireMessage = WireLockState | WireAck | WireError | WirePong; + +// JSON-level ping, per the protocol's "ping (optional fallback)" section — +// used here specifically (not relying on the browser's native WS ping/pong +// alone) because this client runs in a MV3 background service worker, which +// Chromium can idle-kill after ~30s with no activity reaching its own +// onMessage-style handlers; a plain protocol-level ping/pong frame isn't +// guaranteed to count as that activity, but a JSON message our own +// 'message' listener below processes is. Sending well under that 30s +// budget (not waiting until the deadline) keeps the socket (and the +// worker) alive continuously rather than trying to time a wakeup. +const PING_INTERVAL_MS = 20_000; const WIRE_TO_INTERNAL_SCOPE: Record = { buy_only: 'buy-only', @@ -95,10 +110,14 @@ export function connectWsClient(options: WsClientOptions): WsClientController { const socket = new WebSocket(`${BACKEND_WS_URL}?token=${encodeURIComponent(token)}`); currentSocket = socket; let authFailure = false; + let pingTimer: ReturnType | undefined; socket.addEventListener('open', () => { reconnectAttempt = 0; options.onStatusChange('connected'); + pingTimer = setInterval(() => { + if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: 'ping' })); + }, PING_INTERVAL_MS); }); socket.addEventListener('message', (event) => { @@ -129,9 +148,12 @@ export function connectWsClient(options: WsClientOptions): WsClientController { // 'ack' has nothing to react to yet — the client doesn't send // unlock_request/escalate_request until lock-decision logic exists // server-side (see backend/CLAUDE.md non-goals for this milestone). + // 'pong' likewise needs no handling — sending the ping is the whole + // point (see PING_INTERVAL_MS above), receiving a reply isn't acted on. }); socket.addEventListener('close', (event) => { + if (pingTimer) clearInterval(pingTimer); if (currentSocket === socket) currentSocket = undefined; if (manuallyDisconnected) {