An empty board on first load blurting "no cards found" was alarming
on a board the user knew had cards. Track a cardsLoaded flag that
flips on the first /api/cards (meta or full) response — or on a
cached-cards hydration — and show a centred spinner placeholder
while it's still false. The "no cards found" copy only shows once
we've actually heard back and the set is genuinely empty.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dev.sh still has to opt into the relay profile now that it's no
longer in the default set; otherwise `./dev.sh` would print "relay:
http://localhost:8081" but skip starting the container. README gets
a short compose section in Quick Start explaining the app-only
default and how to add --profile relay for a local relay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Running a local relay stopped being the common case — most of the
time we're developing against a real one. Move the relay service
behind a `relay` profile so plain `docker compose up` only brings up
the app. Use `docker compose --profile relay up` when a local relay
is wanted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was pulling CSRF from a meta tag that doesn't exist; the POST landed
without a valid token and the server dropped it before the plugin
handler could reply. Use the imported csrf() helper like the rest of
the suite does.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author name above group bubbles was 8px dim-grey pixel font — visible
in theory, invisible in practice. Bump to 10px semibold accent,
left-aligned above the incoming bubble it labels.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Group inserts were using recipient_id=0 as a "no recipient" sentinel,
which fails the tenants FK (foreign_keys is ON) and 500s on every
group send. Use the sender's own id as the placeholder instead;
group GET queries already key off room_uid so there's no self-message
artefact from the value.
Also exclude group rows from /api/chat/sent so the client's
last_sent_relay_id cursor doesn't stall on group sends that never
reach seen=1 (group receipts aren't tracked in v1).
Client-side: surface a clearer error when a room create hits a 404
on the relay — almost always means the relay is running pre-group
code and needs a restart.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends chat.test.js to cover the new conversation shape (kind/key
fields), the /api/chat/rooms endpoint, the thread-settings
round-trip, and the group-creation pick-view toggle. Room-info view
DOM is structurally checked. Bumps to 1.9.4.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Generalises the flyout from peer-only to a thread abstraction
({kind: 'peer'|'room', key, label}). Conversations list and peer
strip render rooms with a group icon alongside 1:1 peers; clicking
either opens the same thread view.
New UI surface:
- Pick view gets a 1:1 / group mode switch. Group mode reveals a
name field + multi-select member list and a Create button.
- Room threads show an info icon in the flyout header that opens a
member list; owners can add/remove members, rename-later, or
delete the room; non-owners can leave.
- 1:1 threads show a disposable toggle (ph-fire) in the header;
flipping it tells the relay to drop future sends after delivery
instead of persisting.
- Group bubbles show the author's username above each message when
it changes, matching common chat UX.
Thread open triggers a backfill fetch so cold caches / second
devices catch up on older history from the relay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends the local chat cache to mirror rooms + membership from the
relay. chat_messages grows a room_uid column; peer is reused as
author-username for group rows so a single table covers both kinds.
New local tables:
chat_rooms — uid, name, owner, synced_at
chat_room_members — room_uid + username
chat_thread_settings — per-peer disposable flag
New / extended endpoints:
GET/POST/PATCH/DELETE /api/chat/rooms[/{uid}] — proxy to relay
POST /api/chat/backfill — pulls older messages from relay
GET/POST /api/chat/thread-settings — disposable toggle
/api/chat/conversations — mixed list of peers + rooms
/api/chat/messages — accepts peer OR room_uid
/api/chat/send — accepts peer OR room_uid; forwards disposable
/api/chat/poll — mirrors rooms on every tick; sets direction
from `mine` flag so multi-device group sends
render as out-bubbles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds named group threads (chat_rooms + chat_room_members), mirroring
the collab-buckets membership pattern. Group messages share the
chat_messages table via a new nullable room_uid column; recipient_id
is left 0 for group rows and fan-out happens via the member-join at
GET time instead of duplicating per recipient.
Chat retention flips from "drop everything after 7 days" to
persistent-by-default: a new `persistent` flag on chat_messages marks
long-term rows (default 1 — all group messages plus non-disposable
1:1). The 7-day cleanup only targets persistent=0 rows so the sender
can still poll final receipt state before they're pruned.
New endpoints:
POST/GET/PATCH/DELETE /api/chat/rooms[/{uid}] — room CRUD
GET /api/chat/backfill — scrollback / cold-start history fetch
POST /api/chat — accepts recipient OR room_uid + disposable flag
GET /api/chat — returns 1:1 to caller AND rooms caller is in
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers the flyout lifecycle (footer button, toggle, header-back
visibility tracking thread/list view), the pick-view detour, DOM
structural invariants (peer strip inside thread view, form/input/send
present) and the three read-only chat endpoints. Send/receive requires
a real relay and is intentionally skipped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switching conversations used to require bouncing back to the list view
and picking from there. Now the thread view has a horizontal peer
strip at the top: one click hops to another conversation, active tab
is highlighted and auto-scrolls into view, unread peers show a dot.
Scrollbar is hidden — vertical wheel delta is redirected to horizontal
scroll so the user can slide the strip with the mouse wheel when it
overflows.
In-thread header is gone; the back button moved next to "chat" in the
flyout header and only shows while a thread is open. Peer name stays
visible via the active tab.
Bumps to 1.9.3.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Relay UI already shows a tenant's inbox + sent mails. Add a parallel
view for collaborative bucket cards the logged-in tenant is a member
of, so activity on shared boards is visible without needing the hyprcrd
client open.
New `GET /api/collab-inbox` endpoint (session-auth only) decrypts
payloads with the inter-tenant key and returns title + preview + author
+ bucket metadata. UI restructures the compose view into editor + right
sidebar stacking inbox → shared → sent, and polls all three on the
existing 15s tick.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9px pixel font on the design-panel slider value labels (Radius, Border,
Bar height, …) was hard to parse at that size. Switch to 11px regular
and widen the column slightly so two-digit values fit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests assume a normal board — no super-view, no bucket filter, manual
sort, no hides. When the runner fired with the app left in super-view
or with filters on, selectors like `.card[data-id]` missed the
`.superview-item` DOM and multi-select / due-date assertions read the
wrong initial values.
Snapshot the relevant bits of window.state before the run, force a
clean baseline, then restore the user's view afterwards so opening the
runner doesn't silently wipe their context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
caldav.js was carrying the core sync timer, autosync toggle, relay
settings and every poller hook alongside its actual CalDAV/calendar
config concerns. Split out:
- New sync.js owns the single background timer, header Sync button,
autosync pause toggle, configurable interval, and emits `sync.tick`
so plugins can fan out without their own setIntervals.
- collab-buckets + chat now subscribe to sync.tick instead of running
their own pollers, so the manual Sync button refreshes them too and
the Autosync pause actually pauses everything.
- Replace the number input for sync interval with a stepped slider
(5s..1h snap points) and a live label, bumped to 1.9.2.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- header Sync button now has a paired autosync toggle to its right —
reads as one button with no inner seam (outer border only). Dot inside
pulses once per actual sync (bursts on every backgroundSync /
triggerSync / collab pollOnce via a new sync.pulse hook) so the user
can see polls happen. Dot + data-autosync attribute persist the
enabled state in localStorage; paused state stops both the
CalDAV/relay background timer and the collab poll via a broadcast
autosync.changed hook.
- hover polish: Sync regains a full accent border (the missing inner
edge re-appears only on its hover); the toggle fills with solid
accent as hover treatment so the pair never shows a half-outline.
- sync interval moved out of hardcode into a settings field under
Settings → Relay: number input, 5s..3600s, default 15s (was 45s on
caldav / 30s on collab). Changes broadcast via sync.interval.changed
and restart both timers without reload.
- session cookie lifetime raised to 30 days with matching gc_maxlifetime
and SameSite=Lax (was 0 / Strict — some users got kicked to login on
every reload when gc cleaned up the /tmp/sess file or a cross-link
return blocked the cookie).
- search button's kbd element was pushing the button ~3px taller than
Sync; leading-none + tighter padding puts them on the same height.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Temporary diagnostic scaffolding — B-side logs every POST to the relay
(status + body snippet), A-side already console.debugs the GET result
via sync_debug. Lets us narrow down where the sender-side toast gets
lost.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- detail banner timeAgo: SQLite timestamps are UTC without tz marker,
JS was parsing them as local. Fixed by appending Z when absent —
same pattern chat uses. "2h ago" for a 30s-old update is gone.
- banner Reload button now re-fires detail.opened so the collab-buckets
plugin clears the red dot on the board card and calls mark-card-seen.
Dismiss (×) still leaves the dot — that's the acknowledgement that
nothing was actually read.
- instrument /api/relay/poll with a sync_debug field so the JS can
console.debug what happened on the sync-events sub-request (status,
count, error body). Helps diagnose "no toast" reports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- replace chat-bot sync notifications with a dedicated relay_sync_events
queue: POST/GET /api/sync-events, drained by /api/relay/poll and
surfaced as stacking toasts via caldav.js. Old /api/chat/bot removed,
bot=1 rows purged on bootstrap, orange bot rendering dropped from
relay UI. Chat thread stays clean.
- collab pull: fetch attachments BEFORE applying the body so the moment
state.cards reflects the new reference the file is on disk. Fixes the
"preview shows broken/missing image, editor shows it" race where the
browser cached the 404 between attachment landing and user re-render.
- detail stale banner now compares title/body/color/tags in addition to
updated_at — robust against second-granularity timestamp collisions
and any other case where updated_at doesn't tick but content differs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- per-card unread dot on board (collab-buckets): backend tracks
last_seen_at per card, poll exposes unread_cards, frontend decorates
matching board cards; cleared on any open path via detail.opened hook
- detail.opened + cards.reloaded hooks on the hyprcrd event bus
- stale banner in detail view with manual Reload when the currently
viewed card got updated by a peer — no content swap under the user
- collab image sync: push attachments BEFORE card (race fix) + pull-side
self-heal scans every collab card for missing uploads each poll
- relay user deletion now purges chat_messages sent by deleted user so
ghost threads stop appearing for their peers
- calendar header nav buttons styled as square bordered buttons that
honor --ui-radius-button; title bar widened to 280px so font variants
don't wrap
- .toolbar-btn now uses var(--ui-radius-button) instead of hardcoded 3px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fullscreen cluster dashboard view consuming the Oumta Go backend.
All data currently mocked — see plugin TODO.md for integration plan.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
- Context menu tag toggle reads fresh body from state.cards — was
overwriting with stale snapshot, dropping existing tags (e.g. #done)
- Board empty-state centered with flex (vertical + horizontal)
- Plugin settings: batch toggles via Apply button, single reload
instead of one reload per toggle
- New bucket creation activates it directly on the board
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Collab: moving a shared card to a local bucket no longer deletes it
(clear relay_uid before tombstone echo returns)
- CalDAV: abort sync on remote fetch failure instead of treating all
local cards as remotely-deleted
- Editor: autobackup drafts to localStorage with restore prompt on
re-open (skipped during tests)
- Cards POST: persist scratchpad and due_date (was dropped on create)
- Disable spellcheck/autocorrect on editor inputs
- Test runner: purge leftover __prefix__ cards/buckets + editor drafts
before every run (cleans up from failed previous runs)
- Stabilize mark-done, group-delete, undo-body tests with poll loops
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Scratchpad: card-scoped private notes in editor (toggle vs preview),
smaller font, cleared via button, stored in cards.scratchpad
- Zen mode: ESC exits zen before closing detail, Z toggles zen
- Chat: always show absolute time (hh:mm, yesterday, or date+time)
- Backspace/Delete also deletes focused card (not just selected)
- Selected + focused cards use brand accent (not accent2)
- Tooltip: guard against non-element targets
- Collab sync: skip poll while editor is open, 2s push debounce
- Rrule test: only check geburtstag tag, not title format
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Terminal: in-app command line with 22 commands (Shift+T toggle).
Navigation, CRUD, search, view toggles, tab completion, history.
Card history: card_history table tracks field changes on every PATCH.
GET /api/cards/{id}/history endpoint. Terminal `history` command.
bump.sh: standalone version bump for config.php, electron/package.json,
website footer. dist.sh no longer bumps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Image resize respects width attr in preview (no more snap-back)
- Single-task checkbox aligned with flex center instead of translateY hack
- Modal/detail containers clip card-bar at rounded corners
- Right-click on image no longer triggers resize (left-button only)
- Split dist.sh and bump.sh into separate scripts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Inbox poll continues fetching but skips DOM updates when user is typing,
deleting, or confirming. Quiet mode diffs message IDs to avoid
unnecessary re-renders. Focus/blur tracking on all inputs auto-guards.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New tooltip.js module with styled tooltips (data-tip) and multi-line
inline help (data-help). Migrates all ~70 native title attributes
across index.php, JS modules, and all plugins. Adds inline help on
9 strategic UI elements (split, superview, zen, extract, assets, etc).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clicking the super-bucket tab now activates the last-shown child
directly. Only the caret arrow on the right opens the dropdown popover.
Caret has its own hit area with accent hover highlight.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gitea plugin: default_repo field per account (stored in DB, pre-fills
create issue form but stays editable). Editor toolbar: asset picker
flyout with 4-column thumbnail grid and live search filter. Clicking
a tile inserts the matching markdown.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Electron desktop app section, document all 8 plugins, update
features list (assets, super-buckets, find/replace, arrow nav, PDF
upload), fix relay docs (chat shipped, not future), add missing API
endpoints, correct electron data storage paths and php-bin fetch info.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Set upload_max_filesize=64M and display_errors=0 for Electron PHP
- Import shows error dialog instead of silently failing
- Footer count repositions at 1450px instead of 1250px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Updates help screen with all undocumented features. Fixes F1 blocked
by input suppression filter (allow function keys). Stabilizes card-join
tests with poll loop instead of fixed sleep.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shortcut dispatcher now skips non-modifier keys when focus is in an
editable field. Editor find bar ESC stops propagation to close bar
before modal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves board-wide find/replace from standalone overlay into the search
panel as a toggleable replace row. Adds editor toolbar find/replace
button. Ctrl+Shift+H now opens search in replace mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Editor find/replace (public/js/modules/editor.js, public/index.php):
* New #editor-find-bar inside #editor-pane (above the textarea, pushes
it down via flex-col). Two rows: find input + match count + prev/next
+ close, and replace input + replace-one + replace-all.
* Ctrl+H opens find+replace, Ctrl+F opens find-only (replace row
hidden). Same shortcut toggles off if already open in the same mode.
* runFind(): case-insensitive indexOf scan, all match positions stored.
jumpTo() sets textarea.setSelectionRange for the Nth match.
findNext/findPrev cycle with wrap-around.
* replaceOne(): body.setRangeText at the current match position, re-scan.
replaceAll(): regex-based global replace on body.value.
* Enter in find input = next (Shift+Enter = prev). Enter in replace
input = replace one. Escape closes the bar + refocuses textarea.
* closeModal() calls closeFindBar() to clean up on editor close.
Board-wide find/replace (public/js/app.js):
* Ctrl+Shift+H opens a centered overlay that fetches all cards with
full bodies (api('api/cards'), bypasses lazy-load).
* Live search: typing in the find input scans all card bodies case-
insensitive. Matches shown as a scrollable list with color dot,
card title, occurrence count, and a context snippet with <mark>
highlight around the first match.
* Status line: "12 occurrences in 5 cards" or "no matches".
* Replace All button (danger-styled, disabled until both inputs have
text and matches exist). Click opens a showDialog confirmation:
"Replace 12 occurrences of 'foo' with 'bar' across 5 cards?"
On confirm: regex replace per card body → PATCH → _reloadAll.
* Escape / click-outside / Close button dismiss the overlay.
Shortcuts registered in the central registry for the help screen:
* ⌃ H (editor), ⌃ F (editor), ⌃ ⇧ H (global board-wide).
Tests (public/plugins/tests/board-find-replace.test.js):
* Ctrl+Shift+H opens the overlay (polls for #board-find-replace).
* Typing in find shows matching cards (creates 3 cards, 2 match,
asserts status count + list contains matching titles + excludes
non-matching title).
* Replace all updates card bodies after confirmation (creates 2
cards with "hello", replaces with "goodbye", confirms dialog,
verifies via API that bodies are "goodbye world goodbye" and
"goodbye there").
* Replace all button disabled without replace text.
detail.test.js: "closes on ESC" stabilized — guard that detail is
actually open before dispatching Escape, poll (20×50ms) instead of
fixed sleep(150) for the .open class to be removed.
chore: bump version to 0.9.21
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
runner.js: the Escape handler now checks panel.style.display !== 'none'
before closing. During the test run the panel is hidden (display:none)
but the variable is truthy — synthetic Escape events dispatched by
tests (e.g. "closes on ESC") were triggering close() mid-run, removing
the panel entirely so results never showed.
quick-capture.test.js: the "Enter creates card with #inbox tag" test
was asserting found.tags contains 'inbox', but the #inbox auto-tag
was removed in 0.9.17. Renamed to "Enter creates card in active
bucket" and dropped the stale tags assertion.
chore: bump version to 0.9.20
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Orphaned items now use mediaTile() instead of a hand-written triple-
ternary, so audio and PDF files render with the same icon/player tile
as linked items. Eliminates the code duplication that caused audio
files to fall through to the <img> path in the orphaned section.
* applyFilter() tracks visibleCount after toggling item display. When
the active filter hides every item (e.g. "Audio" tab with no audio
files) but total assets exist, a type-specific placeholder is shown:
matching icon (ph-image / ph-music-note / ph-file-pdf) + "no {type}
assets" text. Hidden again on the next filter switch that has results.
* Section header hide logic narrowed to [data-section]:not(.gallery-item)
so it only targets the header divs, not the items themselves.
chore: bump version to 0.9.19
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>