1 fix — default Dutch merchants are now synchronised on every migrate, even without the --seed flag
View changes
🏪 Default merchants on every install Bug fix
On fresh installs where the install agent ran php artisan migrate without --seed, the Dutch default merchant list (Albert Heijn, Jumbo, ING, Shell, Netflix, etc.) was missing until a manual db:seed step. From v2.1.1 onwards this list is kept up to date via an idempotent migration — without touching existing merchants (firstOrCreate on normalized_name).
php artisan migrate after the update will add any missing default merchants. Your own manually created merchants are left untouched.
6 changes — manual merchants + logo upload, expanded category library, transaction detail page, more compact global search, plus-button fix in import flow, idempotent category sync
View changes
🏪 Add merchants manually + logo upload New feature
Until now merchants were only created automatically during an import. From v2.1 there is a dedicated /merchants/create page where you can enter a merchant yourself and attach a logo via URL or file upload (max 2 MB, PNG/JPG/SVG). An uploaded file is stored on the public disk under storage/app/public/merchants and takes precedence over a manually entered URL.
The logo path is stored by MerchantResource::resolveLogoSource() as a relative storage/ URL. The existing logoUrl accessor on the Merchant model automatically resolves that to an absolute URL for the ImageColumn in tables and global search results.
php artisan storage:link automatically — if you have never done that on your install, run it once after the update so uploaded logos are accessible via /storage/....
🏷️ Expanded category library New feature
The default set has grown from 11 to 35 categories: 20 top-level ones (including Sport & Fitness, Holiday, Gifts & Donations, Education, Children, Taxes, Bank Charges, Insurance, Business) and 15 sub-categories under Groceries, Restaurant/Food, Transport, Housing, Subscriptions and Income. The icon dropdown went from 14 to ~45 heroicons, grouped by theme and searchable.
For existing installs there is a one-time migration that calls CategorySeeder::sync(). sync() uses firstOrCreate on (name, parent_id) — so existing categories are left untouched; only missing defaults are added. This keeps a single source of truth for both fresh installs and migrations on live installs.
php artisan migrate after the update adds the new categories. Your existing categories are not affected — if you already had a category with the same name, your version with your colour and icon is kept.
👁️ Transaction detail page New feature
The transaction resource previously only had an index and edit page. From v2.1 there is a dedicated view page with a Filament infolist that neatly displays the date, amount, description, original bank line, account, counter-account, merchant and category. Editing is still available via the prominent "Edit" button at the top.
The table on /transactions now has a recordUrl pointing to this view page — clicking a row opens the detail page rather than jumping straight to the edit screen. Filament's getGlobalSearchResultUrl() automatically picks the view page when it exists, so clicking a result in the global search bar also lands on the detail page.
🔍 More compact global search UX improvement
The global search bar used to dump all matches into one long list. This became unreadable whenever a search term matched hundreds of transactions. From v2.1 the dropdown shows at most 5 results per resource, with an extra "View all X results" entry at the bottom that links to the list page with the query pre-filled via ?search=....
The implementation lives in a reusable trait App\Filament\Concerns\WithCompactGlobalSearch that overrides the default getGlobalSearchResults(). Resources implement globalSearchSeeMoreUrl(string $search): string to supply their own index route. Applied to TransactionResource and MerchantResource.
➕ Plus button in import flow Bug fix
The analysed-files table on /imports/create called mountAction('createAccount') without the Livewire page having a matching createAccountAction() method. Result: clicking the plus button did nothing — no modal, no feedback. Fixed by properly defining the action; the newly created account is immediately selected for the corresponding analysed file.
🌱 Idempotent category sync via migration Bug fix
Previously, new default categories could only reach existing installs via php artisan db:seed — and seeders don't run automatically during an update. From v2.1 the seeder logic has been extracted into a static CategorySeeder::sync() method that is called from both the seeder and a migration.
Result: the CHANGELOG and the seeder no longer need to be kept in sync every time a new default category is added. Future additions work automatically for both fresh installs (via db:seed) and live installs (via a data migration that calls sync()).
php artisan migrate.
7 changes — SNS & Knab PDF import, friendly 403/404 pages, top navigation, admin panel on root, Vite security upgrade, idempotent demo seeder, demo login fix
View changes
📄 SNS & Knab PDF import New feature
Alongside the existing ING parser, there are now full parsers for SNS and Knab bank statements. On upload the source bank is detected automatically based on keywords in the text — no configuration required.
Knab detail: the PDF text extractor loses the Debit/Credit column position. Debit vs. credit is inferred from the keyword Ontvangen (Received) in the description — works on all tested statements.
🎨 Friendly 403 & 404 error pages UX improvement
Laravel's default "Forbidden" and "Not Found" pages have been replaced by BankBird-styled versions with clear explanations and a back button. A raw error page gives the impression the system is broken, even when the cause is ordinary access rules — that confusion is now gone.
The 403 page detects demo mode and then shows "Not available in demo" with a dashboard button instead of a generic error message.
🧭 Top navigation + page footer UX improvement
The admin panel switches from a sidebar menu to top navigation with full content width. The previous sidebar footer showing version info has been replaced by a centred page footer at the bottom of every page. The version link goes to the public updates overview in a new tab.
🏠 Admin panel on the root URL UX improvement
Self-hosted installs no longer serve the admin panel at /admin but at the root URL. A fresh install at bankbird.test therefore opens the dashboard directly, not bankbird.test/admin. The marketing host (bankbird.app) keeps /admin to avoid conflicts with the public landing pages, and the combined host continues to use /dev and /demo for local development.
The active prefix is determined centrally by Demo::panelPath() based on the host. All hardcoded url('/admin/...') calls in views have been replaced by a new Demo::panelUrl(string $path) helper that builds the correct path per host — the admin banner, sidebar footer and 404 page therefore work correctly across all deployment forms.
bankbird.test/admin automatically move to bankbird.test/ after the update. Old bookmarks to /admin/... will no longer work — replace them with the new URL or simply navigate from the root.
🛡️ Vite 5 → 6 security upgrade Security
Two moderate npm audit advisories resolved: GHSA-4w7w-66w2-5vf9 (Vite path traversal in optimised deps .map handling) and GHSA-67mh-4wv8-2f99 (esbuild dev server accepts requests from arbitrary origins). Both advisories affect only the npm run dev development server and are not present in a production build.
We chose the minor-major bump from Vite 5 to 6 rather than running npm audit fix --force (which would have jumped to Vite 8 and also bumped laravel-vite-plugin to 3.x). Tailwind v4 via @tailwindcss/vite@4.2.4 was left untouched and is compatible with Vite 5/6/7/8. Installed versions: vite@6.4.2, laravel-vite-plugin@1.3.0. npm audit now reports 0 vulnerabilities.
Additionally, a new "Advanced updates" section has been added to /updates where dependency and security advisories are published permanently, with severity and status badges, plain-language explanations and collapsible commands for developers.
npm install and npm run build once after the update to pull in the new versions.
🌱 Idempotent demo seeder Bug fix
The demo seeder generated new random amounts on every php artisan db:seed run, leading to duplicate transactions and shifting balances on re-runs. Fixed by using mt_srand with a fixed seed for reproducible amounts, and firstOrCreate on import_hash to prevent duplicate inserts.
Result: the demo database is now deterministic — every seed produces exactly the same transactions with exactly the same amounts, and re-runs are no-ops rather than duplicate creators. This also makes it easier to spot regressions in demo data during code review.
🔑 Demo login 419 fix on combined host Bug fix
On the combined local host the Livewire update endpoint picked the wrong database for demo form submits, causing the CSRF token in the demo database to be validated against the session from the production database — resulting in a 419 mismatch. Fixed by adding a Referer fallback in SwitchDatabaseForCombinedHost: when the host header is ambiguous, the demo database is chosen based on the request's origin URL.