Skip to content

Releases: florian-strasser/LokalBoards

v0.23.0

Choose a tag to compare

@florian-strasser florian-strasser released this 12 Aug 14:53

Breaking

  • LocalBoards is now LokalBoards. "Local boards" is a phrase the dictionary already owns — it competes in search with local government boards, local message boards and local bulletin boards, and a purely descriptive name is close to unregistrable as a trademark. LokalBoards is a near-unique string, it nods to where the project comes from, and it sets the naming convention for the projects that follow. Better now, at two stars, than after the name has spread.

    Nothing about a running instance changes. The app reads its display name from NUXT_APP_NAME, and the database, volume and image names in the documentation are only suggested values — no instance takes them from this repository. An existing deployment keeps working untouched; renaming anything is optional and entirely up to the operator.

    What did move: the canonical site is now lokalboards.com (the old domain redirects), the repository is florian-strasser/LokalBoards — GitHub keeps the stars, issues and pull requests and permanently redirects the old URL, including existing git remotes — and the published image is florianstrasser/lokalboards — the maintainer's own namespace, shared with the projects that follow, rather than one named after a single app. The old image path is no longer updated, so a docker pull localboards/localboards needs changing to keep receiving releases; pinned deployments keep running on whatever tag they already have.

    Entries below this one are left as they were written. They describe releases that shipped under the old name, and rewriting them would misreport what happened.

New Features

  • A board tile can wear a colour instead of a picture. The board settings gained a Colour row beside the thumbnail: twelve presets covering the hue circle, plus a pipette that opens the system colour picker for anything else. The first swatch is the default and leaves the tile in the app's own colour, which is what every existing board keeps — nothing changes until you pick something.

    A picture covers the whole tile, so a colour behind one could never be seen. Rather than let the two quietly fight, they are one choice: picking a colour clears the image and picking an image clears the colour, so the dialog always shows what the tile will actually look like.

    Any colour stays readable. The tile works out whether white or near-black gives better contrast against what you picked — by measuring the actual contrast ratio, not by thresholding brightness, which is the difference between getting #00ff00 and #0000ff right and getting them backwards — and the name plate, the "shared" badge, the unread dot and the avatar rings all follow it. On a dark board that is the familiar white plate with the board's colour as its text; on a pale yellow the plate flips to dark with yellow text, instead of turning into white-on-white. The twelve presets are all chosen to clear 4.5:1 against white, with a unit test that fails if a future edit sneaks a brighter shade into the palette. Hovering shades the colour the same direction the primary colour shades — darker on the light theme, lighter on the dark one — so a coloured tile behaves like every other one.

    The colour is a new nullable color column, added by a migration that existing installations pick up on their next start; boards that predate it simply read as "no colour". It is validated in one shared place used by the picker, the tile and the API, so a value can never be accepted by one and refused by another — which matters here, because the colour ends up in a CSS custom property and only #rrggbb may ever reach the stylesheet. Available over the REST API and through the createBoard/updateBoard MCP tools, which now also report a board's image and color back rather than only accepting them. Translated into all seven languages.

Fixes

  • Attachments that can't be displayed now simply download. Clicking a spreadsheet, a Word file or a zip asked for permission to open a popup first, and only downloaded the file once that was allowed — in Safari a dialog stood between the click and the file every time. The click fetched the attachment and then called window.open, by which point the browser no longer connected the new window to the click that caused it and treated it as a popup. A download link is now built and followed straight away, with nothing awaited in between, so there is no popup to allow and no tab that flashes open and closes. Verified in both Chromium and WebKit: the file arrives, no extra tab is opened, and the page underneath doesn't move.

    Only images and PDFs can actually be shown in a browser, and both keep their behaviour — an image opens in the lightbox, a PDF in a new tab. Everything else was already meant to download; it just took a detour to get there.

    Two things improve along the way. The download now carries the attachment's original filename, so a spreadsheet saves as Quartalszahlen Q3.xlsx instead of the 32-character storage name the file has on disk — including names with umlauts or other non-ASCII characters, which are sent in both the plain and the RFC 5987 form of the header. And the file is streamed from the server instead of being pulled through the browser's memory as a base64 data: URL, which for a large attachment meant holding several copies of it at once.

