Commit Graph
100 Commits
Author SHA1 Message Date
thandClaude Opus 4.6 52da9abd44 chat: client-side rooms, disposable flag, history backfill
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>
2026-04-15 20:15:38 +02:00
thandClaude Opus 4.6 044caae447 relay: group chat rooms + persistent chat history
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>
2026-04-15 20:15:29 +02:00
thandClaude Opus 4.6 618a787b36 tests: UI + API smoke tests for chat plugin
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>
2026-04-15 19:30:43 +02:00
thandClaude Opus 4.6 81d9ce0e87 chat: peer-switch strip, back button moved to flyout header
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>
2026-04-15 19:30:38 +02:00
thandClaude Opus 4.6 bfd5420981 relay: surface shared-bucket activity in compose sidebar
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>
2026-04-15 19:03:18 +02:00
thandClaude Opus 4.6 fa3a6c2501 settings: readable font for design-panel slider values
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>
2026-04-15 19:03:07 +02:00
thandClaude Opus 4.6 1a889ea435 tests: snapshot and reset view state around runs
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>
2026-04-15 19:02:58 +02:00
thandClaude Opus 4.6 1d6499e5dd extract sync orchestration into sync.js, unify on sync.tick heartbeat
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>
2026-04-15 19:02:44 +02:00
thandClaude Opus 4.6 b7f450e41f autosync toggle, configurable sync interval, 30-day session
- 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>
2026-04-15 08:57:24 +02:00
thandClaude Opus 4.6 6cd00f9660 diag: error_log sync-event POSTs to trace missing toasts
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>
2026-04-14 23:24:35 +02:00
thandClaude Opus 4.6 4a668b7fc4 fixes: timeAgo utc parse, reload clears unread, sync-event debug
- 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>
2026-04-14 23:16:48 +02:00
thandClaude Opus 4.6 1966f8f4c0 sync-event toasts, image pull ordering, stale banner field diff
- 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>
2026-04-14 23:00:30 +02:00
thandClaude Opus 4.6 26f95ba079 collab per-card unread, detail stale banner, image sync race fix
- 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>
2026-04-14 22:39:32 +02:00
thandClaude Opus 4.6 39e94b8015 oumta dashboard plugin (mockup)
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>
2026-04-14 21:24:07 +02:00
thandClaude Opus 4.6 69ad2c7e71 fixes: password form wrap, collab-buckets csrf race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:23:48 +02:00
thandClaude Opus 4.6 ae974314f9 sync electron version with main app
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:33:14 +02:00
thandClaude Opus 4.6 3b7c7cf5ac tim plugin, chat bot sync notifications, test fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:32:17 +02:00
thandClaude Opus 4.6 ca47052768 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>
2026-04-14 18:22:02 +02:00
thandClaude Opus 4.6 002562b1d3 fixes: tag assign stale body, board placeholder, plugin pending, bucket activate
- 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>
2026-04-14 16:13:46 +02:00
thandClaude Opus 4.6 3cca14a3c6 fixes: collab move, caldav abort, draft recovery, autobackup, test stabilization
- 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>
2026-04-14 15:01:07 +02:00
thandClaude Opus 4.6 7e2b130434 fixes: scratchpad, zen ESC, chat time, tooltip safety, collab sync race
- 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>
2026-04-14 00:45:31 +02:00
thandClaude Opus 4.6 2169c1de1b terminal plugin, card edit history, bump script
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>
2026-04-13 14:10:43 +02:00
thandClaude Opus 4.6 b9bd294c90 fix: image resize preview, todo checkbox alignment, modal overflow clip
- 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>
2026-04-13 12:17:24 +02:00
thandClaude Opus 4.6 6c0ca4373b relay: file attachments, outbox, CC-self, chat view, mobile layout
Compose: attach files (encrypted, hex-named, linked via relay_uid),
CC-self checkbox tags copy with recipient name. Outbox section shows
sent messages with delete. Chat view: session-based threads/history/
send endpoints, split layout with sidebar, read receipts, 5s poll.
Mobile: threads as horizontal scroll bar above messages. Fix chat
input pushed offscreen by adding min-height:0 on flex container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:48:23 +02:00
thandClaude Opus 4.6 9f85e484c4 example-content plugin, project website with screenshot
Example-content: footer button creates 20 realistic demo cards in a
"Project Alpha" bucket for screenshots and first-time exploration.

Website: full-page sections with large watermark labels, screenshot
hero with typing animation, email/spam section, Gitea detail,
relay encryption flow, keyboard keycaps, deploy options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:54:43 +02:00
thandClaude Opus 4.6 b9345a410f watch plugin: analog clock overlay with SVG watchface
Simple watchface with second-hand wobble animation, 5-min idle
auto-show, W key toggle, light/dark theme colors, backdrop blur
from theme settings. Grace period prevents instant dismiss.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:16:25 +02:00
thandClaude Opus 4.6 00d14b3015 relay: suppress poll re-renders during user actions
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>
2026-04-12 20:26:26 +02:00
thandClaude Opus 4.6 f465306c1d global tooltip system, migrate all title attrs to data-tip/data-help
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>
2026-04-12 19:55:17 +02:00
thandClaude Opus 4.6 527305355c super-buckets: split tab click (activate) from caret click (dropdown)
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>
2026-04-12 19:37:38 +02:00
thandClaude Opus 4.6 04b52ecc19 gitea: default repo setting; editor: asset picker with thumbnail grid
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>
2026-04-12 19:26:04 +02:00
thandClaude Opus 4.6 c5bf8b0407 docs: update all READMEs for v1.1.0
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>
2026-04-12 18:00:06 +02:00
thandClaude Opus 4.6 d5f44370b6 fix: PHP upload limits, import error dialog, earlier footer breakpoint
- 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>
2026-04-12 17:46:56 +02:00
thandClaude Opus 4.6 88ad208714 sync electron/package.json version to 1.1.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:25:34 +02:00
thandClaude Opus 4.6 d3d9355191 electron: external data dir, packaged path fix, footer responsive, test stabilization
- DB + uploads stored outside app bundle (survives updates)
- Router uses DOCUMENT_ROOT instead of __DIR__ (fixes AppImage paths)
- Footer count shifts right below 1250px, min-width 1150px
- Gallery opens gracefully on fetch errors
- Stabilized gallery, export, and find/replace tests with poll loops

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:24:00 +02:00
th ee192a15e7 zwischenparkung 2026-04-12 16:09:34 +02:00
thandClaude Opus 4.6 22a43fdef7 help: add relay, context menu, find/replace, assets, super-buckets docs; fix flaky tests
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>
2026-04-12 13:11:52 +02:00
thandClaude Opus 4.6 bbfe968ff1 fix: suppress shortcuts when typing in input fields
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>
2026-04-12 12:57:05 +02:00
thandClaude Opus 4.6 d1b26e625a search: integrate find/replace into search overlay, add toolbar buttons
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>
2026-04-12 12:53:11 +02:00
thandClaude Opus 4.6 838dfe2a6e relay: custom confirm dialog, custom recipient dropdown, inbox polish
Replaces all native confirm() calls with styled relayConfirm() promise.
Replaces native <select> recipient picker with custom dropdown control.
Adds synced badge, title-only preview, delete-all, inline delete confirm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:40:21 +02:00
thandClaude Opus 4.6 3078906548 find/replace: editor (Ctrl+H/F) + board-wide (Ctrl+Shift+H)
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>
2026-04-12 12:23:18 +02:00
thandClaude Opus 4.6 3eb5202aac fix: ESC doesn't kill panel mid-run, remove stale #inbox test
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>
2026-04-12 12:10:56 +02:00
thandClaude Opus 4.6 c04eac06c4 assets: unified tile render, empty-filter placeholder
* 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>
2026-04-12 11:51:08 +02:00
thandClaude Opus 4.6 bf81d046e6 PDF upload, gallery → assets with type tabs, orphan detection fix
PDF upload support:
* index.php: application/pdf added to $mimeMap + $extMap in the
  /api/upload handler. PDFs are stored in uploads/ like images/audio.
