v2.0.10 — snappier boot + non-blocking relay push on save

DB
- Six indices on the cards table (position+created_at, bucket_id,
  parent_id, updated_at, caldav_uid, relay_uid). The /api/cards
  ORDER BY position was a full-table scan + sort on every board
  reload; now it's an index walk.
- PRAGMA synchronous=NORMAL (safe under WAL) + temp_store=MEMORY.

Save path
- pushCardToRelayRead and deleteCardFromRelayRead now run AFTER the
  response is flushed via fastcgi_finish_request (falls back to
  ignore_user_abort + flush on non-FPM setups). The user's save no
  longer blocks on the relay round-trip.
- cURL timeouts dropped from 10s/30s to 5s/10s, plus a 2s
  connect-timeout. Worst-case on a dead relay goes from ~160s down
  to ~15s — and now that worst case is in the background anyway.

Client
- loadCards phase-2 batches bumped from 25 to 500 cards per fetch.
  For 300 cards that's 1 HTTP trip instead of 12 — spinner wall time
  drops from ~3s to ~0.25s.
- renderBoard() is now requestAnimationFrame-coalesced. The boot
  sequence calls renderBoard from cache paint, meta paint, body
  paint, and the cards.reloaded plugin hook; they used to each
  trigger an innerHTML swap + layout pass. Now they collapse into
  one actual render per frame. Callers that really need a sync
  render (rare) can use the new renderBoardSync().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-18 20:41:48 +02:00
1 parent 87ebcd156b
commit 2d58cd993e
7 files changed
+124 -28

No files matched your search

+8 -5
View File
@@ -108,11 +108,14 @@ async function loadCards() {
updateSortUI();
renderBoard();
// Phase 2: Load full cards in batches → replace spinners with content
const batchSize = 25;
for (let i = 0; i < meta.length; i += batchSize) {
const batch = meta.slice(i, i + batchSize);
const ids = batch.map(c => c.id).join(',');
// Phase 2: Load full card bodies. One fetch up to the ID cap — with
// the /api/cards(?ids=) endpoint now indexed on cards.position +
// created_at a single big batch is faster than many small ones, and
// shorter perceived spinner time matters more than memory. Fall back
// to smaller batches only if the URL would blow past ~2000 chars.
const IDS_MAX_PER_FETCH = 500;
for (let i = 0; i < meta.length; i += IDS_MAX_PER_FETCH) {
const ids = meta.slice(i, i + IDS_MAX_PER_FETCH).map(c => c.id).join(',');
const full = await api('api/cards?ids=' + ids);
if (!full) continue;
for (const card of full) {
+22
View File
@@ -493,8 +493,30 @@ export function renderCardHtml(c, inGroup = false, recent = []) {
}
// ── Render ──
//
// Public renderBoard() is coalesced via requestAnimationFrame: multiple
// callers in the same tick (cache paint → meta paint → body paint →
// plugin-hook re-render → ...) collapse into a single actual render on
// the next frame. Dropped the "three innerHTML swaps per reload" cost
// that showed up as the "Gekringel" during boot. Callers that need the
// DOM in a specific state before doing something else can call
// renderBoardSync() explicitly — but in practice every existing caller
// just wants the board to reflect the latest state.
let rafScheduled = 0;
export function renderBoard() {
if (rafScheduled) return;
rafScheduled = requestAnimationFrame(() => {
rafScheduled = 0;
renderBoardSync();
});
}
export function renderBoardSync() {
if (rafScheduled) { cancelAnimationFrame(rafScheduled); rafScheduled = 0; }
renderBoardImpl();
}
function renderBoardImpl() {
// Diff state.cards against the last seen ids so we can flag arrivals
// as "unseen". The very first renderBoard runs with state.cards = []
// (initial empty state, before the API has returned), so we wait