From b7f450e41fb05a74e1ca0f7477f58449048d1cb8 Mon Sep 17 00:00:00 2001 From: th Date: Wed, 15 Apr 2026 08:57:24 +0200 Subject: [PATCH] autosync toggle, configurable sync interval, 30-day session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - header Sync button now has a paired autosync toggle to its right — reads as one button with no inner seam (outer border only). Dot inside pulses once per actual sync (bursts on every backgroundSync / triggerSync / collab pollOnce via a new sync.pulse hook) so the user can see polls happen. Dot + data-autosync attribute persist the enabled state in localStorage; paused state stops both the CalDAV/relay background timer and the collab poll via a broadcast autosync.changed hook. - hover polish: Sync regains a full accent border (the missing inner edge re-appears only on its hover); the toggle fills with solid accent as hover treatment so the pair never shows a half-outline. - sync interval moved out of hardcode into a settings field under Settings → Relay: number input, 5s..3600s, default 15s (was 45s on caldav / 30s on collab). Changes broadcast via sync.interval.changed and restart both timers without reload. - session cookie lifetime raised to 30 days with matching gc_maxlifetime and SameSite=Lax (was 0 / Strict — some users got kicked to login on every reload when gc cleaned up the /tmp/sess file or a cross-link return blocked the cookie). - search button's kbd element was pushing the button ~3px taller than Sync; leading-none + tighter padding puts them on the same height. Co-Authored-By: Claude Opus 4.6 (1M context) --- config.php | 2 +- electron/package.json | 2 +- public/css/style.css | 75 +++++++++++++++ public/index.php | 36 +++++-- public/js/modules/caldav.js | 93 ++++++++++++++++++- .../plugins/collab-buckets/collab-buckets.js | 26 +++++- 6 files changed, 222 insertions(+), 12 deletions(-) diff --git a/config.php b/config.php index 7830864..eec9a6d 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@ button:first-child { + border-right: 0 !important; +} +.header-sync-group > button:first-child:hover { + border-right: 1px solid var(--t-accent) !important; +} +.header-sync-group > button + button { + border-left: 0 !important; + align-self: stretch; +} +#header-autosync:hover { + background: var(--t-accent); + border-color: var(--t-accent) !important; + color: var(--t-bg) !important; +} + +/* Autosync toggle button. + Border stays neutral in both states — the dot carries the status + signal (accent + pulse when on, grey when off). Hover styling is + handled by the bg-fill rule further down. */ +#header-autosync { + border-color: var(--t-border); + color: var(--t-muted); +} + +.header-autosync-dot { + display: inline-block; + width: 9px; height: 9px; + border-radius: 50%; + background: var(--t-accent); + transition: background 0.15s, transform 0.2s; +} +#header-autosync[data-autosync="off"] .header-autosync-dot { + background: var(--t-dim); + box-shadow: none; +} +/* Hover: button fills with solid accent (matching the Sync button's + hover border color). Dot switches to the app background color so it + reads as a knockout against the fill. Pulsing keeps tracking real + status — when a sync fires during hover the ring still animates. */ +#header-autosync:hover .header-autosync-dot { + background: var(--t-bg); +} +/* Pulse shadow is accent-colored too, which would be invisible against + the accent fill. Swap it to a bg-toned ring during hover so the + pulse stays visible. */ +#header-autosync:hover .header-autosync-dot.pulsing { + animation: header-autosync-pulse-on-hover 0.9s ease-out 1; +} +@keyframes header-autosync-pulse-on-hover { + 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--t-bg) 75%, transparent); transform: scale(1); } + 40% { transform: scale(1.4); } + 100% { box-shadow: 0 0 0 10px color-mix(in srgb, var(--t-bg) 0%, transparent); transform: scale(1); } +} + +/* Burst pulse on each sync. JS adds `.pulsing` at the start of a sync + and removes it after the animation finishes. Tuned to be visible + against the button background — a generous ring that fades out. */ +.header-autosync-dot.pulsing { + animation: header-autosync-pulse 0.9s ease-out 1; +} +@keyframes header-autosync-pulse { + 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--t-accent) 75%, transparent); transform: scale(1); } + 40% { transform: scale(1.4); } + 100% { box-shadow: 0 0 0 10px color-mix(in srgb, var(--t-accent) 0%, transparent); transform: scale(1); } +} diff --git a/public/index.php b/public/index.php index 14d502d..10fa70e 100644 --- a/public/index.php +++ b/public/index.php @@ -17,10 +17,20 @@ if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { } // ── Session ── +// +// 30-day persistence: single-user local-first app, no reason to log the +// user out every 24 minutes (PHP's gc_maxlifetime default). Cookie + +// server-side GC are set to the same duration so the session survives +// reboots AND page reloads, up to a month of inactivity. +$sessionLifetime = 60 * 60 * 24 * 30; // 30 days +ini_set('session.gc_maxlifetime', (string)$sessionLifetime); +ini_set('session.cookie_lifetime', (string)$sessionLifetime); session_set_cookie_params([ + 'lifetime' => $sessionLifetime, + 'path' => '/', 'httponly' => true, - 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', - 'samesite' => 'Strict', + 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', + 'samesite' => 'Lax', // Strict was blocking some cross-link returns; Lax still blocks CSRF ]); session_start(); @@ -1301,11 +1311,16 @@ if (str_starts_with($route, '/api/')) { v - +
+ + +
+
+
sync frequency
+

How often the background sync polls the relay + CalDAV for new messages, collab updates and sync events. Lower = snappier, higher = less network traffic.

+
+ + + sec +
+