Changelog
Version v0.5.0-beta
v0.5.0-beta
This release contains breaking changes. Read the upgrade notes before updating.
I had originally planned on this release taking a lot longer, but it is weekend after all. Many improvements are borrowed from other project i’ve worked on recently, and I wanted to get them out sooner rather than later.
This release brings a personally long awaited overhaul to the table implementation, mobile UX and performance.
It also includes several security hardening tweaks (these are breaking).
Highlights
Table state moved to the URL
Every bit of table state now lives in the URL. Page, page size, sorting, filters, search, which rows are expanded and which columns are visible. Refresh the page, follow a live update, or send someone a link, and you get the same view back.
This fixes a lot of previous issues where various state would reset on reload or SSE events.
Filtering has furthermore been simplified as a single filter button with operator and values. It utilises full faceting from Tanstack Table to prepopulate values with what’s actually present in your data.
Each column decides which operators make sense for it. Text columns get contains, equals, starts with and so on, numbers get comparisons, and dates get before, after and between. Active filters sit next to the search box as chips, and clicking one reopens it for editing.
The decisions table is down to five columns so it fits a laptop screen. Status, origin, country and created date are still fully filterable and sortable, they are just not given a column any more. Clicking through from a host now lands on that host’s active decisions, or its expired ones when nothing is active.
Revamped mobile UX
Previously on mobile it was a downscaled table. This release brings cards on mobile which are far better utilizing the screen space.
For more screenshots, check the updated README.
Each row becomes a card with the IP as its title, the decision type as a badge, the remaining fields labelled underneath, and the same delete and CTI actions. Tapping a card expands it in place with the full alert evidence, exactly like the desktop table.
Since there are no column headers to tap, sorting moved into its own menu that lists every sortable field, including the ones that have no column. Filtering is the same chips and the same Filter button as on the desktop, so a filtered link opens the same view on either. The columns menu is hidden, since cards have no columns to toggle.
Opening the sidebar also behaved badly on a phone. It focused the theme toggle in the footer, which popped that button’s tooltip open over the navigation on every single open, and the open tooltip then swallowed the Escape that should have closed the drawer. It now focuses the first navigation entry, and Escape closes it.
TanStack Table v9 and the React Compiler
The table moved to TanStack Table v9. Features, row models, filter and sort functions are registered once and the column definitions are typed against that registry, so a column’s filter type and its filter function can no longer drift apart.
The more visible win is that every "use no memo" directive is gone. The table components were the last part of the app opted out of the React Compiler, because the old API read table state in a way the compiler could not track. Row and header cells now read it through subscription boundaries, so the compiler handles the whole app.
Both layouts also virtualize once a page gets long, measuring real row heights rather than guessing at them. Setting the page size to All stays responsive no matter how many decisions you have, on a phone as well as a desktop.
Structured logs
Server logs now go through a proper logging module instead of console. Named loggers, six levels behind a LOG_LEVEL threshold, and a LOG_FORMAT of human for a terminal or json for a log collector. Warnings and errors go to stderr, and stack traces only appear at debug level.
LOG_LEVEL=info # fatal, error, warn, info, debug or traceLOG_FORMAT=human # human for a terminal, json for a log collectorSign-ins, failed sign-ins, users created or deleted, and decisions removed from the dashboard are all logged with who did it. A LAPI that stays down is reported once, again if the error changes, then every tenth poll, with a line when it recovers, instead of repeating the same error every minute.
The server also shuts down properly on SIGTERM now. It stops the poller, closes the live streams and disconnects the database, so the process exits on its own instead of waiting to be killed.
Smaller things
-
The sidebar shows the version you are running and links to the releases page. It turns into “Update available” when a newer release is tagged, and
UPDATE_CHECK=falseturns that check off -
Relative times (“2 minutes ago”, “in 3 hours”) update live instead of freezing at whatever they said when the page loaded
-
Expanded rows stay inside the width of the table instead of stretching it
-
A
/api/healthendpoint that never touches the database, used by the container healthcheck -
An invalid environment variable now stops the container and names the variable, instead of a server that answers 500 to every request
⚠️ Breaking Changes
The container runs as a normal user
The image is now built on Alpine, pinned by digest, and runs as the node user. npm, corepack, yarn and the TypeScript compiler are gone from the runtime, and the Prisma CLI keeps only what migrate deploy needs. The image is down to 630 MB. The compose file mounts the root filesystem read-only and drops every capability.
Because of that, a database created by an earlier release is owned by root and cannot be opened. Fix the volume once before starting the new version:
docker compose run --rm --user root --cap-add CHOWN --entrypoint chown dashboard -R node:node /dataThe container prints this command and exits until it is done. Volumes from 0.2.x or earlier have no migration history and must be recreated with docker compose down -v.
Configuration moved to .env
The compose file no longer holds your environment values. After downloading the new compose file, copy .env.example to .env next to it and move your settings there, or keep your old compose file with the values inline.
The file is optional now. Without one the container generates a session secret, keeps it in the volume at /data/auth-secret, and the dashboard comes up with a login page and syncing off until you set the LAPI values.
Updating
docker compose pulldocker compose up -dPin the image to this release:
image: ghcr.io/ollioddi/crowdsec-local-dashboard:0.5.0-betaChanges
⚠ Breaking changes
-
docker: Run as an unprivileged user in a hardened image (#40, 0e03dce) the container no longer runs as root, so a database created by an earlier release is owned by root and cannot be opened. Fix the volume once before starting the new version:
docker compose run —rm —user root —cap-add CHOWN —entrypoint chown dashboard -R node:node /data
The container prints this command and exits until it is done. Volumes from 0.2.x or earlier have no migration history and must be recreated with docker compose down -v.
-
config: Configure the container through .env and start without one (#41, 307223b) the compose file no longer contains the environment values. After downloading the new compose file, copy .env.example to .env next to it and move your settings there, or keep your old compose file with the values inline. BETTER_AUTH_SECRET may be left empty; the container generates one.
Features
- logging: Structured server logs with human and json output (#38, 2636e3c)
- ui: Show the running version and flag a newer release (#39, 7362f61)
- ui: Constrain table expanded rows into width of table, keeping rows scrollable (#45, e041739)
- table: Keep table state in the URL and show cards on mobile (#51, 6b240ff)
Bug fixes
- ci: Open release PRs with a maintainer token so CI runs on them (#35, eae16d6)
- ui: Keep relative times ticking (#48, 0b876f1)
- ui: Focus the first menu entry when the mobile sidebar opens (#49, 4972388)
Documentation
- Minor readme adjustments (37dd907)
Full diff: v0.4.1-beta…HEAD