* version: every span that shows v<APP_VERSION> in the page chrome now
carries data-app-version, and checkAuth() patches them in from the
/api/auth/status response so the GUI version doesn't go stale when
PHP is cached behind anything that revalidates lazily.
* undo: tailwind v4 compiles `-translate-x-1/2` to the *new* CSS
`translate` property, which then stacks with the inline `transform`
the toast set — the box ended up shifted ~ -100% and looked left-
anchored. Drop the tailwind utility, set `translate` directly inline
so there's one source of truth for the offset.
* editor: ctrl/cmd+enter while the editor modal is open now invokes
saveCard() (= save + close), pairing with the existing ctrl+s
(save only) and ctrl+shift+s (save+close) shortcuts.
* collab buckets: drop the inset left edge from .bucket-tab-collab and
the matching pulse-keyframe shadow — the bar tab now reads as a
normal bucket tab, just with the unread dot + 3-cycle pulse on update.
* collab create dialog: drop the box border / background from
.collab-toggle-row and .collab-members so the toggle and the user
list sit directly on the modal background instead of looking like
nested input boxes.
* test progress strip: ensureProgressBar() now also pushes the body
down by 30px AND shrinks it via height: calc(100vh - 30px) so the
fixed-top bar doesn't clip the footer (body is h-screen +
overflow-hidden in the core layout). hideProgress() clears both
inline styles when the run finishes.
chore: bump version to 0.9.2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous lock implementation was advisory only — the relay's lock
table PK was (bucket_uid, card_uid, holder_id), so multiple users could
"hold" the same card simultaneously and the plugin only used the locks
to draw a small badge. Concurrent writers raced through LWW, and an
unsaved editor on User A could be silently overwritten when User B
saved (the symptom: cards appearing to "disappear" from User A's view
once their version was clobbered by an LWW pull).
Now an exclusive, server-enforced lock:
* relay schema: collab_card_locks PK is (bucket_uid, card_uid). One
lock per card. Migration drops the old 3-column-PK table on the
next bootstrap (locks are 60s ephemeral, no data to preserve).
* /api/collab-locks POST: returns 409 + holder info when another
tenant already holds it. Same holder refreshes the TTL.
* /api/collab-cards POST/DELETE: refuse with 423 + locked_by when a
foreign tenant holds the lock. The plugin's local edit is preserved
— the push just stays queued and retries on the next poll until
the lock expires or is released. Tombstones are gated the same way
so a card can't be deleted under the holder's editor.
* Plugin push phase: tracks blocked count + holder names, surfaces
them in the poll response so the frontend can toast the user.
Frontend lock-on-first-keystroke flow:
* The lock is no longer acquired on editor-open — opening for read
doesn't lock anything. On the FIRST input event in #inp-title or
#editor-body the plugin tries to acquire. On success, heartbeat
every 30 s; on 409, immediately enter editor takeover.
* Takeover mode: title + body inputs disabled, all three save
buttons get a capture-phase click blocker (stopImmediatePropagation
to defeat the editor's own listener), a window-level capture-phase
keydown blocker swallows Ctrl/Cmd+S (app.js binds save directly to
the shortcut, bypassing the buttons), and a banner inside the
modal explains the situation. The user can still copy their
unsaved text out before closing.
* If a foreign lock arrives via the regular poll while the editor is
already open on that card, the same takeover engages mid-session.
* Opening the editor on a card whose lock is *already* known to be
foreign starts in takeover mode without arming the input listeners
at all.
* Self-detection: poll response now carries `self` (the relay
username), cached as window._collabSelf, so we don't take ourselves
over and the board decoration ignores our own lock.
Visual:
* Board cards held by a foreign lock now get a frosted overlay +
centered lock icon + holder name (replaces the small corner badge).
* Editor takeover banner uses the danger accent. Save buttons get a
.collab-save-blocked dim style.
* New `.collab-lock-toast` variant of the existing conflict toast,
used for both "your push is queued because X is editing" and
"this card just got locked by X" notifications.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>