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:
1 parent
6cd00f9660
commit
b7f450e41f
6 files changed
+222
-12
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
define('APP_NAME', 'hyprcrd');
|
define('APP_NAME', 'hyprcrd');
|
||||||
define('APP_VERSION', '1.8.2');
|
define('APP_VERSION', '1.9.0');
|
||||||
|
|
||||||
// Database path — writable directory, outside webroot
|
// Database path — writable directory, outside webroot
|
||||||
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyprcrd",
|
"name": "hyprcrd",
|
||||||
"version": "1.8.2",
|
"version": "1.9.0",
|
||||||
"description": "hyprcrd — self-hosted card manager",
|
"description": "hyprcrd — self-hosted card manager",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1224,3 +1224,78 @@ body.anim-slow .board-card.card-pulse-local {
|
|||||||
.detail-stale-banner button:hover { background: var(--t-accent2); color: var(--t-bg); }
|
.detail-stale-banner button:hover { background: var(--t-accent2); color: var(--t-bg); }
|
||||||
.detail-stale-banner .detail-stale-dismiss { border-color: var(--t-border); color: var(--t-dim); padding: 3px 5px; }
|
.detail-stale-banner .detail-stale-dismiss { border-color: var(--t-border); color: var(--t-dim); padding: 3px 5px; }
|
||||||
.detail-stale-banner .detail-stale-dismiss:hover { border-color: var(--t-danger); color: var(--t-danger); background: transparent; }
|
.detail-stale-banner .detail-stale-dismiss:hover { border-color: var(--t-danger); color: var(--t-danger); background: transparent; }
|
||||||
|
|
||||||
|
/* Sync + autosync combo: the pair reads as one button.
|
||||||
|
- At rest: Sync has no right border, autosync has no left border —
|
||||||
|
only the outer rectangle remains.
|
||||||
|
- Hover Sync: the right border comes back in accent so Sync gets a
|
||||||
|
complete outline around itself.
|
||||||
|
- Hover autosync: no accent border at all — instead a background
|
||||||
|
fill so the pair doesn't end up with a mismatched half-outline. */
|
||||||
|
.header-sync-group { align-items: stretch; }
|
||||||
|
.header-sync-group > 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); }
|
||||||
|
}
|
||||||
+30
-6
@@ -17,10 +17,20 @@ if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Session ──
|
// ── 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([
|
session_set_cookie_params([
|
||||||
|
'lifetime' => $sessionLifetime,
|
||||||
|
'path' => '/',
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
||||||
'samesite' => 'Strict',
|
'samesite' => 'Lax', // Strict was blocking some cross-link returns; Lax still blocks CSRF
|
||||||
]);
|
]);
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
@@ -1301,11 +1311,16 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
<span class="text-[10px] text-t-dim font-pixel ml-1" data-app-version>v<?= APP_VERSION ?></span>
|
<span class="text-[10px] text-t-dim font-pixel ml-1" data-app-version>v<?= APP_VERSION ?></span>
|
||||||
<button id="search-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors ml-3">
|
<button id="search-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors ml-3">
|
||||||
<i class="ph ph-magnifying-glass text-sm"></i><span class="font-pixel text-[10px]">Search</span>
|
<i class="ph ph-magnifying-glass text-sm"></i><span class="font-pixel text-[10px]">Search</span>
|
||||||
<kbd class="text-[9px] text-t-dim border border-t-border rounded px-1 py-0.5 ml-1 font-pixel">ctrl+k</kbd>
|
<kbd class="text-[9px] text-t-dim border border-t-border rounded px-1 ml-1 font-pixel leading-none" style="padding:1px 4px;">ctrl+k</kbd>
|
||||||
</button>
|
|
||||||
<button id="header-sync" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" data-tip="sync now">
|
|
||||||
<i class="ph ph-arrows-clockwise text-sm" id="header-sync-icon"></i><span class="font-pixel text-[10px]">Sync</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
<div class="header-sync-group flex items-center">
|
||||||
|
<button id="header-sync" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors" data-tip="sync now">
|
||||||
|
<i class="ph ph-arrows-clockwise text-sm" id="header-sync-icon"></i><span class="font-pixel text-[10px]">Sync</span>
|
||||||
|
</button>
|
||||||
|
<button id="header-autosync" class="flex items-center justify-center px-2 py-1.5 text-xs border border-t-border transition-colors" data-tip="auto-sync">
|
||||||
|
<span id="header-autosync-dot" class="header-autosync-dot"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button id="calendar-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">
|
<button id="calendar-btn" class="flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors">
|
||||||
@@ -1815,6 +1830,15 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
<button id="relay-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plugs-connected text-sm"></i><span class="font-pixel text-[10px]">Test</span></button>
|
<button id="relay-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plugs-connected text-sm"></i><span class="font-pixel text-[10px]">Test</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="relay-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div>
|
<div id="relay-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div>
|
||||||
|
<div class="border-t border-t-border pt-4 mt-4">
|
||||||
|
<div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim mb-2"><i class="ph ph-timer text-xs"></i> sync frequency</div>
|
||||||
|
<p class="text-[10px] text-t-dim leading-relaxed mb-2">How often the background sync polls the relay + CalDAV for new messages, collab updates and sync events. Lower = snappier, higher = less network traffic.</p>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<label class="text-xs text-t-muted flex-1" for="setting-sync-interval">Poll every</label>
|
||||||
|
<input id="setting-sync-interval" type="number" min="5" max="3600" step="1" class="bg-t-bg border border-t-border text-t-bright text-xs font-mono px-2 py-1 rounded-sm focus:border-t-accent focus:outline-none" style="width:72px;text-align:right;">
|
||||||
|
<span class="text-xs text-t-dim">sec</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="settings-panel-behavior" class="hidden space-y-5">
|
<div id="settings-panel-behavior" class="hidden space-y-5">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ $('caldav-save')?.addEventListener('click', async () => {
|
|||||||
$('caldav-sync-now')?.addEventListener('click', () => triggerSync());
|
$('caldav-sync-now')?.addEventListener('click', () => triggerSync());
|
||||||
|
|
||||||
export async function triggerSync() {
|
export async function triggerSync() {
|
||||||
|
pulseAutosyncDot();
|
||||||
const btn = $('cal-sync');
|
const btn = $('cal-sync');
|
||||||
if (btn) btn.style.opacity = '0.5';
|
if (btn) btn.style.opacity = '0.5';
|
||||||
const headerIcon = $('header-sync-icon');
|
const headerIcon = $('header-sync-icon');
|
||||||
@@ -285,6 +286,19 @@ export async function loadRelaySettings() {
|
|||||||
keyInput.value = s.key || '';
|
keyInput.value = s.key || '';
|
||||||
keyInput.placeholder = 'shared secret';
|
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);
|
$('cal-caldav-settings')?.addEventListener('click', openCaldavSettings);
|
||||||
@@ -350,6 +364,7 @@ function drainSyncEvents(relayRes) {
|
|||||||
async function backgroundSync() {
|
async function backgroundSync() {
|
||||||
if (state.bgSyncRunning) return;
|
if (state.bgSyncRunning) return;
|
||||||
state.bgSyncRunning = true;
|
state.bgSyncRunning = true;
|
||||||
|
pulseAutosyncDot();
|
||||||
try {
|
try {
|
||||||
const headers = { 'X-CSRF-Token': getCsrf() };
|
const headers = { 'X-CSRF-Token': getCsrf() };
|
||||||
const [res, relayRes] = await Promise.all([
|
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
|
// Start background sync after app is ready
|
||||||
let bgSyncInterval = null;
|
let bgSyncInterval = null;
|
||||||
export function startBackgroundSync() {
|
export function startBackgroundSync() {
|
||||||
if (bgSyncInterval) return;
|
if (bgSyncInterval) return;
|
||||||
|
if (!isAutoSyncEnabled()) { updateAutosyncButton(); return; }
|
||||||
backgroundSync();
|
backgroundSync();
|
||||||
bgSyncInterval = setInterval(backgroundSync, 45000); // every 45 seconds
|
bgSyncInterval = setInterval(backgroundSync, getSyncIntervalSec() * 1000);
|
||||||
|
updateAutosyncButton();
|
||||||
}
|
}
|
||||||
export function stopBackgroundSync() {
|
export function stopBackgroundSync() {
|
||||||
if (bgSyncInterval) { clearInterval(bgSyncInterval); bgSyncInterval = null; }
|
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();
|
||||||
@@ -27,7 +27,13 @@ let collabByUid = new Map();
|
|||||||
// card_id (number) -> true for cards with foreign edits we haven't opened yet
|
// card_id (number) -> true for cards with foreign edits we haven't opened yet
|
||||||
let unreadCards = new Set();
|
let unreadCards = new Set();
|
||||||
let pollHandle = null;
|
let pollHandle = null;
|
||||||
const POLL_INTERVAL_MS = 30000;
|
// The core CalDAV/relay sync cadence (Settings → Relay → Sync frequency,
|
||||||
|
// default 15s) also drives the collab poll. Users can dial up or down
|
||||||
|
// without touching code. Defaults + clamp mirror the core helper.
|
||||||
|
function getPollIntervalMs() {
|
||||||
|
const v = parseInt(localStorage.getItem('hyprcrd-sync-interval-sec'));
|
||||||
|
return (Number.isFinite(v) && v >= 5 ? v : 15) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 4: edit locks (now exclusive — server-enforced)
|
// Phase 4: edit locks (now exclusive — server-enforced)
|
||||||
// card_uid -> { bucket_uid, holder_username, expires_at }
|
// card_uid -> { bucket_uid, holder_username, expires_at }
|
||||||
@@ -69,6 +75,7 @@ async function pollOnce() {
|
|||||||
if (document.getElementById('modal')?.classList.contains('open')) return;
|
if (document.getElementById('modal')?.classList.contains('open')) return;
|
||||||
try {
|
try {
|
||||||
pollLocked = true;
|
pollLocked = true;
|
||||||
|
hyprcrd.hooks?.emit?.('sync.pulse');
|
||||||
const res = await hyprcrd.api('api/collab-buckets/poll', { method: 'POST' });
|
const res = await hyprcrd.api('api/collab-buckets/poll', { method: 'POST' });
|
||||||
if (!res?.ok) return;
|
if (!res?.ok) return;
|
||||||
if (res.self) window._collabSelf = res.self;
|
if (res.self) window._collabSelf = res.self;
|
||||||
@@ -166,12 +173,23 @@ function showQueuedToast(message) {
|
|||||||
|
|
||||||
function startPolling() {
|
function startPolling() {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
pollHandle = setInterval(pollOnce, POLL_INTERVAL_MS);
|
pollHandle = setInterval(pollOnce, getPollIntervalMs());
|
||||||
}
|
}
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
if (pollHandle) { clearInterval(pollHandle); pollHandle = null; }
|
if (pollHandle) { clearInterval(pollHandle); pollHandle = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restart when the user changes the sync interval in settings.
|
||||||
|
hyprcrd.hooks?.on?.('sync.interval.changed', () => {
|
||||||
|
if (pollHandle) startPolling();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pause / resume alongside the header autosync toggle.
|
||||||
|
hyprcrd.hooks?.on?.('autosync.changed', (on) => {
|
||||||
|
if (on) startPolling();
|
||||||
|
else stopPolling();
|
||||||
|
});
|
||||||
|
|
||||||
// ── Bucket bar decoration ─────────────────────────────────────────
|
// ── Bucket bar decoration ─────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -974,7 +992,9 @@ async function init() {
|
|||||||
decorateBucketBar();
|
decorateBucketBar();
|
||||||
decorateBoardLocks();
|
decorateBoardLocks();
|
||||||
decorateBoardUnread();
|
decorateBoardUnread();
|
||||||
startPolling();
|
// Respect the header autosync toggle — if the user has paused it,
|
||||||
|
// skip the recurring poll entirely. Hook above restarts when toggled.
|
||||||
|
if (localStorage.getItem('hyprcrd-autosync-enabled') !== '0') startPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defer init until the CSRF token is live — `checkAuth()` sets it async on
|
// Defer init until the CSRF token is live — `checkAuth()` sets it async on
|
||||||
|
|||||||
Reference in new issue
Block a user