Commit Graph
40 Commits
Author SHA1 Message Date
thandClaude Opus 4.7 2d58cd993e v2.0.10 — snappier boot + non-blocking relay push on save
DB
- Six indices on the cards table (position+created_at, bucket_id,
  parent_id, updated_at, caldav_uid, relay_uid). The /api/cards
  ORDER BY position was a full-table scan + sort on every board
  reload; now it's an index walk.
- PRAGMA synchronous=NORMAL (safe under WAL) + temp_store=MEMORY.

Save path
- pushCardToRelayRead and deleteCardFromRelayRead now run AFTER the
  response is flushed via fastcgi_finish_request (falls back to
  ignore_user_abort + flush on non-FPM setups). The user's save no
  longer blocks on the relay round-trip.
- cURL timeouts dropped from 10s/30s to 5s/10s, plus a 2s
  connect-timeout. Worst-case on a dead relay goes from ~160s down
  to ~15s — and now that worst case is in the background anyway.

Client
- loadCards phase-2 batches bumped from 25 to 500 cards per fetch.
  For 300 cards that's 1 HTTP trip instead of 12 — spinner wall time
  drops from ~3s to ~0.25s.
- renderBoard() is now requestAnimationFrame-coalesced. The boot
  sequence calls renderBoard from cache paint, meta paint, body
  paint, and the cards.reloaded plugin hook; they used to each
  trigger an innerHTML swap + layout pass. Now they collapse into
  one actual render per frame. Callers that really need a sync
  render (rare) can use the new renderBoardSync().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 20:41:48 +02:00
