From 1aa541e49c34490075288d9def9b5cff9d72e3dd Mon Sep 17 00:00:00 2001
From: AsteroidHunter <65871446+AsteroidHunter@users.noreply.github.com>
Date: Wed, 20 May 2026 14:50:14 -0700
Subject: [PATCH 01/10] Bumped header sizes and added stale-fade tiers to idle
Stop/Notification tickets
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
#7: .brand font-size 14px → 18px, .brand-version 11px → 14px, .conn dot 11px → 14px with its connected-state glow 3px → 4px. Pure CSS, no layout changes.
#1: New staleClass(ticket, now) helper alongside typeClass/formatAge returns 'stale-1'..'stale-4' based on age in minutes (>=2, >=4, >=8, >=16). PermissionRequest tickets and working tickets always return '' so red attention and active processing never fade. Wired into the
class list. Four new CSS rules between .ticket.pressing and .ticket.working apply filter: saturate(N) at 0.75 / 0.5 / 0.25 / 0 — the whole ticket (bg, border, title text, accents) ages together in step jumps. Placed before .ticket.working so the pastel-green wins on the defensive co-apply case. The existing 5s `now` reactive tick already drives age-label re-renders, so tier rollovers ride that same render pass — zero new timers or allocations.
---
src/routes/+page.svelte | 44 +++++++++++++++++++++++++++++++++++------
1 file changed, 38 insertions(+), 6 deletions(-)
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 2159e49..4a16310 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -191,6 +191,21 @@
return 'STOP';
}
+ // Idle Stop/Notification tickets desaturate in step jumps as they age, so a
+ // glanceable signal of "how stale is this thing" is built into the dock.
+ // PermissionRequest never fades (load-bearing red attention); working state
+ // owns its own pastel-green visual and skips fading too.
+ function staleClass(ticket: Ticket, now: number): string {
+ if (ticket.working) return '';
+ if (ticket.event_type === 'PermissionRequest') return '';
+ const ageMin = (now - ticket.created_at) / 60_000;
+ if (ageMin >= 16) return 'stale-4';
+ if (ageMin >= 8) return 'stale-3';
+ if (ageMin >= 4) return 'stale-2';
+ if (ageMin >= 2) return 'stale-1';
+ return '';
+ }
+
function formatAge(createdAt: number, now: number): string {
const seconds = Math.max(0, Math.floor((now - createdAt) / 1000));
if (seconds < 5) return 'now';
@@ -282,7 +297,7 @@
{#each tickets as ticket (ticket.session_id)}
Date: Wed, 20 May 2026 15:15:36 -0700
Subject: [PATCH 02/10] Added a real disconnected state: "disconnected" banner
+ dimmed non-tappable tickets
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
#10. Introduced an everConnected sticky flag (flips true on first SSE onopen, never resets) and an isDisconnected derived (!!clientToken && !connected && everConnected). The sticky flag prevents a false-positive disconnected flash on every page load and on every visibility-change wake, since `connected` starts false until onopen has had a chance to fire on the re-opened EventSource.
When isDisconnected: a small uppercase "disconnected" banner renders below the header (role="status", aria-live="polite"), the ticket list
gets a `disconnected` class that drops opacity to 0.5 and sets pointer-events: none so taps cannot queue /api/focus against a dead daemon, and the "You have zero tickets!" empty-state is suppressed (the banner alone tells the truth — the prior message implied live state). Mock mode is unaffected (connected = true short-circuits isDisconnected regardless of everConnected). focusing state still self-clears via the existing finally setTimeout, so an in-flight tap at the moment of disconnect doesn't strand the pressing visual.
---
src/routes/+page.svelte | 42 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 40 insertions(+), 2 deletions(-)
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 4a16310..aca2d9c 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -20,6 +20,12 @@
let tickets = $state([]);
let connected = $state(false);
+ // Sticky: flips true on the first successful onopen and never resets. Without
+ // it, isDisconnected would flash on every page load and every wake-from-
+ // background, because `connected` starts false and openStream reopens the
+ // EventSource on visibilitychange / pageshow before onopen has had a chance
+ // to fire again.
+ let everConnected = $state(false);
let focusing = $state(null);
let mockMode = false;
// Reactive: cleared when /api/ping returns 403 (token died, daemon restarted).
@@ -84,6 +90,7 @@
);
eventSource.onopen = () => {
connected = true;
+ everConnected = true;
};
eventSource.onerror = () => {
connected = false;
@@ -206,6 +213,12 @@
return '';
}
+ // True only after we successfully connected at least once AND have since lost
+ // the SSE — i.e. the daemon went away or the network dropped. The
+ // everConnected guard prevents a false "disconnected" flash on initial load
+ // and on background-wake reconnects.
+ const isDisconnected = $derived(!!clientToken && !connected && everConnected);
+
function formatAge(createdAt: number, now: number): string {
const seconds = Math.max(0, Math.floor((now - createdAt) / 1000));
if (seconds < 5) return 'now';
@@ -284,17 +297,23 @@
>
+ {#if isDisconnected}
+
disconnected
+ {/if}
+
{#if !clientToken}
Scan the QR code in your terminal to connect
- {:else if tickets.length === 0}
+ {:else if tickets.length === 0 && !isDisconnected}
You have zero tickets!
+ {:else if tickets.length === 0}
+
{:else}
-
+
{#each tickets as ticket (ticket.session_id)}
Date: Wed, 20 May 2026 16:00:40 -0700
Subject: [PATCH 03/10] Added a real browser-tab favicon (SVG + ico + 16/32 PNG
fallbacks)
Copied a placeholder mark (single-path SVG with prefers-color-scheme auto-invert) from PycharmProjects/webpage and dropped it into static/ as favicon.svg, favicon.ico, favicon-32.png, favicon-16.png. app.html previously declared only pointing at the iOS home-screen icon, which is why browsers were auto-deriving a generic "E" tab icon from it. Added explicit declarations (SVG-first, with ico + sized PNG fallbacks for older browsers) so the tab favicon is no longer derived from the iOS icon. The existing apple-touch-icon line is untouched, so the home-screen icon is unaffected.
---
src/app.html | 4 ++++
static/favicon-16.png | Bin 0 -> 347 bytes
static/favicon-32.png | Bin 0 -> 826 bytes
static/favicon.ico | Bin 0 -> 1653 bytes
static/favicon.svg | 9 +++++++++
5 files changed, 13 insertions(+)
create mode 100644 static/favicon-16.png
create mode 100644 static/favicon-32.png
create mode 100644 static/favicon.ico
create mode 100644 static/favicon.svg
diff --git a/src/app.html b/src/app.html
index 115b3fb..3db7e57 100644
--- a/src/app.html
+++ b/src/app.html
@@ -8,6 +8,10 @@
+
+
+
+
-
-
-
-
{@render children()}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index d78a2b0..63618e3 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -183,7 +183,7 @@
} finally {
setTimeout(() => {
if (focusing === ticket.session_id) focusing = null;
- }, 300);
+ }, 80);
}
}
@@ -298,16 +298,12 @@
Expediter
- (v0.1)
+ (v0.7)
- {#if isDisconnected}
-
disconnected
- {/if}
-
{#if !clientToken}
Scan the QR code in your terminal to connect
@@ -353,6 +349,12 @@
{/each}
{/if}
+
+ {#if isDisconnected}
+