Release notes

Current version: 0.16.2

0.16.2

2026-10-01 current

Added

  • Each board remembers the filters and sort you last used on it, in the browser (localStorage, keyed by user and board), so they survive switching boards, a reload, and coming back tomorrow — instead of only lasting while the page stayed open. Precedence: a URL that carries its own filters still wins (so a shared or bookmarked link shows what it says), then what you last used on that board, then nothing. Clear also clears what was remembered. Nothing is stored server-side, and a browser with storage blocked simply doesn't remember.

0.16.1

2026-09-25

Added

  • The New board form now offers everything board settings does, so a board no longer has to be created and then immediately edited: which custom fields are enabled on it, whether the filter bar shows, and which of the filterable fields it contains. As in settings, the filter-field list follows the fields you tick above it, and is dimmed while the bar is off. Board-specific pick-list values stay in settings — they need the board to exist first — and the form says so.

0.16.0

2026-09-25

Added

  • Lanes can be reordered by drag & drop. Each lane header carries a grip (⠿) for board admins — drag it to move the lane, in either orientation. The lane list only accepts a drag while a grip is held, so dragging a card, renaming a lane or picking a lane colour can never pick up the whole lane, and the + Add lane tile stays out of the way. The new order is saved per lane and broadcast, so other viewers see lanes move live (a lane moved by someone else used to need a reload to show up in the right place).

0.15.2

2026-09-25

Changed

  • Filter bar: the row now reads as two jobs rather than one run of widgets — a Filter label opens it (matching the Sort label), thin grey dividers separate neighbouring filter controls, and a thicker rule sets the sort control apart from the filters.
  • Filter bar: a boolean field that carries an icon is now a single three-state button showing that icon instead of an Any/Yes/No dropdown, with no field name beside it — the name lives in the tooltip. Click to cycle: no border = not filtering, green border = only cards where it's on, red border = only cards where it's off. The state is also spelled out in the tooltip and to screen readers. Boolean fields without an icon keep the dropdown.

0.15.1

2026-09-25

Changed

  • Filter bar: pick-list values that carry an icon show as the icon alone, so a long list of values no longer stretches the bar. The label is the chip's tooltip on hover, and its screen-reader name. Values without an icon still show their label, as before.

0.15.0

2026-09-24

Changed

  • Board-level pick-list values are now an override of the global list, not a copy of it. A board starts from the global values and tailors them in place: rename a value, change its icon, remove one from its list, or add one of its own. A rename keeps the value's identity, so cards that hold it simply show the new label — where 0.13.0 created fresh values and quietly orphaned every card already using one. The start from global list / use global list buttons are gone; there is nothing to copy any more. Renamed values carry a reset link (back to the admin's value) and removed ones can be brought back from the Removed here: line.
  • Removing a value is refused only when cards on that board hold it, and the message now says how many ("EBWW" is used by 3 cards on this board.). Other boards' usage is no longer the board admin's problem.
  • Editing an inherited value works instead of silently doing nothing (the emoji picker used to change on screen and save nothing).
  • Migration 0008_option_override_rows converts the 0.13.0 copies into overrides by matching position, re-points existing card values to the global option, and turns copies the board had deleted into hidden values. Dry-run on the production database: renames, hidden values and all card values preserved, no orphans.

Added

  • While a sort is on, dragging a card around inside its own lane now says why it won't stick: the lane's drop frame turns red (instead of the usual accent colour) and the Sort direction button lights up red with a pulse and the hint "Cards are sorted — switch to Manual to reorder them by hand". Dragging to another lane keeps the normal accent frame, since that still works.
  • Empty lanes are much easier to drop into: while a card is in flight, an empty lane's drop area grows to the size of the card being dragged (its width in horizontal lanes) and shows a dashed outline, instead of the 20px sliver it used to be.

Fixed

  • Dragging a card in a sorted lane corrupted the board's stored order in the browser: the dragged order was written to the client store, so switching back to Manual showed an order the server didn't have, and the next manual drag could compute and save a wrong position. In-lane drag & drop is now inert while a sort is active (the stored order is left alone); moving a card to another lane still works.
  • Filter, search and sort no longer leak from one board to the next. The state is kept per board and swapped on navigation, so opening another board no longer re-sorts it, re-runs your search against it, or rewrites its URL. A link carrying its own filters still wins.
  • A failed search no longer shows the previous query's results as if they were current: the bar reports the failure, and an in-flight request is discarded as soon as the query changes.