* editor.js uploadFile(): detects PDF via MIME or .pdf extension.
  Inserts a markdown link [📄 filename](uploads/...) instead of an
  image embed. Paste + drop handlers updated to accept PDFs.
* index.php: new toolbar button #md-upload-pdf with ph-file-pdf icon
  next to the extract button. Triggers a hidden file input
  (accept=".pdf"). Wired in editor.js.

Gallery → Assets rename:
* index.php footer button: "Images" → "Assets", icon ph-folder-open.
* index.php gallery overlay header: "images" → "assets", icon
  ph-folder-open, upload input accept extended to include .pdf.
* Empty state text: "no images" → "no assets".

File-type tabs in the assets overlay:
* index.php: new #gallery-tabs div with 4 buttons (All / Images /
  Audio / PDF), each with data-filter attribute.
* gallery.js: fileType(url) helper returns 'image'|'audio'|'pdf'|
  'other'. Tab clicks toggle .active class and filter gallery-items
  by data-url + fileType. activeFilter persists across re-opens.
* CSS: .gallery-tab styling with hover + .active accent fill.

PDF rendering in the assets grid:
* gallery.js mediaTile(): PDFs render as ph-file-pdf icon + filename
  instead of a thumbnail. Orphaned PDFs show the same icon tile.
* Click on an orphaned PDF opens it in a new tab (window.open).
* "New card" from an orphaned PDF inserts a markdown link (not
  image embed).

Orphan detection fix (gallery.js):
* Root cause: loadCards() in app.js uses two-phase lazy loading —
  first /api/cards/meta (body=null on all cards), then batches of 25
  with full bodies. Opening the gallery before the last batch
  completes meant most cards had body=null, getReferencedUrls()
  found no URLs, and every file appeared orphaned.
* Fix: openGallery() now fetches its own card list with full bodies
  via api('api/cards') in parallel with api('api/uploads'), stored in
  a module-local galleryCards array. Both getReferencedUrls() and
  findCardForUrl() read from galleryCards instead of state.cards.
* Bonus: added a fallback regex uploads/[hex]{32}.[ext] that catches
  references in any format the specific patterns might miss (HTML
  attributes, custom blocks, inline text).

chore: bump version to 0.9.18

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:39:50 +02:00
thandClaude Opus 4.6 195b0e343f delete animation, no #inbox tag, preset delete lock on builtins
Card delete animation (public/css/style.css, public/js/modules/board.js):
* @keyframes card-remove: opacity 1→0 + scale(1)→0.85 + translateY(8px),
  250ms ease-in forwards. .card-removing class with pointer-events:none.
* New exported animateCardRemoval(ids) in board.js: adds .card-removing
  to each matching board-card element, resolves after 260ms so the
  caller can await before the API DELETE + _reloadAll.
* Wired into all three delete paths:
  - context-menu.js group delete: animates parent + all children
  - context-menu.js single delete: same path (allCards has one entry)
  - app.js Del/Backspace shortcut: animates all selected cards

Quick capture no #inbox (public/js/app.js):
* Removed the line that auto-injected #inbox into the body of every
  quick-captured card. Cards now land tag-free in the active bucket
  (or unsorted).

Settings: preset delete lock (public/js/modules/theme.js):
* refreshPresetSelect() now disables the delete button (disabled attr
  + opacity 0.35 + cursor not-allowed) when a builtin: preset is
  selected. Re-enabled when a user: preset is selected. The click
  handler already had a startsWith('user:') guard but the button
  wasn't visually locked.

chore: bump version to 0.9.17

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:25:57 +02:00
thandClaude Opus 4.6 ce6ca73ea0 pulse: 1× local vs 3× remote, local cards don't trigger all-tab dot
Locally-created cards (editor, quick capture, duplicate) now get a
single subtle pulse instead of the triple-pulse reserved for relay-
synced cards. Local cards also never trigger the unread dot on the
fixed "all" bucket tab — the user just created them, there's nothing
"unseen" about their own work.

state.js: new state.recentlyCreatedIds Set. Populated by editor.js
doSave (POST), app.js quick capture (POST), and context-menu.js
duplicate (both date-rename and copy paths).

board.js renderBoard diff: new card ids are checked against
recentlyCreatedIds at diff time. Local ids go into a renderBoard-
local newLocalIds set (never into state.unseenCardIds). Remote ids
go into unseenCardIds as before.

board.js drain block (end of renderBoard): two-phase drain.
Phase 1: newLocalIds → pulseCards(local, 'card-pulse-local') with
900ms timeout, works in any bucket. Consumed recentlyCreatedIds
entries are deleted.
Phase 2: remaining unseenCardIds (remote only) → pulseCards(remote,
'card-pulse-new') with 2400ms timeout, only on activeBucket=all.

decorateAllUnreadDot() reads only state.unseenCardIds which now
contains exclusively remote ids → no dot from local card creation.

style.css: new .card-pulse-local class — same @keyframes but
animation-iteration-count:1. Anim-fast/slow overrides added.

chore: bump version to 0.9.16

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:15:53 +02:00
thandClaude Opus 4.6 088c225202 runner: bump RUNNER_V ?v=3 → ?v=4 so test-shield removal loads
The test-shield (#test-shield) was added in loader.js (always fresh
via plugin manager ?v=time()) but the removal lives in runner.js
hideProgress(). runner.js was still cached at ?v=3 in the browser,
so the shield stayed after the run. Bumping to ?v=4 in loader.js +
all 42 test files forces a fresh runner.js load.

