plugin(collab-buckets): collaborative buckets via relay
End-to-end shared buckets between relay tenants. Bucket metadata + members sync through new relay routes (collab-buckets, collab-cards, collab-locks); card content rides the existing inter-tenant encryption hop. Edit locks, ownership transfer, LWW conflict stashing, undo for local-bucket destructive actions, "Make collaborative…" upgrade path, and a resilient outbox for relay-down create attempts. See plugins/collab-buckets/README.md and TODO.md for the full picture. Core hooks added so the plugin can integrate without monkey-patching: - buckets.js context menu: collab-aware rename/color/delete + Members & owner / Make collaborative entries - editor.js dispatches hyprcrd:editor-open / hyprcrd:editor-close for the per-card edit-lock heartbeat - app.js exposes window.state for the plugin's MutationObserver - index.php POST/PATCH/DELETE on /api/buckets refuse to mutate collab buckets directly (must go through the relay) - Database.php adds buckets.collab_uid column + unique partial index - import handler now restores color and collab_uid (was silently dropping color, which caused the test runner's auto-restore to wipe custom bucket colors) - relay bootstrap creates collab_buckets, collab_bucket_members, collab_cards, collab_card_locks tables and prunes tombstones / expired locks on every request Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a29708df85
commit
26ffa0e71f
16 files changed
+3017
-13
No files matched your search
+28
-3
@@ -341,19 +341,38 @@ if (str_starts_with($route, '/api/')) {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$name = trim($input['name'] ?? '');
|
||||
if ($name === '') { http_response_code(400); echo json_encode(['error' => 'name is required']); exit; }
|
||||
// Normal POST creates a local bucket. Collab buckets go through
|
||||
// /api/collab-buckets (plugin-owned) and can't be created here.
|
||||
$db->prepare('INSERT INTO buckets (name) VALUES (:name)')->execute(['name' => $name]);
|
||||
echo json_encode($db->query('SELECT * FROM buckets WHERE id = ' . $db->lastInsertId())->fetch()); exit;
|
||||
}
|
||||
|
||||
if (preg_match('#^/api/buckets/(\d+)$#', $route, $m) && $method === 'PATCH') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
// Collab buckets: name and color must go through the relay plugin so
|
||||
// all members see the update. Only position (local reorder) is allowed.
|
||||
$b = $db->prepare('SELECT collab_uid FROM buckets WHERE id = :id');
|
||||
$b->execute(['id' => $m[1]]);
|
||||
$existing = $b->fetch();
|
||||
$isCollab = $existing && !empty($existing['collab_uid']);
|
||||
$allowed = $isCollab ? ['position'] : ['name', 'position', 'color'];
|
||||
$fields = []; $params = ['id' => $m[1]];
|
||||
foreach (['name', 'position', 'color'] as $f) { if (array_key_exists($f, $input)) { $fields[] = "$f = :$f"; $params[$f] = $f === 'color' ? validColor($input[$f]) : $input[$f]; } }
|
||||
foreach ($allowed as $f) { if (array_key_exists($f, $input)) { $fields[] = "$f = :$f"; $params[$f] = $f === 'color' ? validColor($input[$f]) : $input[$f]; } }
|
||||
if ($fields) $db->prepare('UPDATE buckets SET ' . implode(', ', $fields) . ' WHERE id = :id')->execute($params);
|
||||
$b = $db->prepare('SELECT * FROM buckets WHERE id = :id'); $b->execute(['id' => $m[1]]); echo json_encode($b->fetch()); exit;
|
||||
}
|
||||
|
||||
if (preg_match('#^/api/buckets/(\d+)$#', $route, $m) && $method === 'DELETE') {
|
||||
// Collab buckets must go through /api/collab-buckets/{uid} so the relay
|
||||
// and plugin mirror stay consistent. Refuse deletion here.
|
||||
$b = $db->prepare('SELECT collab_uid FROM buckets WHERE id = :id');
|
||||
$b->execute(['id' => $m[1]]);
|
||||
$existing = $b->fetch();
|
||||
if ($existing && !empty($existing['collab_uid'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'collab buckets must be deleted via /api/collab-buckets/' . $existing['collab_uid']]);
|
||||
exit;
|
||||
}
|
||||
$db->prepare('UPDATE cards SET bucket_id = NULL WHERE bucket_id = :id')->execute(['id' => $m[1]]);
|
||||
$db->prepare('DELETE FROM buckets WHERE id = :id')->execute(['id' => $m[1]]);
|
||||
echo json_encode(['ok' => true]); exit;
|
||||
@@ -625,8 +644,14 @@ if (str_starts_with($route, '/api/')) {
|
||||
$found = $existing->fetchColumn();
|
||||
if ($found) { $bucketMap[$oldId] = $found; continue; }
|
||||
}
|
||||
$stmt = $db->prepare('INSERT INTO buckets (name, position, created_at) VALUES (:name, :position, :created_at)');
|
||||
$stmt->execute(['name' => $b['name'], 'position' => $b['position'] ?? 0, 'created_at' => $b['created_at'] ?? date('Y-m-d H:i:s')]);
|
||||
$stmt = $db->prepare('INSERT INTO buckets (name, position, color, collab_uid, created_at) VALUES (:name, :position, :color, :collab_uid, :created_at)');
|
||||
$stmt->execute([
|
||||
'name' => $b['name'],
|
||||
'position' => $b['position'] ?? 0,
|
||||
'color' => $b['color'] ?? '#64748b',
|
||||
'collab_uid' => $b['collab_uid'] ?? null,
|
||||
'created_at' => $b['created_at'] ?? date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$bucketMap[$oldId] = $db->lastInsertId();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user