v0.22.3

Choose a tag to compare

@florian-strasser florian-strasser released this 10 Aug 08:58

Security

  • Cleared five advisories across both lockfiles. Dependabot flagged three, npm audit surfaced two more; both projects now report zero known vulnerabilities.

    • dompurify — an XSS where a hook that removes an element during IN_PLACE sanitisation leaves the removed element's descendants attached and executable, so a nested <img onload=…> fires after sanitize() has returned (GHSA, moderate). It reaches us through isomorphic-dompurify, which is what sanitises card descriptions, comments and notification messages before they are rendered with v-html. Our own use is not the vulnerable shape — sanitizeHtml calls sanitize() with an allowlist, not IN_PLACE, and registers no hooks — but the package is on the one path that stands between a collaborator's stored Markdown and another user's browser, so it is pinned to the fixed 3.4.13 rather than argued around.
    • js-yaml → 4.3.1 and nanoid → 3.3.18, both high-severity denial of service (quadratic CPU consumption resolving !!omap; a custom generator looping forever when size is zero). Both are build-tool transitives — js-yaml via the JSON-schema ref parser, nanoid via PostCSS — and neither had an alert open yet; they were taken along because they had fixes waiting.
    • image-size — two high-severity infinite loops in the ICNS and JXL/HEIF parsers, reported twice against the documentation site. There is no patched release: every published version is affected, upstream has shipped nothing, and the package sits three levels down under @nuxtjs/seonuxt-seo-utils. It turned out that module was never registered in the docs nuxt.config.ts in the first place — Nuxt does not load modules just because they are in package.json — so nothing it provides was ever running: the live site has no sitemap and emits no og: tags, and its robots.txt is the static file in public/. The dependency has been removed, which takes the advisory with it and drops 50 packages from the docs tree. The inert site: block in the config is left in place for whenever the module is actually wired up; a sitemap can be had from @nuxtjs/sitemap alone, which does not pull image-size in.

    Verified past the audit report: the app builds, all 126 tests pass, and the documentation site builds without the removed module.

New Features

  • The documentation site now has a sitemap. @nuxtjs/sitemap replaces the @nuxtjs/seo bundle that was removed above — it is the one piece of that bundle the site actually wanted, and it brings no image-size, so the advisory does not come back. It is registered in modules this time, which is what the old dependency never was.

    Page scanning finds the static routes, but the documentation and API pages are all served by a single dynamic route each (app/pages/docs/[slug].vue), so their URLs only exist as Markdown files. A small Nitro route reads them back out of the content database and hands them to the module as a source; the legal pages are left to page scanning, because their content paths (/legal/privacy-policy) are not the routes they are served at. The result is 26 URLs — the landing page, both section indexes, eleven documentation pages, ten API pages and the two legal pages — and every one of them was requested against the built server and returns 200 with its content rendered.

    Two things were wrong in the site config and are fixed: the canonical URL said www.localboards.de, which 301-redirects to the apex host, so every entry would have pointed at a redirect; and the sitemap now honours the trailing slash the live site canonicalises to, so the listed URLs are the ones actually served rather than another redirect hop. public/robots.txt points crawlers at the sitemap.

Fixes

  • The documentation site's NUXT_APP_NAME did nothing. Its nuxt.config.ts declared app.head twice; in an object literal the second key wins outright, so the first block — the one that read the app name from the environment — was silently discarded, along with the environment-driven <html lang> that the second block happened to repeat. The two are now one block, and the app name additionally feeds the %s | … title suffix instead of being hard-coded there. Setting NUXT_APP_NAME at build time now really does rename the site, in the page titles and the suffix behind them.

    The merge itself is deliberately behaviour-neutral: with no environment variables set, the rendered <head> and <html> tags of the landing page, a section index, a documentation page and a legal page are byte-identical to what the old config produced. The duplicated charset/viewport entries were dropped from the meta array because the dedicated charset/viewport keys in the same block already emit them — verified in the output, which contains exactly one of each, before and after.

