v2.0.9 — sync toast split, relay group chat UI, find/replace flaky fix
- sync: the post-sync toast lumped relay pulls into the CalDAV stats and always labelled the banner "CalDAV". Split into two separate toasts (CalDAV cloud-arrow + Relay broadcast icon), each only shown when its numbers are non-zero. Banners now stack vertically so both can appear together on a sync that pulled on both sides. - relay UI: the session chat view was 1:1-only. Group rooms (which the client API already supports) now surface as first-class threads in the sidebar — sorted chronologically with 1:1 peers, tagged "ROOM", with a users-three icon. Sending posts to `room_uid` instead of `recipient`; history shows the sender name above each incoming message so it's clear who's talking in the room. Relay backend: `/api/chat/threads` returns rooms alongside 1:1 threads; `/api/chat/history` accepts `?room_uid=`; `/api/chat/send` accepts `room_uid` in the body. - tests: bump "replace all updates card bodies" poll budget from 4s to 10s and poll both updated cards. Flaky on cold runs because a fresh PHP worker + SQLite WAL warm-up plus two sequential PATCH calls can just barely exceed 4 seconds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4aca21812e
commit
87ebcd156b
6 files changed
+175
-51
No files matched your search
@@ -59,14 +59,16 @@ export async function triggerSync() {
|
||||
if (res.ok) {
|
||||
const s = res.stats || {};
|
||||
const rp = relayRes?.pulled || 0;
|
||||
const total = { pushed: s.pushed || 0, pulled: (s.pulled || 0) + rp, deleted: s.deleted || 0 };
|
||||
const caldavStats = { pushed: s.pushed || 0, pulled: s.pulled || 0, deleted: s.deleted || 0 };
|
||||
const relayStats = { pulled: rp };
|
||||
const allLog = res.log || [];
|
||||
const hasErrors = allLog.some(l => l.status === 'error');
|
||||
const msg = `pushed:${total.pushed} pulled:${total.pulled} del:${total.deleted}${rp ? ' relay:' + rp : ''}`;
|
||||
const msg = `pushed:${caldavStats.pushed} pulled:${caldavStats.pulled + rp} del:${caldavStats.deleted}${rp ? ' relay:' + rp : ''}`;
|
||||
if (syncStatus) { syncStatus.textContent = msg; syncStatus.style.color = hasErrors ? 'var(--t-danger)' : 'var(--t-accent)'; }
|
||||
await window._reloadAll?.();
|
||||
showLog(allLog);
|
||||
showSyncNotification(total);
|
||||
showSyncNotification('CalDAV', 'ph-cloud-arrow-down', caldavStats);
|
||||
showSyncNotification('Relay', 'ph-broadcast', relayStats);
|
||||
} else {
|
||||
if (syncStatus) { syncStatus.textContent = res.error || 'sync failed'; syncStatus.style.color = 'var(--t-danger)'; }
|
||||
}
|
||||
@@ -99,19 +101,29 @@ export function autoSync() {
|
||||
|
||||
// ── Sync notifications ──────────────────────────────────────────
|
||||
|
||||
function showSyncNotification(stats) {
|
||||
/** Per-source sync toast. Caller picks the label ("CalDAV" vs "Relay")
|
||||
* and icon so the user knows which pipeline just finished, instead of
|
||||
* the old always-CalDAV banner that lumped relay traffic in silently. */
|
||||
let syncNoteOffset = 0;
|
||||
function showSyncNotification(label, icon, stats) {
|
||||
const s = stats || {};
|
||||
const parts = [];
|
||||
if (s.pulled) parts.push(`${s.pulled} received`);
|
||||
if (s.pushed) parts.push(`${s.pushed} sent`);
|
||||
if (s.pulled) parts.push(`${s.pulled} received`);
|
||||
if (s.pushed) parts.push(`${s.pushed} sent`);
|
||||
if (s.deleted) parts.push(`${s.deleted} deleted`);
|
||||
if (!parts.length) return;
|
||||
|
||||
const bottom = 60 + syncNoteOffset * 48;
|
||||
syncNoteOffset++;
|
||||
const note = document.createElement('div');
|
||||
note.style.cssText = 'position:fixed;bottom:60px;right:16px;z-index:200;background:var(--t-surface);border:1px solid var(--t-accent);border-radius:4px;padding:10px 14px;box-shadow:0 4px 20px rgba(0,0,0,0.4);max-width:300px;';
|
||||
note.innerHTML = `<div style="font-family:var(--t-font-pixel);font-size:10px;color:var(--t-accent);display:flex;align-items:center;gap:6px;"><i class="ph ph-cloud-arrow-down"></i>CalDAV: ${parts.join(', ')}</div>`;
|
||||
note.style.cssText = `position:fixed;bottom:${bottom}px;right:16px;z-index:200;background:var(--t-surface);border:1px solid var(--t-accent);border-radius:4px;padding:10px 14px;box-shadow:0 4px 20px rgba(0,0,0,0.4);max-width:300px;`;
|
||||
note.innerHTML = `<div style="font-family:var(--t-font-pixel);font-size:10px;color:var(--t-accent);display:flex;align-items:center;gap:6px;"><i class="ph ${icon}"></i>${label}: ${parts.join(', ')}</div>`;
|
||||
document.body.appendChild(note);
|
||||
setTimeout(() => { note.style.transition = 'opacity 0.3s'; note.style.opacity = '0'; setTimeout(() => note.remove(), 350); }, 4000);
|
||||
setTimeout(() => {
|
||||
note.style.transition = 'opacity 0.3s';
|
||||
note.style.opacity = '0';
|
||||
setTimeout(() => { note.remove(); syncNoteOffset = Math.max(0, syncNoteOffset - 1); }, 350);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
// One toast per incoming sync event: "alice synced: Card Title".
|
||||
|
||||
@@ -78,14 +78,17 @@ describe('Search: Find/Replace', () => {
|
||||
const okBtn = document.getElementById('dialog-ok');
|
||||
if (okBtn) click(okBtn);
|
||||
|
||||
// Poll until the replace has actually persisted
|
||||
// Poll until both replaces have persisted. Budget is generous
|
||||
// (~10s) because a cold PHP run plus two sequential PATCH calls
|
||||
// can easily blow past the original 4-second window — the retry
|
||||
// always passes because opcache + SQLite WAL are warm by then.
|
||||
let updated1, updated2;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await sleep(100);
|
||||
updated1 = await getCard(c1.id);
|
||||
if (updated1?.body === 'goodbye world goodbye') break;
|
||||
updated2 = await getCard(c2.id);
|
||||
if (updated1?.body === 'goodbye world goodbye' && updated2?.body === 'goodbye there') break;
|
||||
}
|
||||
updated2 = await getCard(c2.id);
|
||||
expect(updated1.body).toBe('goodbye world goodbye');
|
||||
expect(updated2.body).toBe('goodbye there');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user