chore: bump version to 0.9.15

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:04:23 +02:00
thandClaude Opus 4.6 88108ecc1b focus: restore body focus on overlay close, test shield, ESC closes results
Focus restoration:
* editor.js closeModal(): blur activeElement after the close so board
  shortcuts (arrows, bare keys) work immediately without clicking.
* detail.js closeDetail(): same blur, skipped when openedFromSearch
  (search input should keep focus in that case).
* search.js closeSearch(): blur after removing .open.
* calendar.js closeCalendar(): blur after closeDayPopup.
All four check activeElement !== body before blurring to avoid no-ops.

Test shield (loader.js):
* ensureProgressBar() now also creates #test-shield — a transparent
  fixed overlay (z-index one below the progress bar) covering the
  viewport below the bar. Blocks all real mouse interaction while
  tests run; synthetic events from the test suite still work because
  they bypass pointer-events. cursor:not-allowed as visual feedback.
  Removed by hideProgress() alongside the progress bar.

ESC closes test results (runner.js):
* The existing keydown listener now also handles Escape when the
  test-panel is visible. stopPropagation prevents the app's Escape
  chain from firing a second close underneath.

Tests (board.test.js):
* New describe "Board: Focus restoration after overlay close":
  - closing the editor returns focus to body
  - closing the detail view returns focus to body
  - closing search returns focus to body
  - arrow keys work after editor round-trip (enter → enter → escape)
    — the exact flow the user reported as broken. Dispatches
    ArrowDown → Enter (detail) → Enter (editor) → Escape (close) →
    asserts body focus → ArrowDown → asserts .card-focused exists.
* Fixed .not.toBe() → (x !== y).toBe(true) since the custom runner
  doesn't have a .not chain.

chore: bump version to 0.9.14

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:02:27 +02:00
thandClaude Opus 4.6 b459159b0f shortcuts: central registry + auto-generated help, test stabilization
New module public/js/modules/shortcuts.js:
* registerShortcut({key, ctrl, shift, alt, match, guard, priority,
  section, label, keys, handler}) — declarative shortcut registration
  with priority ordering, custom match predicates, and runtime guards.
* Single document-level keydown dispatcher iterates the registry
  (sorted by priority desc), matches key+modifiers or custom match(),
  checks guard(), fires handler with preventDefault.
* guards object: board(), editor(), detail(), search(), email(),
  canvas(), bodyFocused(), noOverlay(), isOpen(id) — reusable
  predicates that replace the repeated blocker-array patterns.
* renderShortcutHelp() auto-generates the #shortcuts overlay content
  from the registry, grouped by section. New shortcuts added via
  registerShortcut appear in the help screen automatically.
* Shortcuts with guard: () => false are help-screen-only entries for
  element-level shortcuts handled by their own modules (editor
  textarea, search input, canvas overlay, etc.).

Migration (public/js/app.js):
* The ~200-line monolithic keydown handler is replaced by ~80
  declarative registerShortcut() calls.
* Escape chain: 10 separate registrations with priorities 100→20,
  each with its own guard (quick-capture, detail/zen, modal, search,
  tasks, gallery, calendar, shortcuts, help). A hidden: true flag
  suppresses them from the help screen; one visible entry with
  guard: () => false shows "Esc — close any overlay".
* Global combos: Ctrl+K (search), Ctrl+N (quick capture), Ctrl+Z
  (undo), F1 (help), ? (shortcuts).
* Editor: Ctrl+S (save), Ctrl+Shift+S (save+close), Ctrl+Enter
  (save+close). Element-level shortcuts (Tab indent, Alt+arrows,
  autocomplete triggers) registered as help-screen-only.
* Board bare keys: a/f/j/n/t/c//, Tab/Shift+Tab, 1-9, arrow nav,
  Enter → preview, Delete/Backspace selected.
* Detail: ←/→ group navigation, Enter → editor.
* Help-screen-only: search ↑↓/Enter, board mouse actions, canvas
  tool shortcuts (B/E/L/R/O/F/T/⌫), mail ⌫/⇧⌫, m (plugin).

Removed from board.js: the document-level Delete/Backspace listener
(now in the central registry with the same logic).

Help screen (public/index.php):
* The hardcoded 3-column shortcut HTML is replaced by an empty
  .shortcut-help-content div that renderShortcutHelp() fills at boot.
* CSS: .shortcut-help-content section gets break-inside:avoid +
  margin-bottom for clean column flow.

Test stabilization:
* card-interactions.test.js: due-pill, overdue-border, and
  child-hidden-on-board tests now set state.activeBucket='all'
  before _reloadAll and poll for the DOM element (40×50ms) instead
  of a fixed sleep(200). Eliminates the flaky "card not found" when
  activeBucket was leaked by a prior test.
* editor-features.test.js: "save creates card and closes editor"
  polls for #modal to lose .open class (40×50ms) instead of a fixed
  sleep(400). doSave(true) does 2-3 serial API calls before
  closeModal which sometimes exceeded the old budget.

chore: bump version to 0.9.13

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:50:49 +02:00
thandClaude Opus 4.6 7c113fd5ab shortcuts: board arrow-nav, enter→preview→edit, help screen polish, n/a/f/j hotkeys
Board arrow-key card navigation (public/js/app.js):
* ↑/↓/←/→ on the board (body focus, no overlay) move a single-card
  focus cursor (.card-focused, accent2 outline) through the visible
  card grid. Column count is computed live from the first row's
  offsetTop so it tracks the CSS grid columns at any viewport width.
* First arrow press focuses the first card; subsequent presses move
  in the grid. Focused card scrolls into view (smooth, nearest).
* Enter on a focused card → openDetail (preview). Focus cleared.
* Enter in detail view → closeDetail + openModal (editor). Card ref
  is captured before closeDetail nulls state.viewingCard.
* Click anywhere → focus cleared. Re-renders restore focus via
  window._restoreCardFocus() called from board.js renderBoard().

CSS (public/css/style.css):
* .card-focused: accent2 outline + glow, visually distinct from
  .card-selected (accent, multi-select) and .preview-active.
* .shortcut-table kbd keycaps: JetBrains Mono font (not pixel),
  10px, 3D raised look with gradient + bottom border + inner
  highlight + shadow.
* Responsive shortcut grid: 3 cols > 900px, 2 cols > 640px, 1 col.

New bare-key shortcuts (public/js/app.js):
* n → openModal(null) (new card, same as the + button)
* a → activateBucket('all')
* f → activateBucket('favorites')
* j → open today's journal card (find by YYYY-MM-DD prefix in
  state.cards), or create a new one (switch to journal view, open
  empty editor which auto-fills the date title)

Ctrl+N quick capture (public/js/app.js):
* Now sends bucket_id: state.activeBucket on the POST (unless the
  active view is a virtual filter like all/current/etc., in which
  case bucket_id stays null). Cards captured while viewing a user
  bucket land directly in that bucket.