0.14.0

2026-09-24

Added

  • Card sorting. A Sort control sits in the board toolbar (inside the filter bar when it's on, in a slim bar of its own otherwise): sort by added date, latest comment, or any custom field enabled on the board, ascending or descending via the ↑/↓ toggle. Pick-list fields sort by each value's place in the list — the board's own list when it has one — not alphabetically. Cards with no value (and cards with no comment) stay last in both directions; ties keep the board's own card order.
  • The sort is a per-viewer view setting that never changes the stored card order, and it rides in the URL next to the filters (?sort=priority:desc, ?sort=created:asc), so bookmarks and shared links restore it. Manual (drag & drop) is the default and is the stored order.
  • While a sort is active, dragging a card within a lane no longer reorders it (the sort decides the order), but dragging it to another lane still works and parks it at the end of that lane's stored order.
  • Cards now carry the values of every custom field enabled on the board (not just the filterable ones) so any of them can be sorted on.

0.13.0

2026-09-23

Added

  • Board-specific pick-list values. A global admin can tick Overridable per board on a pick-list field in Admin → Custom fields. Board admins then get a Board-specific pick-list values section in board settings where they add, rename, re-icon and delete values for that field on their board. A board inherits the global list until it adds its first own value; from then on its own list replaces the global one there. Two shortcuts: start from global list copies the global values as a starting point, and use global list drops the board's own values and inherits again (refused while a card still uses one of them, same safeguard as the admin list). Other boards and the global list are never affected.
  • Cards keeping a value from a list the board no longer uses still show it, marked (removed) (or in italics on the button control), so a value never silently disappears; clearing or changing it works as usual. Saving a value only accepts options in force on that board.
  • New columns custom_fields.options_overridable and custom_field_options.board_id (migration 0007_board_option_overrides; global options have no board). New board-admin endpoints under /boards/:id/custom-fields/:fieldId/options, and a resolveFieldOptions helper (with unit tests) that every reader of pick-list options now goes through — card overlay, filter bar and value validation.

0.12.2

2026-09-23

Added

  • Card overlay: an OK button at the bottom right, under the comment Post button, as a plain way to close the card (next to the existing ×, Escape and click-outside). It first accepts whatever is in flight — an open title or description editor, and a typed-but-unposted comment — so nothing is lost on close. Delete card stays at the bottom left, on the same row.

0.12.1

2026-09-21

Added

  • Board filter bar state lives in the URL, so bookmarks and shared links reopen the board with the same filters, using readable names: ?q=login&priority=high,none&blocked=yes&story-points=2..8&due-date=2026-01-01... Parameter names are slugs of the field names; pick-list values are slugs of the option labels (none = cards without a value); ranges are min..max with either end optional. Where a name can't be used unambiguously (two fields with the same slug, a name without letters/digits, or one that clashes with q / none) the first 8 characters of the id are used instead, and id-based links are always accepted. Renaming a field or option drops that one filter from older bookmarks; unknown or malformed parameters are ignored. The URL updates as you filter without adding browser-history entries, and opening/closing a card keeps the filters in the URL.

0.12.0

2026-09-21

Added

  • Board filter bar. A board admin can turn it on per board in Board settings → Filter bar. It sits above the lanes and contains:
    • a free-text search matching card title, description, and comments (case-insensitive; every word must match), re-run automatically when cards or comments change live;
    • one control per custom field the board admin picks for the filter bar (in the same settings section). Only fields enabled on the board and marked Filterable by a global admin in Admin → Custom fields can be picked: pick list → toggle chips (any-of, plus none for cards without a value); boolean → Any / Yes / No; text → contains; integer / decimal → min–max; date → from–to.
    • Non-matching cards are hidden; lane counters show shown/total and the bar shows N of M cards with a Clear button. Filter state is per browser session and not shared. Drag-and-drop still works while filtered (hidden cards keep their place).
  • custom_fields.filterable, boards.show_filters (migration 0005_add_filters) and board_custom_fields.filter_enabled (migration 0006_board_filter_fields, defaults to on for existing rows) columns, and a GET /boards/:id/search?q= endpoint. The card.customValueChanged WebSocket event now also carries the new raw value.

0.11.3

2026-08-12

Changed

  • Card overlay: small edit link next to the Description section title (visible to author/admin roles) as a discoverable alternative to the existing double-click-to-edit gesture.

0.11.2

2026-08-12

Added

  • Pick-list custom fields with ≤ 5 options, all labels ≤ 15 chars render as a compact segmented control (buttons with icon + label) instead of a <select>. Click a button to select; click the already-selected one to clear. Right-aligned to the row.

Changed

  • Card overlay layout refined: section content now indents 0.5rem right of the uppercase section titles (hanging-title look); custom-field labels use the plain body text color instead of muted gray; every custom-field row has a fixed 2.4rem height so short controls (checkboxes) no longer pull their row tighter than input rows; pick-list dropdowns match the label font size (0.9rem) and right-align their displayed value.

Removed

  • -alpha.1 suffix from the version number — the project is out of the pre-alpha convention.

0.10.1

2026-08-12

Fixed

  • Card overlay no longer flashes a "Loading…" placeholder on every save/refresh. load() now only sets loading=true on the very first fetch; subsequent refreshes (after a save, comment post, or WS echo) keep the current content visible while the new response arrives.

Changed

  • Custom field values in the card overlay right-align to a shared right edge — labels stay left-aligned. Integer/float inputs shrink to 8rem (fits 999,999,999 with padding + spinner) and render digits right-aligned; date shrinks to 10rem; text and picklist stay at 20rem; boolean checkbox is right-justified too.

Added

  • Dev-only page /dev/add-passkey to enroll a new passkey on an existing user account. Handy after copying a snapshot of the production DB (whose passkeys are bound to the prod RP hostname and refused by browsers on boards-dev.indri.be). Preserves the existing user id, team memberships, admin flag. Route + endpoints 404 in production builds (gated by SvelteKit's dev flag).

0.10.0

alpha.1 — 2026-08-11

Added

  • Boolean custom fields can carry an emoji icon (via the same EmojiPicker used for pick-list options). When a card's boolean value is true, the field's icon shows on the card tile next to the title — same treatment as chosen pick-list icons. custom_fields.icon added via migration 0004_add_custom_field_icon.
  • Live updates for custom field value changes: setting or clearing a card's custom value now broadcasts a new card.customValueChanged WebSocket event carrying the recomputed customIcons for that card. Other viewers see chips appear or disappear on the tile immediately, and any open card overlay refetches. Extracted computeCardCustomIcons helper (server-side) so board load and the broadcast handler share the same query logic.

0.9.3

alpha.1 — 2026-08-11

Added

  • Search field at the top of the emoji picker popover. Autofocused on open, filters the grid by keyword match (each of the ~125 emojis has hand-curated search keywords covering names, colors, and workflow concepts — e.g. "green" surfaces 🟢 🟩 💚; "fire" surfaces 🔥; "urgent" surfaces 🔥). Enter picks the first match; Escape closes.

0.9.2

alpha.1 — 2026-08-11

Changed

  • Emoji picker palette grew from ~100 to ~125: added colored dots (🔴 🟠 🟡 🟢 🔵 🟣 🟤 ⚫ ⚪) and colored squares (🟥 → 🟫 + ⬛ ⬜), plus a few reactions/math symbols (👏 🙌 ➕ ➖ ✖️ ❓ ❗) and remaining hearts (🤍 🤎). The grid now scrolls inside the popover (max-height 18rem) so long palettes don't spill off screen.

0.9.1

alpha.1 — 2026-08-11

Changed

  • The pick-list option icon field in Admin → Custom fields is now an emoji dropdown (EmojiPicker component) instead of a plain text input. Opens a popover with a curated ~100-emoji grid covering status, priority, progress, labels, people, areas, sentiment, and arrows, plus a "Clear" button. Closes on outside click or Escape.

0.9.0

alpha.1 — 2026-08-11

Added

  • Custom fields (phase 1). A global admin can define custom fields in Admin → Custom fields, with types text, integer, float (decimal), date, boolean, and picklist (list of predefined options with an optional emoji icon). Board admins pick which global fields to enable on their board from the board settings page. Each card's overlay shows the enabled fields with the right editor per type; values are stored per-card. Pick-list values that carry an emoji also render as a small badge next to the card title on the tile, so at-a-glance signals stay visible without opening the card. Delete safeguards: a picklist option cannot be deleted while any card uses it; a field cannot be deleted while it's enabled on any board or has any values. Live updates for custom-field value changes will land in a later release; reload / reopen the overlay to see other viewers' edits.
  • New tables: custom_fields, custom_field_options, board_custom_fields, card_custom_values (migration 0003_custom_fields).

0.8.0

alpha.1 — 2026-08-11

Added

  • Click a tag chip to highlight every occurrence of that tag across the board. Up to 5 tags can be highlighted at once, each drawn from a fixed palette (yellow → green → blue → pink → purple). Clicking a highlighted chip un-highlights and frees its color slot for reuse. Session-only, per-viewer; clicks don't open the card overlay.

0.7.0

alpha.1 — 2026-08-11

Added

  • Collapse/expand a lane via the chevron (▾ / ▸) in its header. In vertical mode the collapsed lane shrinks to a narrow strip with the title rotated vertically and the card count at the bottom; in horizontal mode the row simply shortens. State is per-user × per-board and persisted in localStorage under wwb.collapsed.<userId>.<boardId>, so reloads keep the same lanes hidden without leaking preferences across users on the same browser.

0.6.1

alpha.1 — 2026-08-11

Fixed

  • Tags added by editing a card's title or description through the overlay didn't appear on the tile until reload. The card.updated echo was being dropped for the origin client (the OPTIMISTIC_TYPES skip), and the overlay only applied title/description optimistically — never the server-computed tags. Fix: on card.updated for the origin, still apply the tags field from the patch.

Changed

  • Tag chips use a darker background so they stand out more against the card — #cbd0d8 on light, #454c5c on dark, with matching darker text.

0.6.0

alpha.1 — 2026-08-11

Added

  • #tag support on cards. Any #word in a card's title, description, or a non-deleted comment is extracted (case-insensitive, deduped, unicode-aware, mid-word foo#bar correctly ignored). Extracted tags render as small pill chips at the bottom of the card tile — light-gray background, dark-gray text (inverted for dark mode). Tags recompute server-side on card create/update and on comment add/edit/delete, and the new tag set is broadcast via WebSocket so all viewers see chips appear/disappear live without a reload. Backed by a new shared extractTags/unionTags module (11 unit tests) and a computeCardTags server helper.

0.5.3

alpha.1 — 2026-08-10

Changed

  • Card title in the overlay edit mode now uses the shared .inline-edit treatment with h1 sizing (1.35rem / weight 700), matching the rendered card heading and giving the same seamless in-place feel as the lane/card/comment edits.

0.5.2

alpha.1 — 2026-08-10

Changed

  • The comment-edit "blends with rendered text" styling is now a shared .inline-edit class in app.css and applied to the new-card title input, the lane rename input, and the add-lane input. Each carries the weight/size of what it will become (bold 600 for lane names, 500/0.95rem for card titles), so typing feels like editing the final rendered element rather than filling a form.

0.5.1

alpha.1 — 2026-08-10

Changed

  • Comment edit textarea now visually mirrors the rendered comment (same font, size, color, transparent background) with a subtle dashed border to signal editability and a solid accent border on focus. The textarea auto-sizes to fit the full content on open and while typing, and focus lands with the cursor at the end so you can continue typing immediately.

0.5.0

alpha.1 — 2026-08-09

Added

  • Comment editing: each comment now has an edit button next to delete, visible only to the comment's own author (admins have no override — literal owner-only). Clicking edit swaps the rendered comment for a textarea with Save / Cancel; Save PATCHes the comment body via a new PATCH /boards/:id/cards/:cardId/comments/:commentId endpoint (reviewer role + owner). Other viewers see the update live via a new comment.updated WebSocket event, and the tile's latest-comment preview is refreshed if the edited comment was the newest.

0.4.3

alpha.1 — 2026-08-09

Changed

  • Comment shortcut swapped: Shift+Enter posts the comment and closes the overlay, Enter inserts a newline. Multi-line comments were awkward when Enter submitted, so the textarea-native newline now wins.

0.4.2

alpha.1 — 2026-08-09

Added

  • Opening a card now focuses the comment textarea directly so you can start typing a comment without an extra click. Pressing Enter posts the comment and closes the overlay (returning you to the board); Shift+Enter inserts a newline. The Post button uses the same submit-and-close flow. Reader-only roles (viewer) see no textarea and get no auto-focus.

0.4.1

alpha.1 — 2026-08-09

Fixed

  • Arrow-key board navigation now works from any focus context, not just when a card is already focused. Previously the keydown handler was scoped to the .lanes container and gated on the target being a .card-tile/.add-btn, so pressing an arrow key while focus was on the Settings link, page body, or any other non-card control did nothing. A window-level handler now catches the first arrow press outside the board and jumps focus to the first card, letting subsequent arrows navigate normally. Guards preserve typing in inputs/textareas/contenteditables and stay quiet while the card overlay is open.

0.4.0

alpha.1 — 2026-08-09

Added

  • Keyboard navigation across cards. Focus a card, then press the arrow keys — the in-lane axis (Up/Down on vertical boards, Left/Right on horizontal) walks card → next card → … → "+ Add card" button at the tail. The cross-lane axis (Left/Right on vertical, Up/Down on horizontal) hops to the same slot in the adjacent lane, clamped to the last card if the target lane is shorter. Enter/Space on a card opens the detail overlay (unchanged); Enter on the "+ Add card" button starts the new-card form.

0.3.0

alpha.1 — 2026-08-07

Added

  • Lane color picker — each lane admin can pick a color from a 10-swatch palette (slate, red, orange, amber, green, teal, blue, indigo, purple, pink) or clear it. The colored lane gets a tinted header strip + accent border and each card in that lane gets a matching left border. Requires DB migration 0002_add_lane_color (adds a nullable color column to lanes).

Changed

  • Cards now pop against the lane background: dark surface (--card-bg) that contrasts against the lane panel in both light and dark modes, a subtle drop shadow, and a small lift + heavier shadow on hover. Previous flat treatment made cards visually blend with lane background, especially in dark mode.
  • Board page (/boards/[id]) uses the full viewport width instead of being capped at 1400px so lanes/cards get all the horizontal room available.
  • Display name is now "Work with Boards" (was "WorkWithBoards"). Applies to browser tab title, topbar brand, and the WebAuthn Relying Party name shown in passkey prompts. RP ID unchanged, so existing passkeys still work.

0.2.0

alpha.1 — 2026-08-06

Added

  • Horizontal boards now actually render horizontally: each lane is a full-width row with its title rotated 90° anticlockwise (reads bottom-to-top) on the left and cards stacking left-to-right in a horizontally scrolling strip. Drag-and-drop follows the layout direction. The laneOrientation='horizontal' setting existed since 0.1.0-alpha.1 but the client was rendering lanes as vertical columns regardless.

Fixed

  • WebSocket broadcasts silently dropped when HTTP handlers tried to notify subscribers. Root cause: SvelteKit's SSR bundle and the tsx-loaded server.ts entry each ended up with a separate copy of ws/server.ts, so the WS server populated one boardRooms Map while API routes broadcast into a different, always-empty one. Pinned the room/client state to globalThis so both module copies read/write the same maps.

0.1.0

alpha.1 — 2026-08-05

Initial working build.

Added

  • Passkey (WebAuthn) authentication via SimpleWebAuthn — first registered user becomes global admin automatically.
  • Team management with role-based permissions per team: viewer, reviewer, author, admin. Effective role on a board is the most permissive across the owner team and any shared teams. Global admins short-circuit to admin.
  • Board admin approval workflow — new registrations land in pending state until a global or team admin approves them and assigns team memberships.
  • Boards with configurable lane orientation (vertical or horizontal), drag-and-drop cards using string-based fractional indexing, and live WebSocket updates across viewers.
  • Card overlay with Markdown description, comments (author + timestamp), and file/image attachments (MIME sniffed by magic bytes, 25 MB cap, thumbnails via sharp).
  • Board settings page — rename, change owner team, change orientation, add/remove shared teams, soft-delete.
  • Presence indicators showing who's currently viewing a board.
  • Rate limiting on registration and login endpoints (per-IP token buckets).
  • Docker Compose deployment (app + Caddy for auto-HTTPS + backup sidecar snapshotting every 6h, retention 30).
  • Playwright E2E tests using the CDP virtual authenticator; Vitest unit tests for resolveBoardRole and fractional indexing (16 unit tests).
  • CHANGELOG-backed /releases page.

Fixed

  • Vite dev triggered a full-page reload loop because our WebSocket upgrade handler intercepted Vite's HMR socket and rejected it as unauthenticated. Scoped our handler to /api/ws and skipped any upgrade advertising a vite-* sub-protocol.
  • Vite dev also reloaded on every request because it watched SQLite's WAL/SHM files. Added data/, *.db*, .certs/ to server.watch.ignored.
  • Duplicate registration for an existing email silently returned isFirstUser: false and sent the (already-approved) global admin to the pending page. Registration now rejects known emails with a 409, and additional passkeys must be added from an authenticated session (no more account-binding of stranger devices).
  • Dashboard had no "+ New board" button so admins couldn't reach /boards/new. Added it, gated on "is a team admin" or global admin.
  • .env values (ORIGIN, RP_ID) were not loaded in Vite dev, so WebAuthn verification failed with an origin/RP-ID mismatch. Added dotenv/config at the top of vite.config.ts.
  • Board detail page threw HTTP 500 whenever a board contained comments — the latest-comment query chained .then(...) onto db...all(), but better-sqlite3's .all() returns an array synchronously (not a Promise), so .then was undefined. Awaited the query and filtered separately.

Changed

  • docker-compose.yml bind-mount source is now configurable via DATA_DIR (defaults to ./data). Recommended for prod: put data outside the project directory (DATA_DIR=/var/lib/boards) so rm -rf ~/boards can't nuke the SQLite file or attachments.
  • SimpleWebAuthn verification: passed requireUserVerification: false to both verifyRegistrationResponse and verifyAuthenticationResponse. Registration options request userVerification: 'preferred', so credentials may be stored without a UV bit; the library defaulted to true at verify time and rejected them with "User verification required, but user could not be verified" — users could register but never log in.
  • User approval UI on /admin/users: replaced the confusing "checkbox + disabled role dropdown" pair with a single per-team role dropdown that has "— not a member —" as the first option. Admins can now pick a distinct role per team in one click without having to enable it first. The Approve button stays disabled until at least one team role is picked (or global-admin promotion is checked).
  • Board URLs now use a name-derived slug instead of the raw UUID (e.g. /boards/sprint-planning instead of /boards/4eee9e31-3af6-…). Collisions get numeric suffixes (sprint-planning-2, -3, …). Existing boards get backfilled with their id's short prefix during migration 0001_add_board_slug. Old UUID URLs still resolve and 302-redirect to the slug URL for bookmark stability. Renames don't touch the slug by default — an opt-in checkbox in board Settings regenerates it.

Removed

  • Bundled Caddy reverse proxy. Bring your own — Nginx Proxy Manager, Traefik, standalone Caddy, Cloudflare Tunnel, etc. The app now publishes on host port ${APP_PORT:-3000} for whichever proxy you use to reach it. README.md has an example NPM-alongside-app override.

Real-time sync improvements

  • Comment deletion now updates the card tile on every viewer's screen — server computes the new "latest comment" post-delete and pushes it inside the comment.deleted event; store applies it.
  • New attachments now bump the card tile's attachment count live on every viewer — added attachment.added handler in the board store.
  • Acting user's own tile now also updates after they post/delete a comment or upload an attachment. Previously the originClientId echo-skip (needed to avoid drag-drop flicker) was too broad; now only optimistic-UI event types (card.*, lane.*) skip their own echo, while non-optimistic ones (comment.*, attachment.*) apply through.