diff --git a/relay/inc/routes/collab-cards.php b/relay/inc/routes/collab-cards.php index 6a85630..f940db8 100644 --- a/relay/inc/routes/collab-cards.php +++ b/relay/inc/routes/collab-cards.php @@ -105,6 +105,47 @@ if ($route === '/api/collab-cards' && $method === 'POST') { // ── GET: pull deltas since a given timestamp ───────────────────── +// ── Session-auth summary for the relay UI ───────────────────── +// Lets the relay's web UI surface collab bucket activity to the +// logged-in tenant without exposing the per-client encryption dance: +// decrypts payloads with the inter-tenant key (we're authorizing via +// the PHP session, so there's no per-request client secret). Returns +// just title + small body preview + author + bucket so the UI can list +// recent activity like an inbox. +if ($route === '/api/collab-inbox' && $method === 'GET' && $sessionTenant) { + $interKey = getInterTenantKey($db, $MASTER_KEY); + if (!$interKey) jsonResponse(['error' => 'relay super-shared key not set'], 500); + $stmt = $db->prepare(' + SELECT c.bucket_uid, c.card_uid, c.author_username, c.payload, c.updated_at, c.deleted, + b.name AS bucket_name, b.color AS bucket_color + FROM collab_cards c + INNER JOIN collab_bucket_members m ON m.bucket_uid = c.bucket_uid + LEFT JOIN collab_buckets b ON b.uid = c.bucket_uid + WHERE m.tenant_id = :tid AND c.deleted = 0 + ORDER BY c.updated_at DESC + LIMIT 200 + '); + $stmt->execute(['tid' => $sessionTenant['id']]); + $out = []; + foreach ($stmt->fetchAll() as $row) { + $plain = clientDecrypt($row['payload'] ?? '', $interKey); + $fields = $plain ? (json_decode($plain, true) ?: []) : []; + $body = (string)($fields['body'] ?? ''); + $out[] = [ + 'bucket_uid' => $row['bucket_uid'], + 'bucket_name' => $row['bucket_name'], + 'bucket_color' => $row['bucket_color'] ?? '#38bdf8', + 'card_uid' => $row['card_uid'], + 'author_username' => $row['author_username'], + 'title' => (string)($fields['title'] ?? ''), + 'preview' => mb_substr(trim(strip_tags($body)), 0, 140), + 'updated_at' => $row['updated_at'], + 'mine' => $row['author_username'] === $sessionTenant['username'], + ]; + } + jsonResponse(['ok' => true, 'items' => $out]); +} + if ($route === '/api/collab-cards' && $method === 'GET') { if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401); if (isRateLimited($db, 'collab-cards-get', 120)) jsonResponse(['error' => 'rate limited'], 429); diff --git a/relay/ui/app.js b/relay/ui/app.js index 0754255..d136b8d 100644 --- a/relay/ui/app.js +++ b/relay/ui/app.js @@ -93,6 +93,7 @@ function showView(name) { loadTenants().then(() => populateRecipientPicker()); loadInbox(); loadOutbox(); + loadCollabInbox(); startInboxPoll(); $('compose-body')?.focus(); } else { @@ -126,7 +127,11 @@ function startInboxPoll() { stopInboxPoll(); inboxPollHandle = setInterval(() => { if (inboxBusy) return; - if (!$('view-compose')?.classList.contains('hidden')) { loadInbox({ quiet: true }); loadOutbox({ quiet: true }); } + if (!$('view-compose')?.classList.contains('hidden')) { + loadInbox({ quiet: true }); + loadOutbox({ quiet: true }); + loadCollabInbox({ quiet: true }); + } }, 15000); } function stopInboxPoll() { @@ -594,6 +599,42 @@ async function loadOutbox({ quiet = false } = {}) { `).join(''); } +// ── Shared buckets / collab inbox ── +let lastCollabJson = ''; +async function loadCollabInbox({ quiet = false } = {}) { + const list = $('shared-list'); + const countEl = $('shared-count'); + if (!list) return; + const res = await api('api/collab-inbox'); + const items = res?.items || []; + const json = JSON.stringify(items.map(c => c.card_uid + ':' + c.updated_at)); + if (quiet && json === lastCollabJson) return; + lastCollabJson = json; + if (countEl) countEl.textContent = items.length; + if (!items.length) { + list.innerHTML = ` +