Commit Graph
63 Commits
Author SHA1 Message Date
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 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
th 5f9db690a4 bak 2026-04-09 19:14:52 +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 849aec84d4 dist und hilfe 2026-04-08 14:35:25 +02:00
th e85b5203cd bak 2026-04-06 18:09:04 +02:00