Shortcuts help screen (public/index.php):
* max-w-4xl, 3-column grid layout.
* Complete shortcut inventory across all modules:
  - Navigation: a/f/j/n/t/c/m, 1-9, Tab/Shift+Tab, /
  - Global: Ctrl+K/N/Z, Esc, F1, ?, Del
  - Editor: Ctrl+S, Ctrl+Shift+S, Ctrl+Enter, Tab, Alt+↑↓,
    Shift+Alt+↑↓, Enter (list continue), [[ (link ac), # (tag ac),
    ?snippet
  - Search: ↑↓ navigate, Enter open, Esc close
  - Board+Detail: arrows focus, Enter→preview, Enter→editor,
    ←→ group nav, click/dbl/right/ctrl+click, drag edge/center
  - Canvas: b/e/l/r/o/f/t tool shortcuts, ⌫ delete
  - Mail: ⌫ delete selected, Shift+⌫ skip confirm
* kbd elements use normal font (JetBrains Mono) instead of pixel.
* Help overlay's old shortcuts section replaced with a one-liner
  pointing to ? for the full reference.
* #shortcuts added to runner.js closeAll() list.

chore: bump version to 0.9.12

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:32:31 +02:00
thandClaude Opus 4.6 c8835825e7 test fixes: dialog selector, closeAll scope, copy filter, runner ?v=3
* card-interactions.test.js: group delete tests used `.dialog-ok`
  (class selector) but the button has `id="dialog-ok"`. querySelector
  returned null, the click was skipped, the dialog stayed open and
  blocked every downstream test. Fixed to getElementById.
* runner.js closeAll(): added #dialog, #bucket-super-popover (remove),
  and #gitea-overlay to the cleanup list. A failed test that leaves any
  of these open no longer poisons the rest of the suite.
* runner.js copy handler: when there are failures or flaky tests, the
  "copy" button now builds the clipboard text from lastResults —
  only failed + flaky lines (suite name, test name, timing, error,
  original error for retried). When everything passed, copies the
  full output as before. lastResults is set in renderResults().
* bucket-context.test.js: move-left test used `[data-bucket]` to find
  user bucket tabs, which misses super-bucket tiles (they carry
  `data-bucket-super`). Changed to `[data-bucket-drag]` which is on
  both regular and super-bucket tiles but not on fixed filters.
* context-menu.test.js: make-ticket pre-fill assertion now polls up
  to 2s for the title input to be populated (async openCreateForm
  repo-fetch was racing against the immediate assertion).
* card-join.test.js: clickJoin sleep bumped from 400ms → 800ms.
  joinSelectedCards does 3+ serial API calls (fetch bodies, PATCH
  winner, N×DELETE losers, _reloadAll) which sometimes exceeded the
  old 400ms window on slow runs.
* Runner cache: RUNNER_V bumped from ?v=2 → ?v=3 in loader.js + all
  42 test files so the browser picks up the new runner.js content
  (copy handler, closeAll, retry progress bar) without a hard reload.

super-buckets: remember last selection per group

* buckets.js: module-scope lastSuperSelection Map tracks the last
  active member per super-bucket parent. Updated in renderBar()
  whenever activeBucket matches a group member. tileFor() uses it
  as a fallback when the user is on a different bucket — the tile
  shows the last-picked child's name and color instead of reverting
  to the parent. The dropdown popover also highlights the last
  selection when no member is currently active.

chore: bump version to 0.9.11

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:05:38 +02:00
thandClaude Opus 4.6 56c90225cb group delete with confirmation + undo, retry progress bar fix
context-menu: group delete (public/js/modules/context-menu.js):
* When the delete action targets a card that has children (a group
  parent), the confirmation dialog is ALWAYS shown regardless of the
  hyprcrd-confirm-delete setting — group deletion is destructive
  enough that it should never happen accidentally. The dialog lists
  every child card by title and the OK button reads
  "Delete all N" (parent + children counted).
* Children are deleted first (avoids orphan flicker), then the parent.
* Undo: a single pushUndo entry recreates the parent via POST first,
  then all children with parent_id pointing at the new parent id.
  due_date is restored via a follow-up PATCH since POST doesn't
  accept it. Ctrl+Z fully restores the group hierarchy.
* Non-group cards keep the existing behavior (simple confirm if the
  setting is on, single delete, pushCardDelete for undo).

tests (public/plugins/tests/card-interactions.test.js):
* New describe "Card: Group Delete" with two tests:
  - "deleting a group removes parent AND all children": creates a
    parent + 2 children, right-clicks the parent, clicks delete,
    asserts the dialog text contains both child titles, confirms,
    verifies all 3 cards are gone from the API.
  - "Ctrl+Z after group delete restores parent + children": creates
    parent + 1 child, deletes the group, confirms both gone, fires
    Ctrl+Z, finds the restored cards by title, asserts
    child.parent_id === restoredParent.id.
  Both tests save/restore the hyprcrd-confirm-delete localStorage
  value and soft-skip if the parent isn't visible on the board.

runner: retry progress bar (public/plugins/tests/runner.js):
* hideProgress() was called inside runAll() at the end of the first
  pass, which removed the #test-progress element before the retry
  pass started. The retry ran invisibly — "retrying…" never showed.
  Moved hideProgress() out of runAll() into run(), after both the
  first pass and the retry pass are done. The bar now stays visible
  for the whole sequence: first pass fills to 100%, retry resets the
  fill to 0 (via updateProgress(0, total, 'retrying…', false)) and
  fills again for just the failed tests.

chore: bump version to 0.9.10

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 09:34:31 +02:00
thandClaude Opus 4.6 c16659c2ad new-card unread dot + pulse fix, relay inbox readable, UI polish pass
Board: new-card unread indicator (public/js/modules/board.js,
public/js/modules/buckets.js, public/js/modules/state.js,
public/css/style.css):

* state.unseenCardIds (Set) tracks card ids that arrived since the
  last visit to the "all" view. Diff runs in renderBoard() against a
  module-private knownCardIds snapshot.
* Bug fix: the first renderBoard runs with state.cards = [] (before
  the API returns). The previous diff treated every card from the
  real load as "new" and pulsed the entire board. Now the diff is
  skipped when knownCardIds is null or empty — the first non-empty
  render is a silent prime.
* When activeBucket === 'all' and unseenCardIds is non-empty, the set
  is drained and every matching board card gets a 3-cycle accent glow
  (`.card-pulse-new`, 0.8s × 3, pinned for anim-fast/slow). Works
  both when navigating TO all and when a poll arrives while already
  on all.
* When the user is on a different bucket: unseenCardIds accumulates
  across polls and a red dot (`.bar-unread-dot`) appears on the "all"
  tab. Position is top:3px right:3px *inside* the tab to survive
  the double overflow clip (`#main-header overflow:hidden` +
  `#bucket-bar overflow-x:auto`). renderBar() bakes the dot into the
  all-tab template on every render so it survives bar re-renders that
  aren't followed by a board render.
* decorateAllUnreadDot() in board.js is a fallback called at the end
  of every renderBoard for cases where the diff changed unseenCardIds
  but renderBar hasn't run yet.

Relay: readable inbox (relay/inc/routes/messages.php,
relay/ui/app.js, relay/ui/layout.php, relay/ui/style.css):

* New session-based GET /api/messages/inbox — returns up to 200
  messages for the logged-in user, decrypted server-side with the
  inter-tenant key (no extra leakage — the relay already sees
  plaintext during the routing hop). Does NOT mark messages as
  fetched; the hyprcrd client is still the canonical receiver.
* loadInbox() now fetches from /api/messages/inbox and renders
  actual inbox items: sender (with user-circle icon + accent2),
  optional title (bold), body (pre-wrap, max-height 120px), relative
  timestamp (formatRelTime: "just now", "2 min ago", "today 14:32",
  "3 d ago", or YYYY-MM-DD for older). Unfetched messages show a
  small "new" accent badge next to the sender name.
* deleteMessage(id, btn) — confirms, calls existing DELETE
  /api/messages/{id} (already session-aware), removes the DOM row,
  re-loads the list, toasts "message deleted".
* Auto-poll: startInboxPoll / stopInboxPoll refresh the inbox every
  15s while the compose view is active. Stops on view switch.
* Header-status badge: mirrors the real message count (not just the
  /api/status count) and toggles the has-unread class + dot so the
  tap-to-scroll-to-inbox target stays fresh.

Relay UI polish (relay/ui/style.css):
* .header-status is now a clickable button with tray icon, hover
  tint, focus-ring, and a red dot (::after pseudo) when has-unread.
  jumpToInbox() scrolls smoothly to #inbox-section and flashes it
  with a 1.6s accent box-shadow pulse.
* .inbox-item .from is flex-row with icon + badge-new pill.
* .inbox-item .del gets hover-background for better mobile tappability.
* @keyframes flash-highlight for the scroll-to-inbox animation.

chore: bump version to 0.9.9

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 09:09:29 +02:00
thandClaude Opus 4.6 a67bedc6f2 relay: burger menu for mobile, admin reachable, badge matches header buttons
The relay's nav row used to flatten on mobile by hiding admin / user
identity / compose buttons outright, leaving only "account" + "logout".
Admin was completely unreachable from a phone. Replaces that with a
proper burger menu and exposes every nav target.

* layout.php: new <button id="nav-burger"> with a ph-list icon between
  the spacer and #nav-buttons. Inline `style="display:flex"` removed
  from #nav-buttons — display is now CSS-driven, so the burger
  popover and the desktop row don't fight each other.
* app.js showView(): the mobile-block on `name === 'admin'` is gone.
  Burger button visibility is now mirrored against the same
  setup/login/change-password gating that hides #nav-buttons. Picking
  any view also clears `.menu-open` so the popover collapses behind
  the freshly-shown view.
* app.js toggleNavMenu(): exposed on window for the inline onclick.
  Document-level click outside closes the menu.
* style.css: default `#nav-buttons:not(.hidden)` becomes flex (same
  semantics as the old inline). #nav-burger is `display:none` on
  desktop, `inline-flex` on mobile. Mobile media query drops the old
  display:none rules for #nav-admin / #view-admin / #nav-compose,
  keeps #nav-user hidden (redundant with the menu), and adds a
  `.menu-open` rule that turns #nav-buttons into a popover anchored
  to the header (position:absolute, right:12px, surface bg, border,
  shadow, z-index 50). Buttons inside the popover go full-width
  left-aligned. header { position:relative } so the absolute
  positioning has an anchor.
* style.css: also #view-admin .section gets a simple display:block
  override on mobile so the desktop grid-area layout doesn't break
  the page when admin is opened from a phone.

header .user .badge sizing (carried over from the previous turn): the
admin badge in the relay header now matches the .nav-btn box-model
exactly (5px 10px padding, 9px font, 1px border, 3px radius, line-
height:1, pixel font), so it sits flush with the nav buttons instead
of being a tiny pill in the middle of the row.

chore: bump version to 0.9.8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:40:26 +02:00
thandClaude Opus 4.6 923de0cdd6 hotkeys, gitea ticket-from-card, collab unread, runner auto-retry
board hotkeys (public/js/app.js):
* "c" toggles the calendar from the board (no modifier, body focus,
  no full-screen overlay in front).
* "t" opens a tag quick-pick popover that lists every unique tag from
  state.cards. Filter input + arrow-key/enter pick + ESC/click-outside
  close. Picking sets state.activeTags = [tag] and re-renders the
  board + tag bar. Toggle: a second "t" press closes the picker.
* "1"-"9" picks the Nth user bucket in the bar. The fixed filters
  (all/current/favorites/journal/none) are skipped. Super-buckets
  count as one slot and activate via the new activateBucket() helper
  in buckets.js — that bypasses the click handler so the dropdown
  popover does NOT open; the currently shown member (parent or
  active child) is selected directly.
* ESC chain in the existing handler now also closes #calendar (was
  missing — calendar.js only handles ESC for the day-popup, not the
  whole overlay).

buckets (public/js/modules/buckets.js):
* New exported activateBucket(idOrSpecial) helper: setActiveBucket
  + clear activeGroup + renderBar + window._renderBoard. Used by
  the number-key hotkey to select super-buckets without triggering
  their dropdown.

board split panel (public/js/modules/board.js):
* closeBoardPreview() now removes the .active class from the
  toolbar's #board-split-toggle. The previous code removed
  .text-t-accent which was a no-op since toggleBoardSplit() uses
  .active — the X button on the panel closed the split but left
  the toolbar showing the toggle as still on.

email plugin (public/plugins/email/email.js):
* ESC closes the overlay reliably: the previous handler was on
  #email-overlay, which only fires when focus is *inside* the
  overlay (rare after open). Moved to a document-level keydown.
* Bare "m" toggles the mail overlay open/closed. Suppressed when
  Ctrl/Meta/Alt is held, focus is in an input/textarea/contenteditable,
  or any other full-screen overlay is in front.

collab-buckets (public/plugins/collab-buckets/backend.php):
* When a card change is applied in the pull phase from a foreign
  author (insert / update / tombstone), bump
  collab_buckets.updated_at to NOW for that bucket. has_unread is
  computed as updated_at > last_seen_at and was previously never
  triggered for incoming cards — the bucket-bar tab now gets the
  unread dot + 3-pulse when the user is looking at a different
  bucket. Own pushes are skipped (the relay echoes them back too).

gitea plugin: make ticket from card (public/plugins/gitea/gitea.js):
* New window._giteaMakeIssueFromCard(card) helper. Opens the gitea
  overlay, switches to the create-issue form, fetches the card body
  if it's lazy/null, and pre-fills #gitea-create-title and
  #gitea-create-body. Does NOT submit — the user picks the repo and
  reviews before clicking submit. Soft-bails on the welcome view
  if no gitea account is configured yet.

context-menu (public/js/modules/context-menu.js):
* New "Make ticket" entry in the card right-click menu, only rendered
  when window._giteaMakeIssueFromCard is exposed. Action handler
  hides the menu and calls the helper. Sits next to "Copy text".
* joinSelectedCards() now dedupes hashtag mentions in the merged body
  (first occurrence wins, case-insensitive) so a tag that appeared on
  multiple source cards isn't repeated in the resulting body. Followed
  by a tab/space/blank-line cleanup pass.

test runner auto-retry (public/plugins/tests/runner.js):
* runAll() takes an optional Set<string> of `${suite}::${test}` keys
  and runs only those when set. Progress bar denominator reflects
  the filtered count.
* run() collects every failed test after the first pass, restores the
  backup so the retry runs against the same baseline state, then
  re-runs only those via runAll(failedKeys). Retry results are merged
  into the original results array with `retried: true` and the
  original error preserved on `originalError`.
* renderResults distinguishes three states: passed first try (green ✓),
  pass-on-retry (amber ⚠ "flaky — passed on retry" with the first-run
  error shown), failed twice (red ✗ "failed twice"). Summary line
  carries a "N flaky" count and goes amber if only flaky tests
  appeared in the run.

tests (public/plugins/tests/context-menu.test.js):
* Two new tests for the make-ticket action:
  - action exists in the menu when window._giteaMakeIssueFromCard
    is exposed (soft-skip otherwise)
  - clicking it opens the gitea overlay, surfaces the create-issue
    form, and pre-fills title + body without submitting. Soft-skips
    the assertion when no gitea account is configured (welcome view).

chore: bump version to 0.9.7

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:31:05 +02:00
thandClaude Opus 4.6 285e3b4abe chat: read receipts (delivered + seen) and persisted active bucket
Read receipts on chat messages — single check for delivered (recipient
polled), double check for seen (recipient opened the thread). Pending
state shows a clock until the relay acks the post.

Relay (relay/):
* bootstrap.php: ALTER TABLE chat_messages ADD COLUMN seen + a
  (sender_id, id) index for the sender-side state poll. Migration is
  idempotent via PRAGMA table_info.
* routes/chat.php: two new routes
  - GET  /api/chat/sent?since=N — returns the receipt state (id,
    recipient_id, fetched, seen, created_at) for messages the caller
    sent. No body — sender already has the plaintext locally.
  - POST /api/chat/seen { peer } — marks every message FROM peer TO
    caller as seen=1. Idempotent. Frontend calls this when opening
    a thread; sender's next /sent poll picks the new state up.

Plugin backend (public/plugins/chat/backend.php):
* schema migration: chat_messages gets `delivered` and `seen` columns
  (idempotent), only meaningful for direction='out'.
* /api/chat/send: stores the relay's autoincrement id as `relay_id`
  on the local outgoing row so the receipt poll can match against it.
* /api/chat/poll: after the normal incoming pull, also calls
  GET /api/chat/sent?since=last_sent_relay_id and updates local
  outgoing rows with delivered/seen via UPDATE WHERE relay_id. The
  cursor (last_sent_relay_id state value) only advances past rows
  whose state is final (seen=1); rows still in flight stay re-pollable.
  Response now carries `receipts: <count>` so the frontend can refresh
  the open thread when state changes without new incoming.
* /api/chat/read: after the local is_read=1 update, fires a POST to
  the relay's /api/chat/seen so the sender gets the double-check.
  Silent on failure — the user already saw the messages locally.
* /api/chat/messages: SELECT now includes delivered, seen.

Plugin frontend (public/plugins/chat/chat.js):
* renderThread() appends a receipt icon to outgoing bubbles:
  - seen=1                      → ph-checks (accent, double check)
  - delivered=1, seen=0         → ph-check  (muted, single check)
  - both 0                       → ph-clock  (dim, pending)
  Incoming messages get nothing.
* pollOnce() refreshes the open thread when receipts > 0 even with
  no new incoming, so the icons update in real time while the user
  is looking at the thread.

CSS (public/plugins/chat/chat.css):
* .chat-bubble-time becomes flex with gap so timestamp + receipt
  icon sit on one line.
* .chat-receipt.pending / .delivered / .seen color tiers.

board: persist active bucket across reloads

* state.js: state.activeBucket initializes from
  localStorage.getItem('hyprcrd-active-bucket') || 'all'. New
  setActiveBucket(value) helper writes through to localStorage; new
  ensureActiveBucketValid() helper falls back to 'all' if the
  persisted id no longer exists in state.buckets.
* buckets.js: loadBuckets() calls ensureActiveBucketValid() right
  after the API fetch and before renderBar(). All four production
  assignments (delete-keep, delete-all, popover entry click, regular
  tab click) replaced with setActiveBucket() so the persisted value
  stays in sync. Tests still mutate state.activeBucket directly so
  they don't touch localStorage.

chore: bump version to 0.9.6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:43:28 +02:00
thandClaude Opus 4.6 9b4869812f join: merge multi-selected cards into one (right-click + toolbar)
Right-click on a card that's part of a ≥2-card selection now offers
"Join N cards" at the top of the context menu, and the same action
sits next to "Group" in the bottom select-bar.

Title strategy:
* sequential ctrl-click selection → first selected wins
* bulk gesture (rubber-band) → longest title wins

The bulk vs sequential distinction is tracked via a new
`state.selectionWasBulk` flag set in board.js when the rubber-band
becomes active and cleared in clearSelection() and after a successful
join. ctrl-clicks alone don't touch it, so bulk + ctrl-click hybrid
selections stay bulk.

Body format: list of every original title as `## heading` lines, then
each card's body separated by `---`. The winner keeps its color, tags,
bucket, due_date, parent_id; only the body is rewritten. Losers are
deleted via DELETE /api/cards/{id}.

Undo: a single pushUndo entry restores the winner's original body and
recreates every loser via POST + due_date PATCH (since POST doesn't
take due_date). Ctrl+Z reverts the whole join.

Implementation:
* state.js: new selectionWasBulk boolean in state.
* board.js: rubber-band onMove sets state.selectionWasBulk = true on
  first activation; clearSelection clears it. New select-join click
  handler dynamic-imports joinSelectedCards from context-menu.js to
  keep the dependency lazy.
* context-menu.js: joinSelectedCards is now exported. Inserts
  data-action="join" into the menu when the right-clicked card is in
  a ≥2-card selection. Action handler dispatches to joinSelectedCards
  before falling through to the regular if-chain so it can early-return.
* index.php: new #select-join button in #select-bar with ph-arrows-merge
  icon, sits between Group and Bucket.

Tests (public/plugins/tests/card-join.test.js, registered in tests.js):
* visibility: shows for ≥2 selected with right-clicked card in the
  selection; hidden for solo selection; hidden when right-click target
  isn't selected.
* sequential winner: first ctrl-clicked card survives, others deleted.
* merged body lists every title at the top and contains every body.
* selection cleared after a successful join.
* bulk winner: longest title survives regardless of insertion order.
* undo: Ctrl+Z restores winner body + recreates losers.

undo.test.js race-fix:
* New waitForStateCard(id, predicate, timeout=2000) helper polls
  state.cards every 30ms until the in-memory state reflects the
  expected change. Replaces fragile fixed `sleep(300)` waits between
  chained user actions where the second action reads from state.cards
  before the first action's _reloadAll has landed.
* Multi-undo color test: between the two color changes, wait for
  state.cards to actually contain the pink color before doing the
  second rightClick. Without this the second action's contextmenu
  handler reads the stale card and pushes the wrong undo snapshot,
  collapsing both undo entries onto the original color.
* Due_date undo test: after PATCHing due_date directly via API, wait
  for state.cards to reflect it before opening the editor — the
  editor's snapshot for the undo entry reads from state.cards.find()
  and would otherwise capture due_date=null.

chore: bump version to 0.9.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:25:42 +02:00
thandClaude Opus 4.6 7f08dc1a0d list view: single column with date / bucket / tags, larger title
Drops the multi-column compact list in favour of a one-row-per-card
layout. Each row: color dot · optional cal/fav icon · title (flex-grows
+ ellipsis) · up-to-4 tag chips with overflow indicator · bucket name +
folder icon · right-aligned date column.

Date column picks (in order): due_date → leading YYYY-MM-DD in the
title → updated_at/created_at. Termin/geburtstag tags are filtered out
of the chip list since they're already represented by the icon + dot.

Title font goes from 11px → 14px so the list reads more like a real
list than a label cloud. Rows have 8px vertical padding, hover
background, and a thin bottom-border. Whole grid is capped at 1100px
and centered.

chore: bump version to 0.9.4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:00:16 +02:00
thandClaude Opus 4.6 08f73dfd19 super-buckets, dnd insert cursor for cards + buckets, drag-state survives polls
Super-buckets: a bucket can act as a dropdown of nested children, 1-level
deep, created by dragging one bucket onto the center of another. The
parent shows up as a single tile in the bar with a stack icon and the
active member's name; clicking opens a popover that lists parent + all
children. Selecting an entry sets state.activeBucket. Implementation:

* src/Database.php — ALTER TABLE buckets ADD COLUMN parent_id + index.
* public/index.php — PATCH /api/buckets accepts parent_id with depth /
  cycle / collab guards. DELETE on a parent un-nests its children
  rather than cascade-deleting. The /api/import handler does a second
  pass to remap parent_id through the new bucket id map so backup +
  restore round-trips the hierarchy.
* public/js/modules/buckets.js renderBar() — top-level buckets only;
  buckets with ≥1 child render as a `.bucket-super` tile with
  data-bucket-super=<parent.id> and data-super-shown=<active member>.
  Three drop zones per tile: left/right 25% → reorder via insert
  cursor, center 50% → nest (PATCH parent_id) with .bucket-nest-target
  outline. canNestInto() mirrors the backend rules client-side.
  openSuperDropdown() renders a popover under the tile with parent +
  children as real .bucket-tab[data-bucket=…] entries (so the collab
  plugin's MutationObserver still picks them up); appended inside
  #bucket-bar but `position:fixed` so the bar's overflow:auto can't
  clip it. showBucketCtx() now scopes move-left/right to siblings
  (top-level peers OR same parent_id) so the action works in both
  the bar and the dropdown.

Drag-and-drop insertion cursor for cards (matches the bucket fix):

* public/js/modules/board.js bindCardEvents() — single
  #card-drop-cursor element inside #board, repositioned by per-card
  dragover. Three zones (left/right 25% → cursor in gap, center 50% →
  card-drop-group highlight). dragleave does NOT hide the cursor (was
  flickering as the mouse moved between adjacent cards); only dragend
  clears it. handleCardDrop recomputes the drop zone from e.clientX
  since the cursor approach no longer tags the target with
  .card-drop-before / .card-drop-after.
* "Vor der ersten Card" works automatically: the first card's left
  25% positions the cursor at offsetLeft - 6, which sits inside
  #board's px-6 padding and stays visible.
* public/css/style.css — old .card-drop-before::before /
  .card-drop-after::after pseudo-elements removed in favour of the
  #card-drop-cursor rule.

Drag ghost survives background polls:

* board.js draggingCardIds: Set<string> at module scope. dragstart
  fills it; dragend clears. renderBoard() re-applies the
  .card-dragging class to every member of the set after every render
  so caldav / collab polls calling _reloadAll mid-drag don't
  visually pop the source card back to its normal state.

Tests (public/plugins/tests/super-buckets.test.js):

* API parent_id: nest, un-nest, self-parent rejected, level-2
  rejected (bucket with own children, target is itself a child),
  DELETE un-nests, export → import preserves parent_id.
* Bar rendering: super-tile DOM markers, label = parent name when
  no child active, label = active child name when selected, popover
  opens with parent + children entries, click child sets activeBucket,
  click super-tile twice toggles popover closed, plain bucket stays
  plain.
* Drop cursor element exists.
* Both describe blocks reset state.activeBucket = 'all' in afterEach
  — the rendering tests set it to a child id and the leak was breaking
  downstream Card: Due Date & Done / Card: Grouping tests by filtering
  every newly-created card with bucket_id=null off the board.

chore: bump version to 0.9.3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:55:03 +02:00
thandClaude Opus 4.6 2059e50ce3 ui: version-display via JS, undo-toast centering, ctrl+enter save, test progress polish
* 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>
2026-04-11 20:56:59 +02:00
thandClaude Opus 4.6 fe3399a44b tests: in-run progress strip + cache-bust runner.js across files
* runner.js: shows a 30px fixed-top progress strip during runs (label
  "test x/total:", current suite/test name with ellipsis on the left,
  fixed 50%-wide progress fill on the right that turns danger-red on
  the first failure). The DOM node is created in loader.js so it appears
  immediately on shortcut press; runner.js only updates it. The fill
  width is fixed at flex:0 0 50% so it doesn't jiggle as the test name
  changes length. Suites now live on window._testSuites as a defensive
  fallback so two runner instances stay in sync.

* Cache invalidation: every test file's `from './runner.js'` is now
  `from './runner.js?v=2'`, and loader.js imports the runner with the
  same RUNNER_V suffix. Same URL → one module instance → suite
  registration and runAll see the same array. Bumping RUNNER_V (in
  loader.js + every test file in lock-step) is the way to force a
  re-fetch when runner.js changes.

* loader.js: ensureProgressBar() draws the strip via setProperty(...,
  'important') with z-index 2147483647 attached to documentElement so
  nothing in user CSS, scanline overlay (z 9999) or modals can occlude
  it. Auto-run path (?test=1) also pre-renders the strip.

chore: bump version to 0.9.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:19:22 +02:00
thandClaude Opus 4.6 f6cf7b4358 collab-buckets: exclusive locks, server-enforced, with editor takeover
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>
2026-04-11 19:36:57 +02:00
thandClaude Opus 4.6 c8fe15b0c9 chore: bump version to 0.9.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:50:30 +02:00
thandClaude Opus 4.6 43c15b81a3 tests: rebind runner to Ctrl+Shift+Alt+T and cover recent fixes
Plugin loader + runner shortcut moves to Ctrl+Shift+Alt+T (the old
Ctrl+Shift+T conflicts with the browser "reopen closed tab"). Loader
now imports runner.js *without* the cache-busting query so it picks
up the same module instance the test files register their suites
into — previously the shortcut opened a fresh runner with an empty
suites array and reported 0/0 passed.

New / fixed coverage:
- card-interactions: favorite toggle assertion is now case-insensitive
  (context menu was capitalized to "Unfavorite"/"Favorite" earlier);
  done-badge test guards against state.hideDone / hideTagged that would
  filter the test card off the board; new test for the always-on
  Mark done / Unmark done toggle on a card without due_date.
- context-menu: copy-text action exists; copy-text strips fenced code
  blocks, image markdown and inline #tags. Mocks navigator.clipboard
  via Object.defineProperty so the assertion works without a focused
  / secure-context clipboard.
- export: round-trip preserves a custom bucket color (covers the import
  handler regression that was wiping bucket colors on every test run).
- theme: design preset selection persists in localStorage across
  settings open/close, refreshPresetSelect ignores unknown keys.
  afterEach restores both hyprcrd-design-active-preset and
  hyprcrd-design so a test run can't permanently swap the user's
  design.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:50:25 +02:00
thandClaude Opus 4.6 ccd0477841 theme: persist active design preset across settings reopen
The <select> element survives settings open/close, so its lingering value
masked the otherwise-empty restore path on the second open and the
dropdown always reverted to whatever was last picked in the *current*
session. Track the active preset in localStorage on every change /
save, restore from localStorage first (it's the source of truth), and
clear the entry on delete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:50:07 +02:00
thandClaude Opus 4.6 ff3e0e8736 context-menu: add Copy text action and always show Mark done
- New "Copy text" entry next to "Save .md": copies title + blank line +
  body to the clipboard, with fenced code blocks (incl. slide-image),
  markdown images / links / formatting, and inline #tags stripped.
- "Mark done" is now visible on every card (used to require a due_date
  to even show up). On a card already tagged #done it flips to "Unmark
  done" and removes the tag. The original due-date-clearing behavior is
  preserved when transitioning a due card to done.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:50:01 +02:00
thandClaude Opus 4.6 26ffa0e71f 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>
2026-04-11 18:49:53 +02:00
thandClaude Opus 4.6 a29708df85 chore: cursor-blink on empty-state placeholders
Adds the cursor-blink class to "no X" / "select a Y" placeholders across
core (calendar popup, board preview, search preview, detail split, notifs)
and plugins (chat, email, gitea, slideshow). Visual consistency with the
rest of the pixel-font UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:49:25 +02:00
thandClaude Opus 4.6 2aec2d8f44 theme: exempt status-indicator animations from anim-speed override
Cursor blink, card-bar-pulse, sync spinner and the chat unread blink all
have deliberate cadences — letting body.anim-fast/anim-slow speed them up
turns them into unreadable strobes (or the sync icon spinning at 100ms).
Higher-specificity rules pin each one to its natural duration via
!important.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:48:54 +02:00
th f886ded054 fixes 2026-04-11 13:38:14 +02:00
th 22f73ab3b9 fixes + theme 2026-04-11 13:01:27 +02:00
th 5af49655d6 logseq import plugin 2026-04-11 01:45:36 +02:00
th 3ae84ccdd7 chat fix 2026-04-11 01:28:49 +02:00
th 262b853c5b slideshow 2026-04-11 01:22:52 +02:00
th 988a5410f7 chat drin 2026-04-10 22:49:25 +02:00
th ef238f3b9c image push fix 2026-04-10 18:50:04 +02:00
th 5b18c46ac2 multi-tenant relay und card-push to user 2026-04-10 18:23:07 +02:00
th 7219bae655 gitea brauchbar 2026-04-10 15:44:51 +02:00
th dd1afbbeb2 gitea plug 2026-04-10 14:46:50 +02:00
th 86508ffcca gitea 2026-04-10 11:46:15 +02:00
th cc97754ae6 email plugin 2026-04-10 10:42:18 +02:00
th 3f4ba3c0f4 plugin infrastruktur 2026-04-10 07:58:07 +02:00
th 1545b90f39 plugin infrastruktur 2026-04-10 07:57:53 +02:00
th dd2a995f6c canvas feature 2026-04-10 06:35:30 +02:00
th 0d494bf507 UI-tests und fixes aus tests 2026-04-09 23:21:37 +02:00
th b5e30afd0b UI-tests und fixes aus tests 2026-04-09 22:52:40 +02:00
th 968d980be1 UI-tests und fixes aus tests 2026-04-09 22:18:45 +02:00
th 73da8fb849 cleanup and refactor 2026-04-09 19:39:56 +02:00
th 5f9db690a4 bak 2026-04-09 19:14:52 +02:00
th 5aa3abc860 fixes 2026-04-08 23:13:11 +02:00
th cd9dc3e5bc settings 2026-04-08 23:05:41 +02:00
th e6243701a3 haufen sachen 2026-04-08 22:46:48 +02:00
th 416ca9e854 kalender und fixes 2026-04-08 17:24:55 +02:00
th 940d9bbde4 todo fixes 2026-04-08 15:55:43 +02:00
th fe93a0f80e settings fix 2026-04-08 14:41:45 +02:00
th 849aec84d4 dist und hilfe 2026-04-08 14:35:25 +02:00
th 60c9d56c05 dist 2026-04-08 14:16:07 +02:00
th e0fca064ae calender stuff 2026-04-08 14:04:23 +02:00
th 8e78468743 calendar 2026-04-08 07:47:00 +02:00
th 836826dba5 hilfe eingebaut 2026-04-07 23:57:30 +02:00
th ae6c818848 CDN entfernt 2026-04-07 22:56:45 +02:00