sync-event toasts, image pull ordering, stale banner field diff
- replace chat-bot sync notifications with a dedicated relay_sync_events queue: POST/GET /api/sync-events, drained by /api/relay/poll and surfaced as stacking toasts via caldav.js. Old /api/chat/bot removed, bot=1 rows purged on bootstrap, orange bot rendering dropped from relay UI. Chat thread stays clean. - collab pull: fetch attachments BEFORE applying the body so the moment state.cards reflects the new reference the file is on disk. Fixes the "preview shows broken/missing image, editor shows it" race where the browser cached the 404 between attachment landing and user re-render. - detail stale banner now compares title/body/color/tags in addition to updated_at — robust against second-granularity timestamp collisions and any other case where updated_at doesn't tick but content differs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
26f95ba079
commit
1966f8f4c0
10 files changed
+170
-75
No files matched your search
@@ -190,6 +190,7 @@ export async function triggerSync() {
|
||||
const relayRes = await fetch('api/relay/poll', {
|
||||
method: 'POST', headers: { 'X-CSRF-Token': getCsrf() },
|
||||
}).then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
drainSyncEvents(relayRes);
|
||||
|
||||
if (res.ok) {
|
||||
const s = res.stats || {};
|
||||
@@ -265,6 +266,7 @@ $('relay-test')?.addEventListener('click', async () => {
|
||||
if (res?.ok) {
|
||||
setRelayStatus(`ok — ${res.pulled} pulled`, 'success');
|
||||
if (res.pulled) await window._reloadAll?.();
|
||||
drainSyncEvents(res);
|
||||
} else {
|
||||
const detail = res?.sent_username ? ` (sent username: ${res.sent_username})` : '';
|
||||
setRelayStatus((res?.error || 'failed') + detail, 'error');
|
||||
@@ -315,6 +317,33 @@ function showSyncNotification(stats) {
|
||||
setTimeout(() => { note.style.transition = 'opacity 0.3s'; note.style.opacity = '0'; setTimeout(() => note.remove(), 350); }, 4000);
|
||||
}
|
||||
|
||||
// One toast per incoming sync event: "alice synced: Card Title".
|
||||
// Stacks downward so a burst of multiple doesn't overlay each other.
|
||||
let syncEventToastOffset = 0;
|
||||
function showSyncEventToast(ev) {
|
||||
const body = String(ev?.body || '').trim();
|
||||
if (!body) return;
|
||||
const bottom = 110 + syncEventToastOffset * 48;
|
||||
syncEventToastOffset++;
|
||||
const note = document.createElement('div');
|
||||
note.style.cssText = `position:fixed;bottom:${bottom}px;right:16px;z-index:200;background:var(--t-surface);border:1px solid var(--t-accent2);border-radius:4px;padding:9px 14px;box-shadow:0 4px 20px rgba(0,0,0,0.4);max-width:320px;`;
|
||||
note.innerHTML = `<div style="font-family:var(--t-font-pixel);font-size:10px;color:var(--t-accent2);display:flex;align-items:center;gap:6px;"><i class="ph ph-check-circle"></i>${body.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}</div>`;
|
||||
document.body.appendChild(note);
|
||||
setTimeout(() => {
|
||||
note.style.transition = 'opacity 0.3s';
|
||||
note.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
note.remove();
|
||||
syncEventToastOffset = Math.max(0, syncEventToastOffset - 1);
|
||||
}, 350);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function drainSyncEvents(relayRes) {
|
||||
const events = Array.isArray(relayRes?.sync_events) ? relayRes.sync_events : [];
|
||||
for (const ev of events) showSyncEventToast(ev);
|
||||
}
|
||||
|
||||
async function backgroundSync() {
|
||||
if (state.bgSyncRunning) return;
|
||||
state.bgSyncRunning = true;
|
||||
@@ -331,6 +360,7 @@ async function backgroundSync() {
|
||||
await window._reloadAll?.();
|
||||
showSyncNotification(total);
|
||||
}
|
||||
drainSyncEvents(relayRes);
|
||||
} catch {} finally {
|
||||
state.bgSyncRunning = false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user