autosync toggle, configurable sync interval, 30-day session

- 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) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-15 08:57:24 +02:00
1 parent 6cd00f9660
commit b7f450e41f
6 files changed
+222 -12

No files matched your search

+92 -1
View File
@@ -173,6 +173,7 @@ $('caldav-save')?.addEventListener('click', async () => {
$('caldav-sync-now')?.addEventListener('click', () => triggerSync());
export async function triggerSync() {
pulseAutosyncDot();
const btn = $('cal-sync');
if (btn) btn.style.opacity = '0.5';
const headerIcon = $('header-sync-icon');
@@ -285,6 +286,19 @@ export async function loadRelaySettings() {
keyInput.value = s.key || '';
keyInput.placeholder = 'shared secret';
}
// Sync-interval number input (shared setting, persisted in localStorage).
// `change` commits on blur or Enter; `input` would save on every keystroke
// and restart the timer on each digit typed — noisy and pointless.
const intervalInp = $('setting-sync-interval');
if (intervalInp) {
intervalInp.value = String(getSyncIntervalSec());
intervalInp.onchange = () => {
setSyncIntervalSec(intervalInp.value);
// Reflect the clamped value back so users see when we refused a 2
// or 5000.
intervalInp.value = String(getSyncIntervalSec());
};
}
}
$('cal-caldav-settings')?.addEventListener('click', openCaldavSettings);
@@ -350,6 +364,7 @@ function drainSyncEvents(relayRes) {
async function backgroundSync() {
if (state.bgSyncRunning) return;
state.bgSyncRunning = true;
pulseAutosyncDot();
try {
const headers = { 'X-CSRF-Token': getCsrf() };
const [res, relayRes] = await Promise.all([
@@ -369,13 +384,89 @@ async function backgroundSync() {
}
}
// ── Configurable sync interval ────────────────────────────────
// One setting drives both the CalDAV/relay background sync and the
// collab-buckets plugin poll. Stored in localStorage as seconds.
// Changed via the Relay settings panel → emits `sync.interval.changed`
// so every poller can restart with the new cadence without reload.
const SYNC_INTERVAL_KEY = 'hyprcrd-sync-interval-sec';
const SYNC_INTERVAL_DEFAULT = 15; // snappy default — tune up on slow machines or paid relays
const SYNC_INTERVAL_MIN = 5;
const SYNC_INTERVAL_MAX = 3600;
export function getSyncIntervalSec() {
const v = parseInt(localStorage.getItem(SYNC_INTERVAL_KEY));
return Number.isFinite(v) && v >= SYNC_INTERVAL_MIN ? v : SYNC_INTERVAL_DEFAULT;
}
export function setSyncIntervalSec(sec) {
const n = parseInt(sec);
const clamped = Math.max(SYNC_INTERVAL_MIN, Math.min(SYNC_INTERVAL_MAX, Number.isFinite(n) ? n : SYNC_INTERVAL_DEFAULT));
localStorage.setItem(SYNC_INTERVAL_KEY, String(clamped));
window.hyprcrd?.hooks?.emit?.('sync.interval.changed', clamped);
}
// ── Autosync on/off toggle ────────────────────────────────────
// User can pause background polling from the header (play/pause icon
// next to the Sync button). Setting persists in localStorage.
const AUTOSYNC_KEY = 'hyprcrd-autosync-enabled';
export function isAutoSyncEnabled() {
return localStorage.getItem(AUTOSYNC_KEY) !== '0';
}
export function setAutoSyncEnabled(on) {
localStorage.setItem(AUTOSYNC_KEY, on ? '1' : '0');
if (on) startBackgroundSync();
else stopBackgroundSync();
updateAutosyncButton();
// Broadcast so collab-buckets (and any future pollers) pause alongside.
window.hyprcrd?.hooks?.emit?.('autosync.changed', on);
}
// Start background sync after app is ready
let bgSyncInterval = null;
export function startBackgroundSync() {
if (bgSyncInterval) return;
if (!isAutoSyncEnabled()) { updateAutosyncButton(); return; }
backgroundSync();
bgSyncInterval = setInterval(backgroundSync, 45000); // every 45 seconds
bgSyncInterval = setInterval(backgroundSync, getSyncIntervalSec() * 1000);
updateAutosyncButton();
}
export function stopBackgroundSync() {
if (bgSyncInterval) { clearInterval(bgSyncInterval); bgSyncInterval = null; }
updateAutosyncButton();
}
// Restart the background timer when the user changes the interval.
window.hyprcrd?.hooks?.on?.('sync.interval.changed', () => {
if (bgSyncInterval) {
clearInterval(bgSyncInterval);
bgSyncInterval = setInterval(backgroundSync, getSyncIntervalSec() * 1000);
}
});
// ── Header autosync button ────────────────────────────────────
// Accent dot when on, grey when off; a single burst-pulse fires at the
// start of every actual sync so the user sees the poll happen.
function updateAutosyncButton() {
const btn = $('header-autosync');
if (!btn) return;
const on = isAutoSyncEnabled();
btn.dataset.autosync = on ? 'on' : 'off';
btn.setAttribute('data-tip', on
? `auto-sync: on (every ${getSyncIntervalSec()}s) — click to pause`
: 'auto-sync: paused — click to resume');
}
// Pulse the dot once; retriggers cleanly even if called mid-animation by
// ripping the class and forcing a reflow before re-adding.
export function pulseAutosyncDot() {
const dot = $('header-autosync-dot');
if (!dot) return;
dot.classList.remove('pulsing');
void dot.offsetWidth;
dot.classList.add('pulsing');
}
// Also accept pulses from other pollers (collab-buckets, etc.) via hooks.
window.hyprcrd?.hooks?.on?.('sync.pulse', pulseAutosyncDot);
$('header-autosync')?.addEventListener('click', () => setAutoSyncEnabled(!isAutoSyncEnabled()));
// Also refresh tooltip when interval changes
window.hyprcrd?.hooks?.on?.('sync.interval.changed', updateAutosyncButton);
updateAutosyncButton();