collab attachments, multi-day events, calendar improvements
- Collab buckets sync attachments alongside cards (images/PDFs/audio). New /api/collab-attachments endpoint on relay, encrypted at rest, bucket-scoped for members. - CalDAV multi-day events via **Bis:** body field. Calendar renders event on every day in range with day counter (1/6, 2/6, ...). - Calendar preserves month across close/reopen (only jumps to today on first open). - Calendar title: centered nav group with [MM-DD] Mon Y [KW-KW] — dimmed brackets, fixed width so nothing wiggles. - Editor: scratchpad uses custom confirm, autobackup-draft dialog replaced with custom styled dialog. - Scanlines theme feature removed (performance issue). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
002562b1d3
commit
ca47052768
11 files changed
+254
-64
No files matched your search
@@ -493,6 +493,25 @@ function collabSyncCards(PDO $db): array {
|
||||
'push' => $res['updated_at'] ?? $card['updated_at'],
|
||||
]);
|
||||
$stats['pushed']++;
|
||||
|
||||
// Push referenced local attachments to the bucket (encrypted,
|
||||
// member-scoped). Extract `uploads/<hex>.<ext>` from the body.
|
||||
$body = $card['body'] ?? '';
|
||||
if ($body && preg_match_all('#uploads/([a-f0-9]+\.\w+)#', $body, $fm)) {
|
||||
foreach (array_unique($fm[1]) as $fn) {
|
||||
$localPath = UPLOAD_DIR . '/' . $fn;
|
||||
if (!is_file($localPath)) continue;
|
||||
$bytes = file_get_contents($localPath);
|
||||
if ($bytes === false) continue;
|
||||
$encoded = collabEncryptPayload(['raw' => base64_encode($bytes)]);
|
||||
if ($encoded === null) continue;
|
||||
collabRelayCall('/api/collab-attachments', 'POST', [
|
||||
'bucket_uid' => $card['bucket_uid'],
|
||||
'filename' => $fn,
|
||||
'data' => $encoded,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Pull deltas ─────────────────────────────────────
|
||||
@@ -628,6 +647,28 @@ function collabSyncCards(PDO $db): array {
|
||||
]);
|
||||
$stats['pulled']++;
|
||||
if ($foreignAuthor) $unreadBuckets[$c['bucket_uid']] = true;
|
||||
|
||||
// Fetch missing attachments referenced in the body.
|
||||
$pulledBody = $fields['body'] ?? '';
|
||||
if ($pulledBody && preg_match_all('#uploads/([a-f0-9]+\.\w+)#', $pulledBody, $fm)) {
|
||||
$uploadDir = UPLOAD_DIR;
|
||||
if (!is_dir($uploadDir)) @mkdir($uploadDir, 0755, true);
|
||||
foreach (array_unique($fm[1]) as $fn) {
|
||||
$dest = $uploadDir . '/' . $fn;
|
||||
if (file_exists($dest)) continue; // already have it
|
||||
$attRes = collabRelayCall('/api/collab-attachments?bucket_uid=' . urlencode($c['bucket_uid']) . '&filename=' . urlencode($fn));
|
||||
if (!($attRes['ok'] ?? false)) continue;
|
||||
$atts = $attRes['attachments'] ?? [];
|
||||
foreach ($atts as $a) {
|
||||
if (($a['filename'] ?? '') !== $fn) continue;
|
||||
$decoded = collabDecryptPayload($a['data'] ?? '');
|
||||
if ($decoded && isset($decoded['raw'])) {
|
||||
$bytes = base64_decode($decoded['raw']);
|
||||
if ($bytes !== false) file_put_contents($dest, $bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Bump `collab_buckets.updated_at` for every bucket that received
|
||||
|
||||
@@ -58,4 +58,25 @@ describe('Calendar', () => {
|
||||
await sleep(150);
|
||||
expect($('#calendar')).toBeHidden();
|
||||
});
|
||||
|
||||
it('preserves month across close/reopen', async () => {
|
||||
// Open calendar and navigate forward 3 months
|
||||
click('#calendar-btn');
|
||||
await sleep(200);
|
||||
click('#cal-next');
|
||||
click('#cal-next');
|
||||
click('#cal-next');
|
||||
await sleep(200);
|
||||
const navigatedTitle = $('#cal-title').textContent;
|
||||
|
||||
// Close
|
||||
click('#cal-close');
|
||||
await sleep(150);
|
||||
expect($('#calendar')).toBeHidden();
|
||||
|
||||
// Reopen — must stay on the navigated month, not jump to today
|
||||
click('#calendar-btn');
|
||||
await sleep(200);
|
||||
expect($('#cal-title').textContent).toBe(navigatedTitle);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user