thandClaude Opus 4.7 f986a41ccc v2.0.4 — re-auth on 401, robust bucket-switch test
- auth: when a background poll or save returns 401 (session expired
  while the UI is still open), the api() helpers now prompt for the
  password instead of letting the save fail silently. Concurrent 401s
  collapse onto a single dialog via a shared reauthPromise, and each
  caller retries its original request once after login. /api/auth/* is
  excluded from the retry path. Extracts promptLogin() in app.js and
  exposes it as window._reauth so both the core util.js api() and the
  plugin-SDK hyprcrd.js api() can share the flow.
- tests: the "open-switches-bucket" detail test assumed buckets[0] was
  a plain top-level bucket, but the bar renders super-bucket parents
  with data-bucket-super and hides child buckets entirely, so the
  .bucket-tab.active[data-bucket] selector found nothing on setups
  with nested layouts. Test now picks two plain top-level buckets or
  skips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:50:44 +02:00
thandClaude Opus 4.7 d83de30d6d security: v2.0 — AES-GCM crypto, SSRF guards, rate limits, hardening
Breaking wire-format change: dual-read (V1+V2) / single-write (V2) so
existing user secrets, stored messages, and inter-tenant key keep
working. Old clients and relays lose cross-version chat until both
sides are upgraded.

Relay
- Switch clientEncrypt/masterEncrypt to AES-256-GCM with HKDF-derived
  per-ciphertext salts; keep CBC-V1 fallback on decrypt only.
- On-bootstrap migration re-envelopes tenant secret_encrypted and the
  inter-tenant key to GCM-V2 (idempotent).
- CORS: stop reflecting arbitrary Origin; allow own origin + optional
  RELAY_CORS_ORIGINS env allowlist; drop Access-Control-Allow-Credentials.
- Generic error messages replace "no such user: <name>" enumeration.
- Exception messages no longer echoed back; logged server-side.
- Collab-lock max hold capped at 10min so a member can't grief-refresh
  forever.
- Attachment filenames validated against hex.<ext> pattern.
- Passwords min length 10; /api/account/password rate-limited.

App / Electron client
- Shared hyprcrdRelayEncrypt/Decrypt helpers (GCM-V2 write, V1 read).
- Chat + collab-buckets plugins delegate to the shared helpers.
- Poll + push + attachments migrated to the dual-format path.
- /api/relay/settings GET returns has_key boolean; secret never echoed.
- Setup endpoint now CSRF-checked.
- SSRF: CURLOPT_PROTOCOLS restricted to HTTP(S) everywhere; bookmarks
  favicon fetch also blocks private/loopback/link-local.
- Login / password-change rate limiter moved to DB-backed IP keying.
- Passwords min length 10 throughout.
- Zip import enforces per-file (25MB) and total (500MB) caps, extension
  allowlist, no-overwrite.
- Verbose relay-push debug gated behind APP_DEBUG env.

Version bumped to 2.0.0 (protocol-breaking).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:02:04 +02:00
thandClaude Opus 4.6 d052fb5f9d shopping: ship as plugin, integrate phone view into relay UI, fix sync
Restructure the shopping-list feature end to end:

  - New public/plugins/shopping/ plugin with backend.php (proxy
    routes registered via PluginManager::onApiRoute), shopping.js
    (self-contained; injects the header Shopping button, owns the
    picker dialog + sync loop via hyprcrd.hooks), plugin.json and a
    placeholder css file. Core no longer knows about shopping —
    index.php loses the static button and the proxy block, app.js
    loses the initShopping wiring, the old js/modules/shopping.js
    is gone.
  - Phone view moves out of a standalone /shopping page and into
    the relay UI as a regular `view-shopping` behind a nav-burger
    entry (relay/ui/layout.php + app.js + style.css). Same session
    auth as compose/chat/account — no separate login. Auto-refresh
    every 4 s while the view is active; no manual refresh button.
    Headings "# h1 … ###### h6" render between items for grouping.
    Orphan section headings (no open items under them) are hidden.
  - Sync: drop timestamp cursors (broke across SQLite-UTC / PHP
    timezones and silently blocked pushes after a pull). Both sides
    diff the (uid, title, body) triple instead — timezone-agnostic
    and authoritative. Client ticks push-before-pull so fresh local
    edits land on the relay before a stale pull can overwrite them.
    Relay drops its "server newer" guard and stamps server times
    with gmdate so anything left comparing to SQLite is UTC.
  - Client also ticks on cards.reloaded, not just sync.tick, so
    any edit path (editor save, checkbox toggle, context menu,
    undo) pushes the update immediately.
  - Tests: updated for the plugin (still uses the same selectors
    because the plugin injects the same ids), plus a plugin-
    registration check and "(no active list)" option assertion.
  - Help section copy updated — references the Shopping header
    button and the shopping tab in the relay UI rather than the
    gone /shopping URL.

Both #einkauf and #shopping work as trigger tags; phone is
check-only, editing stays in hyprcrd. Version bumped to 1.9.21.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 01:24:58 +02:00
thandClaude Opus 4.6 670f125627 shopping list: phone-synced card via relay
Pick one #einkauf-tagged card as "active"; its body is pushed to the
relay on every sync.tick and rendered on a phone-friendly session-
auth'd page at /shopping. Phone shows only the open "- [ ]" items as
tappable rows — tapping flips the line to "- [x]" on the relay in
place. The next client tick pulls those toggles back and PATCHes the
local card, so hyprcrd stays the source of truth for item text.

  - Relay: shopping_state table (one row per tenant), POST/GET
    /api/shopping for content push/pull, POST /api/shopping/check
    for phone toggles, GET /shopping for the phone UI.
  - Client: hamburger menu in the header with an "Einkaufsliste"
    entry that opens a picker listing all #einkauf cards with a
    radio for the active one. Timestamp cursors in localStorage
    keep push/pull idempotent.
  - Phone can only toggle, not edit text — edit stays in hyprcrd,
    which makes the merge trivial (last-writer wins by timestamp).

Multi-card support is there (any card with #einkauf is a candidate)
but only one is active for the phone at a time, per the spec.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:32:42 +02:00
thandClaude Opus 4.6 41272bd9ce board: spinner during first load instead of "no cards found"
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>
2026-04-15 21:16:45 +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 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 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 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 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 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 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 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 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 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 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 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 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 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
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 262b853c5b slideshow 2026-04-11 01:22:52 +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 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 e6243701a3 haufen sachen 2026-04-08 22:46:48 +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 719f9d51ff tags 2026-04-07 21:58:45 +02:00
th a9603aa0e8 bak 2026-04-07 17:47:09 +02:00
th 9c71e65041 bak 2026-04-06 21:39:37 +02:00
th 0d4d369870 bak 2026-04-06 20:27:19 +02:00
th e85b5203cd bak 2026-04-06 18:09:04 +02:00