v0.22.2

Choose a tag to compare

@florian-strasser florian-strasser released this 06 Aug 20:00

Fixes

  • A comment found by search now takes you to the comment, not just its card. The result linked to ?card=…, so on a card with a long thread you landed at the top and had to hunt for the line you had just searched for. Comment results now link to ?card=…&comment=…: the card opens, the comment scrolls into the middle of the view and its border is marked in the primary colour for a few seconds, then settles back. It works on a cold deep link as well as from the open app, waits for the authoritative comment list (the comment may not be in the board's prefetched copy at all), and the parameter is dropped from the URL when the card is closed, so a reload doesn't jump again. An unknown or stale comment id simply opens the card as normal.

v0.22.1

Choose a tag to compare

@florian-strasser florian-strasser released this 05 Aug 21:44

Security

  • Updated Nuxt to 4.5.2, clearing eleven advisories. They landed together against 4.5.0 and cover the framework's server-side rendering and routing: a critical unauthenticated DevTools RPC allowing arbitrary command execution on a developer's machine, server-side remote code execution through runtime template injection in server island props, a runtime payload cache that could disclose one user's SSR data to another user (or to unauthenticated clients), route rules silently dropped for mixed-case paths — which bypassed appMiddleware auth gates, an incomplete fix for CVE-2026-53721 — an unauthenticated out-of-memory crash via unbounded v-for expansion in island rendering, and unauthorised component instantiation via server island props.

    The same advisories applied to the documentation site, which was still on 4.4.8; it is now on 4.5.2 as well. Its lockfile had to be re-resolved from scratch, because the pinned rolldown blocked the upgrade as a peer conflict — the security overrides from earlier releases (brace-expansion, postcss, sharp, minimatch) were checked afterwards and all still hold. Both lockfiles report zero known vulnerabilities.

    Verified past the audit report: the app builds, all 126 tests pass, and a running instance serves the dashboard, a board, a deep-linked card, the health endpoint and the search API — with the card modal, the search panel and navigation exercised in a browser with no console errors, since these advisories are in exactly those rendering paths.

v0.22.0

Choose a tag to compare

@florian-strasser florian-strasser released this 05 Aug 21:43

New Features

  • Search across everything you can see. A search field sits in the header between the logo and the nav — same height as the nav beside it, with a border in its own background colour that turns primary on focus, like the card description and comment editors — type in it and results drop down underneath as you go, grouped into boards, cards, comments and attachments. On a phone, where there isn't room beside the nav, the field wraps onto its own full-width line, and the placeholder — a full sentence naming what gets searched — fades out at the right edge instead of being chopped off mid-word. The fade is applied only when the text genuinely doesn't fit (measured against the field, so it appears and disappears as the window is resized) and only while the placeholder is showing, so neither a placeholder that fits nor a typed query is ever dimmed. It doesn't only match names — a card is found by the text of its description, by any comment on it, or by the filename of an attachment, so "where did we discuss that?" is one search rather than a hunt through boards. Each hit shows where it lives (board · area, or comment author · card · board), a snippet of the surrounding text when the match is buried in a description or comment, and the matched term highlighted. Each result is drawn as the thing it found, using the app's own components' styling: a board hit is the same grey box a card uses, with the board's name and the avatars of everyone on it, a card hit is a card tile — status circle, checklist progress (green when complete), comment and attachment counts, the due date (emphasised when it has passed) and the assignee's avatar, from the same parser and formatting the board uses — a comment hit is a comment bubble with the author's avatar and name underneath, the bubble's border lighting up on hover rather than a slab of colour behind the row, and an attachment hit is the file row from the card. Results are recognisable at a glance instead of being one more line of text in a list. Snippets read as prose rather than as Markdown source: link and image labels survive, task items keep their state as ☑/☐, and headings, bullets, quotes and emphasis markers are dropped — a snippet is a fragment cut from the middle of a document, so its block structure is usually broken anyway. Clicking a result opens the board — or the card itself, straight into its modal. The panel closes when you click away or pick a result, and comes back when you return to the field; Escape clears the field, as it does in any search box.

    Results are strictly scoped to what the caller can already open: every query carries the same "owned by me or shared with me" condition, and being an admin grants nothing extra, matching how board access works everywhere else. Typing is debounced into a single request and responses are sequenced, so a slow earlier reply can't overwrite a newer one; searches shorter than two characters never reach the database; and % and _ are escaped so they're searched for literally instead of matching everything. Translated into all seven languages.

  • Checklist progress on the board. A card whose description contains a task list now shows how far it has got — 0/3, 2/5 — next to the comment and attachment counts on its tile, so a board can be scanned without opening anything. The counter turns into the secondary color once every item is ticked.

    It updates live for everyone: ticking a box in the card, editing the description, or adding and removing items all move the number immediately, in every open browser, without a reload. That comes for free from where the number is derived — the card's stored Markdown, which the board already loads and which every one of those paths already keeps in sync. Nothing extra is stored, no new column, no extra request. Cards without a checklist look exactly as before. The parser handles the different bullet markers, ordered items, nesting and uppercase [X], and ignores task-list syntax inside fenced code blocks; it's covered by unit tests.

Internal

  • The demo capture gained a search view (30-search), so the gallery and the README show the feature rather than just the empty field, and one seeded comment now mentions the logo so a single search demonstrates hits across cards, comments and attachments at once.

v0.21.5

Choose a tag to compare

@florian-strasser florian-strasser released this 04 Aug 09:12

Security

  • Cleared eleven advisories in transitive dependencies. Dependabot flagged seven and npm audit surfaced four more once those were resolved; all are pinned to fixed releases through overrides, and both lockfiles now report zero known vulnerabilities.

    • undici — five advisories (one high): cross-user information disclosure and a parse-time crash via degenerate private cache directives, response desynchronisation via the retry interceptor, CRLF injection through a blob-like body type, cache-key confusion from whitespace around = in Cache-Control, and cookie-attribute injection. Two copies exist in the tree, Nuxt's 8.x and the MCP toolkit's 7.x, and both lines were affected — pinned to 8.10.0 and 7.29.0 respectively rather than collapsing them onto one major.
    • ip-address — two SSRF/trust-boundary bypasses (IPv4-mapped/NAT64 misclassification, and a CIDR suffix suppressing special-use classification), via the MCP toolkit → 10.4.0. Worth noting these do not weaken LocalBoards' own webhook SSRF guard, which classifies addresses itself in server/utils/webhookTarget.ts and never used this package.
    • brace-expansion — a second DoS advisory (GHSA-rgw5-rvv9-x895) that bypasses the mitigation shipped in v0.21.1 → 5.0.9, in the app and the docs site.
    • fast-uri (host confusion via a backslash authority introducer), hono (ReDoS in the CORS middleware) and postcss (arbitrary .map read when from is unset, an incomplete fix of the earlier advisory) → 3.1.5, 4.13.0 and 8.5.25.

    Verified beyond the audit report: the app builds, all tests pass, and a running instance still serves the dashboard, boards, the deep-linked card, the health endpoint and an MCP initialize handshake — the last one matters because hono and ip-address are the MCP transport's own dependencies.

Fixes

  • Ticking a card off now shows up in its timeline straight away. Marking a card done (or reopening it, setting a due date, assigning it) records an activity entry on the server, but the open card's "Comments and activity" list only read that list when the card was opened — so the new line appeared only after closing and reopening the card. The timeline now re-reads the activity whenever the card is saved, and also when the change arrives from someone else over the socket, so a card left open picks up a colleague's changes too.

v0.21.4

Choose a tag to compare

@florian-strasser florian-strasser released this 02 Aug 19:20

Improvements

  • Every e-mail now looks like the notification e-mail. The welcome mails (self sign-up and admin-created), the password-reset link, the board invitation and the account-deletion notice were all bare <p> tags with a raw URL pasted in — while the notification digest had a proper layout. They now share one shell (server/utils/emailLayout.ts): the same font stack, spacing and 600px measure, with the action as a real button and the raw link kept underneath in small print for clients that strip it or for copying by hand. The button label is translated per action ("Sign in", "Open board", "Set a new password") in all seven languages. Like the notification mail, they set no page background or text colour, so they read correctly in both light and dark mail clients. The notification digest now draws its shell and button from the same place instead of its own copy. Verified by sending all six through a local SMTP server and rendering what actually arrived.

Fixes

  • The header's notification bell now stops glowing as soon as everything is read. Opening a card marks its notifications read on the server, and opening a board does the same for its board-level ones — but the bell kept a private copy of the list and never heard about it, so its unread dot stayed lit until the page was reloaded. The bell's notifications now live in shared state that the board page refreshes right after the server confirms the change. The dot follows the real count rather than switching off on the first read: with three unread items it stays lit through the first two cards and only goes out once the last one is read.

v0.21.3

Choose a tag to compare

@florian-strasser florian-strasser released this 29 Jul 10:55

Fixes

  • Dates now render in the instance's timezone and language, which also fixes a hydration mismatch. Every displayed date was formatted with toLocaleString(undefined, …), which resolves to the renderer's locale and timezone — the Node process on the server (en-US/UTC inside the Docker image) and the browser on the client. The same instant therefore produced different text on each side, and Vue reported "Hydration completed but contains mismatches". It was most visible when opening a board straight to a card (?card=), which server-renders the card with all its comment and activity timestamps, but any board with a due date on a tile hit it too.

    Dates are now pinned to the server's timezone (TZ) and the configured NUXT_LANGUAGE, carried to the browser the same way the UI language already is. Both sides produce identical text, so dates render during SSR like everything else — no placeholder, nothing appearing after hydration — and everyone working on a board reads the same wall clock instead of each browser showing its own. Verified with a server in UTC and browsers in Berlin, New York and Tokyo: all four see 03.08.2026, 10:24, with no mismatch across 16 page loads; switching the server to TZ=Europe/Berlin moves every viewer to 12:24, and NUXT_LANGUAGE=en reformats it to 08/03/2026, 12:24 PM.

  • A card opened after someone else commented showed the old comments. The board prefetches every card's comments when it loads, so the modal can open without a round trip — but another user's comment reaches your browser only as a count over the socket, never the content. The tile's badge went from 1 to 2 while the prefetched list stayed at 1, and opening the card rendered that stale list; only a full page reload reconciled them. The comment section now re-fetches the card's comments when it opens: the prefetch still renders instantly and the authoritative list replaces it a moment later, and it's handed back up so the board's cached card and the tile's badge follow. Only an actual difference triggers an update, so reopening an unchanged card is still a no-op. While a card is open, live updates were already covered by its own socket room.

v0.21.1

Choose a tag to compare

@florian-strasser florian-strasser released this 26 Jul 07:16
  • Cleared the new high-severity brace-expansion advisory (GHSA-mh99-v99m-4gvg / DoS via unbounded expansion length). expand() caps the number of results it produces but not their total length, so chained brace groups can exhaust memory and crash the process. The fix is 5.0.8, and it exists only on the 5.x line — the 2.x line's newest release (2.1.2) is still affected with no backport. Simply forcing 5.0.8 everywhere breaks the old consumers: 5.x's CommonJS entry exports { expand } instead of a callable module, so minimatch@5/@9 fail with expand is not a function (verified, not assumed). The 2.x requirement was pinned by those old minimatch copies inside Nitro's archiver chain, so they're lifted to minimatch@^10.2.5, which takes the fixed brace-expansion line — leaving exactly one copy of each in the tree. Upgrading archiver itself to 8.0.0 was tried first and rejected: it's ESM-only without a default export, which breaks Nitro's import archiver from "archiver". Verified by round-tripping a real zip through both affected code paths (.directory()readdir-glob, .glob()glob).

  • Cleared four vulnerabilities in the documentation site's dependencies (docs/): postcss (path traversal via source-map auto-loading, GHSA — fixed in 8.5.18), valibot (flatten() throwing on inherited object property names, fixed in 1.4.2), the same brace-expansion DoS, and sharp (inherited libvips CVEs below 0.35.0). The docs site is a separate project that isn't part of the deployed app, but the tree is clean again and the docs still build. Both lockfiles now report zero known vulnerabilities.

v0.21.0

Choose a tag to compare

@florian-strasser florian-strasser released this 25 Jul 22:50

New Features

  • Arrange your dashboard: sort boards and group them. The dashboard is now one space you organise yourself. Drag boards into any order, create named groups (e.g. "Work", "Clients", "Personal") and drag boards into them, reorder and collapse groups, and rename or delete a group at any time (deleting a group keeps its boards — they drop back to ungrouped). The old fixed "Your boards / Shared boards" split is gone: owned and shared boards live together and a small Shared badge marks the ones you don't own, so a single group can mix both.

    The arrangement is entirely your own. It's stored per user against each board, never on the board itself, so two people who both have access to the same shared board can sort and group it completely differently — one person's layout never affects anyone else's. New and newly-shared boards appear ungrouped at the top until you file them. Leaving a board, or a board being deleted, quietly removes it from your arrangement.

  • Cards now keep their own history. Until now a change to a card only existed as a notification or an e-mail — transient, per-recipient, and gone once read. Card changes are now recorded permanently on the card and shown in the "Comments and activity" section, interleaved with the comments in one chronological timeline: who created it, marked it done or reopened it, moved it between areas (naming both), assigned it to someone, and set or cleared a due date. Each entry carries the actor's avatar and a timestamp, so opening a card months later tells you how it got to where it is. The history is stored structured rather than as prose, so it's rendered in the reader's own language regardless of who performed the action.

  • The notification list was rebuilt to read like the comment section. Each entry now leads with the actor's avatar and name, followed by what they did and when, and a comment appears in its own bubble underneath instead of being crammed into one line of text. Unread entries carry a dot, and an empty list says so rather than showing nothing. Notifications now record who triggered them (a new actorId), which is what makes the avatar possible — previously the only trace of the actor was their name embedded in the message text. System notifications (due reminders) show as LocalBoards, and notifications created before this release still show the actor's name parsed from the message.

Fixes

  • Fixed comment notifications that showed an empty card name (on card ""). A card with a blank name — reachable via the Trello import, which inserted names unchecked — produced a message the display regex couldn't parse, so the name silently vanished. The parser now handles it and falls back to "Untitled card", which also repairs notifications already stored, and the MCP comment path no longer writes an empty name in the first place.
  • Tooltips no longer make the board's horizontal scrollbar flicker away on Safari. The tooltip was rendered inside the hovered element, so on a board wide enough to scroll, hovering a button (e.g. an area's delete icon) triggered a WebKit repaint that dropped the scroll area's scrollbar until the next scroll. Tooltips now render into <body> with fixed positioning, so they're outside the scroll container entirely — which also stops them being clipped inside modals and the board's scroll area.

Performance

  • Added the database indexes the query patterns actually need. The baseline tables shipped with only their primary keys, so every join down the board → areas → cards → comments/attachments chain, every session and API-key lookup, and every membership check was a full table scan. Migration 0012 adds secondary indexes on the columns filtered and joined on — session(token), apikey(key), invitations(board) / invitations(user), areas(board), comments(card), attachments(card), boards(user), a composite notifications(userId, isRead, boardId), and a few more. Verified with EXPLAIN: the hot queries now do index lookups instead of scans. It's a no-op where an index already exists and is safe to re-run.