Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
[Unreleased]
Added
- The table opens on the newest year that has receipts and loads only the years it shows: the year chips count on the server, choosing years fetches those years (one year is about 270 receipts instead of 780), and a text search says how many more it found in other years.
?jahr=alleshows everything - Recurring receipts are grouped automatically, per vendor and item per year exactly: two or more receipts that count for tax (business or university, not meals) become one group "Vendor, Item, Year" with their sum, each time the table is opened. From now on, a receipt a person takes out of a group, or whose group is dissolved, is not grouped again
- The receipts list loads faster: the recognized text of a receipt is no longer part of the list (the detail panel loads it when a receipt is opened), rows come in pages of 200, and a search term of three or more characters is also searched in the recognized text on the server, so finding a receipt by a word of its page still works. The text column is not drawn in the grid any more. The search field above the table has its own result count, searches live while receipts load, and says when the text search found more receipts than it shows; the assistant can ask for the text of up to 25 receipts when it needs it
- A year filter above the receipts table: one chip per year that has receipts, with their number, "Alle", and "ohne Datum" where receipts have none. A click chooses a year or takes it out, Shift and a click chooses every year up to it, so one year, several or a range can be shown. The choice narrows the table, the board, the calendar and the CSV export, is kept in the address (
?jahr=2022-2024,2026), hides a group that has no receipt in the chosen years and shows the others with the receipts of those years only, and drops hidden rows from the selection - What was bought is part of a receipt's name: a new column "Item" holds it in a few words, the reader fills it, and the name reads "vendor, item, total, date" ("Amazon, SSD-Gehäuse, 106,24 €, 18.07.2024"). A business meal keeps its name without an item. The name now follows its cells: correcting vendor, item, total, currency or date in the table builds it again, unless the name was typed by a person. Receipts stored before get their item from their stored text through
POST /api/receipts/items, without being read again - The finance area leads back to the receipt: the name of a receipt in a form line, in the payment proposals and in the list of linked payments opens that receipt in the receipts table, and an open check has "Beleg in der Tabelle öffnen". The table opens the receipt's panel once its rows are loaded, and says so when the linked receipt no longer exists or belongs to another company
- Table views: "Collapse all" / "Expand all" above the first section folds and unfolds every section at once, and the choice is kept with the view. A group whose sum does not cover all its receipts says so next to the number ("36,50 (1 von 2)" when one receipt has no value in that column). The filter bar next to the search no longer draws a frame of its own around "+ Add filter". From
@marlinjai/data-table-react0.7.0 - The reader sees the page: the language model that classifies a receipt is given the page itself (the image, or the PDF pages) next to the recognized text, and also reads invoice date, total, currency, net and tax lines. Its answer is checked, never stored on its own word: a total the invoice's arithmetic proves stands, a figure the recognized text does not print is not taken, and every disagreement is a visible check ("Betrag prüfen", the new "Datum prüfen", "Nur Text gelesen"). A provider that is down, a missing file or a refusal leave the upload complete with the text reading. "Seite neu lesen" in a receipt's detail panel reads a stored receipt again without touching assignment, business share or tax decisions. Measured on 288 real documents with
scripts/evaluate-page-reading.ts: 285 dates and 287 totals right, no total wrong, no wrong date without a flag (the text reader alone: 260 and 265 of 284); 0.0008 US dollars per receipt (Claude Haiku 5.5 through OpenRouter, no new secret, no setting). Plan:docs/plans/2026-10-11-vision-reading.md - Groups in the receipts table, like sub-items in Notion: "Neue Gruppe" creates a row that holds receipts. Receipts are put in from the selection ("Neue Gruppe mit n Belegen", "In Gruppe legen"), taken out again ("aus der Gruppe nehmen", or "Herausnehmen" in the group's panel), and drawn under their group, which folds and shows the sum of its receipts (gross when they share one currency, and the two euro columns). A group is never a receipt: it is in no count and in no sum, in the tax module, the meal register, the review list, the overview, the exports, the duplicate check or the assistant, it takes no amount, no file and no reading, and "Gruppe auflösen" removes it and leaves its receipts in the table. In a grouped view the receipts follow their group's section and keep their own category. One level: a group cannot lie in a group. Needs
@marlinjai/data-table-react0.6.0. Plan:docs/plans/2026-10-11-receipt-sub-rows.md - The dashboard always shows every receipt: the table fetches the remaining pages by itself and the "load more" button is gone, so sums and groups never stand on a partial list
- Table views (Table, By Konto, By Vendor): columns can be resized, sorted and aligned from the header again, and a receipt can be dragged from one group into another by the grip on its row, which changes the grouped column (for the Table view: the category). Dragging one of several selected receipts takes the selection along. From
@marlinjai/data-table-react0.5.0 - Contacts have their own page at
/app/contacts: one searchable list of persons and organizations and one panel per contact; merging and custom fields sit under "Verwaltung". The meal register links to it - Entries in the meal register can be edited ("Bearbeiten"), with the same form as open meals; unsaved changes are not dropped without asking
- Reader: no German tax is estimated on a receipt in another currency, a discount is never read as a tip or a tax, and a new reading in the review list carries the currency and its exchange rate
- Moving a workspace to another company, the receipts side: the signed endpoint
POST /api/internal/workspace-moverestamps the company on every row of one workspace (the tables are read from the Prisma data model, so a new one cannot be forgotten) and moves the contacts its meals name as guests to the target company under the same ids, with@marlinjai/contacts-core0.3.0. A dry run reports the counts and writes nothing; anything that cannot move cleanly blocks the whole apply (a contact asked for by id that the target already has, and a meal that would end up naming one person twice, block too); a repeat finishes a move that stopped half way; the same call with the companies swapped is the rollback. The report says when nothing in the workspace confirms the source company. If a meal gains the second row between the check and the write, the rows are rolled back instead of one being removed. Own secretWORKSPACE_MOVE_SECRET, refused without it. How to call it:docs/operations/workspace-move.md - A receipt with two tax rates says so: a new column "Tax Rates" next to "Tax Rate" shows every rate the receipt carries ("7 % + 19 %", the one that carries most of the bill first), and the receipt's detail panel shows the same. The number column keeps the main rate for sorting and filtering. The text follows every change of the rate or the tax lines (reading, a new reading, the meal form, an edit in the table, an import). Receipts already stored get their text when the column is created, read again from their stored text where they print two rates
- The ten-day rule at the turn of the year: payments between 22 December and 10 January are listed in a tab "Jahreswechsel" with the rule and its source; a regularly recurring one (rent, insurance, an advance payment of value-added tax) can be confirmed to count in the year it belongs to. Nothing is moved without an answer, and the answer belongs to the payment day (migration
0016_tax_year_boundary) - Payments can be linked by hand to any receipt of the year or the year before, or to an invoice with something open, with a part of the amount; a payment without a name can be marked private; re-labelled payments are listed and can be put back; refunds ask for their receipt
- Pay-later purchases are payments: the purchase list of the pay-later service (Klarna, as the table made from its data extract) is a fourth file source, read whole or not at all, idempotent by the service's payment reference and undone like every other import. A purchase paid in instalments is one payment of the full amount on the purchase day; cancelled, fully refunded and not yet processed purchases are left out and counted. Plan:
docs/plans/2026-10-11-pay-later-payments.md - A debit that only funds another imported payment account is a transfer between own accounts, by one rule for PayPal and the pay-later service: PayPal's debit on the bank, the pay-later settlement, money PayPal pays back to the bank, and a PayPal purchase charged to the pay-later card. Such a payment is not asked about and is never an expense, so a purchase through PayPal no longer shows a second time as a "payment without a document". Derived when payments are loaded, so nothing stored changes (no migration) and every existing link stays valid; a person's re-labelling of a single payment stands above the rule
- Receipts on an estimated euro amount (a foreign-currency receipt without a linked payment) get a proposed payment: the same vendor in the counterparty or text, booked from 3 days before to 10 days after the receipt, within 8 percent of the estimate. Each proposal can be taken on its own; "Alle n eindeutigen Vorschläge übernehmen" takes those where the receipt has exactly one such payment and the payment fits no other receipt. Nothing is linked without that click, and the server works the list out again before it links
- Linking a payment is quick with a year of payments: the picker for linking by hand is drawn for one payment at a time ("Von Hand zuordnen") instead of once per open payment, and the check that a receipt belongs to the workspace reads the workspace's own table instead of asking every table of the database in turn. The same lookup serves every single-receipt action of the finance area. Measured on 2,150 invented payments and 550 receipts: the link itself from about 1.8 seconds to about 0.05, the redraw from about 0.9 seconds for 200 open payments (the test ran out of memory at all 890, 489,500 list entries) to about 0.05 for all 890
- Follow-up to the pay-later import after an independent review (plan
docs/plans/2026-10-11-pay-later-payments.md, decisions 14 to 24; migration0019_tax_payment_cancelled, additive: one nullable column, no row rewritten, no link touched):- The funding rule holds only on the days the service's own list covers, so a bank debit to PayPal from a year the PayPal list does not reach is an ordinary payment again. On a card or pay-later account a "PAYPAL *shop" row is a transfer only when the PayPal list holds that purchase. Money a pay-later service pays back is a refund that asks for its receipt; money PayPal pays out stays a transfer
- The payments tab lists what the rule treats as a transfer (collapsed, with count and sum) and each can be made a payment of its own ("Kein Übertrag, eine eigene Zahlung"); an open payment naming a held service can be called a transfer. Per service and year it sets what was collected from the bank against the purchases the list states, by month, and calls out a year that does not fit
- A purchase a later list shows as cancelled or refunded in full stops counting: it is kept and marked, its link is released with the receipt named ("Stornierte Käufe"), and undoing that import restores both
- The same money counts once on a receipt: a purchase and the bank debit that funds it count once, linking the purchase takes the link over from the debit, and a further payment that takes a receipt beyond its own amount is asked about first
- A purchase stored without the service's number is recognized when a later file gives it one, instead of being stored twice
- One "Übernehmen" records an accepted proposal like the bulk action; both send the pairs the person saw, link only what the server still proposes, are safe against a second tab, and say exactly what was linked
- The reader names every status wording it knows; an unknown one is read as a purchase and reported after the import instead of being dropped or refusing the file; a part refund keeps its purchase; money back sharing its purchase's reference is no double; the currency is read whatever its case; a list in another time zone is moved to the German day
- Accepting many proposals is one write instead of one per link (44 links took over 30 seconds on production); the button says "Übernimmt n Vorschläge …" and is locked until the answer is in, and a second press links nothing twice
- A payment that looks like one sum for two receipts of a vendor is shown as "eine Zahlung für mehrere Belege?" and is never accepted in one click
- A counterparty that was linked by hand to a vendor's receipt stands for that vendor in later proposals (the list names the product, the invoice the company); derived from the links, nothing stored
- Money coming in from PayPal is a transfer only when PayPal's own list shows it leaving; a bank debit without a counterparty whose text names the merchant of one PayPal purchase is the bank's part of that purchase; the list of transfers names the receipt a transfer is linked to
- Receipt lines in the finance area: a receipt can be split into positions that add up to its total, each treated on its own (one business, one private; one an asset, one expensed at once); a line can have its own decision or follow its receipt, and an asset can be built from single lines (migration
0015_tax_receipt_lines) - Review list: a receipt stored by the old reader gets a new reading of its stored text, shown as "stored / newly read" per field (name, vendor, total, net, tax rate, tip, category) and written only when taken. No model is asked. "Geprüft, stimmt so" keeps what is stored
- Review list "Belege prüfen" on the dashboard: receipts that need a look, with the reasons in plain words (not readable, amount or date missing, impossible tax figures, not classified, total not confirmed, possible duplicate) and the actions "Beleg öffnen", "Geprüft, stimmt so", "Beide behalten" and "Löschen". Worked out from the stored receipts on every load, so a warning no longer depends on the upload page that first showed it (migration
0014_receipt_reviews, additive) - Business-meal register: the current year is always offered, the register opens on the newest year that has a meal, and meals without a date are counted instead of being invisible
- Payments in the finance area: accounts, import of export files (N26, Tomorrow and PayPal as CSV, the bank interface's transaction list as JSON), read whole or not at all and idempotent across overlapping files; one answer per counterparty (business, private, own account); links from payments to receipts and issued invoices, made automatically only on an invoice number in the payment text with the exact amount, otherwise proposed and confirmed by a person; a linked payment gives a receipt its payment day and the euro amount actually charged (migration
0013_tax_payments) - Revenue in the finance area: a list of issued invoices with their payments; revenue counts on the day the money arrived, an unpaid invoice is shown as outstanding, and profit or loss is shown once invoices are recorded (migration
0012_tax_revenue_vat) - Forecast of the small-business limits (25,000 euros for the previous year, 100,000 for the running year, both by money received): a range to year end, the month a limit would be passed, and a preparation list for the change to regular taxation
- Regular value-added taxation: the status is a first answer plus dated changes, and every receipt, invoice and asset is judged by the status on its own date; under regular taxation costs count net, the tax is input tax at the business share, invoices carry output tax; advance return periods (monthly or quarterly, by issue or by payment date) and payments to the tax office
- Asset register in the finance area: an asset is built from the receipts that make up its cost (purchase, shipping, customs) or carried in with its book value; depreciation by method (low-value, pool, equal amounts by month, one year for computer hardware and software, declining) with each limit checked against its legal source; disposal with remaining book value and proceeds; book values per year (migration
0011_tax_assets) - A receipt above the low-value asset limit is an open check that leads into a new asset, instead of being expensed at once
- Verified production database recovery: six-hourly backups, 30 days of off-server copies, an isolated restoration verifier, and a recovery runbook.
- Finance area at
/app/finance: the income-surplus statement (Einnahmenüberschussrechnung, EÜR) computed from the receipts, line by line with the receipts behind each line; a queue of open checks for receipts that still need a decision; a list of vendor rules - Shares for several purposes per receipt (for example 50 percent business plus 30 percent study), decided per receipt or once per vendor from a date on; the business part goes to the statement, the study and employment part to the employment annex (migration
0009_tax_decisions:tax_item_decisions,tax_vendor_rules) - Tax module
src/lib/tax: onecomputeYearover integer cents, rule sets per year with a source and check date for every legal value; form lines of 2025 read off the official form - Business-meal register (Bewirtungsverzeichnis) at
/app/meals: meal type, guests from a contact list, occasion, place, tip and host per receipt; completeness and the 70 percent deductible amount derived on read; a queue of incomplete meals; a per-year register with export as CSV (comma-separated values) and PDF (Portable Document Format, summary plus one sheet per meal with the receipt and a signature line) - One question per workspace decides the base of the 70 percent: small business under section 19 of the value-added tax act (gross) or not (net); no amounts or exports until it is answered
- Contact list behind a
ContactStoreinterface (migration0008_meal_register:contacts,meal_guests,workspace_tax_settings) - Upload: the same file is recognised by its content hash before anything is uploaded; a receipt photographed twice gets a warning with "keep both" or "discard"; multi-page scans can be split into one receipt per page
pnpm test:db: database-backed tests against a throwaway Postgres (requiresTEST_DATABASE_URL)- Phone capture on the upload page: a camera button, one photo at a time, no redirect; a business meal is followed by the question for guests and occasion; a badly read photo can be retaken onto the same receipt
- Photos are scaled to 2000 pixels and converted to JPEG in the browser before upload (also in the batch uploader)
- Offline capture queue in the browser: photos taken without a connection, on a lost connection or with an expired session are kept and sent later, visibly counted
- Service worker that caches no application code: it only serves a self-contained offline page when a navigation fails, with a server-side kill switch (
DISABLE_SERVICE_WORKER) - Share target (Android): a file shared to the installed app is processed like a photo taken there
- Classifier reads meal type, eaten in or taken away, tip, tax lines and the restaurant address; continuous integration now runs typecheck, lint, unit tests and the database tests
- Multi-receipt batch upload with queue UI — select multiple files at once, sequential processing with per-file progress indicators (uploading, OCR, classifying, done/error), overall X/Y progress counter
- Failed files show per-file errors without blocking other uploads
Changed
-
A receipt is named after its vendor, its total and its date ("Musterwerk, 119,00 €, 14.03.2025"), put together from the cells the receipt holds. The line items are no longer part of the name, and the name is no longer written by the language model. A name that states the stored total follows a new reading; a name a person typed stays
-
The model that reads a receipt is Claude Haiku 5.5 through OpenRouter (
anthropic/claude-haiku-5.5) instead of Claude Sonnet 4.6, chosen by the measurement in the plan;AI_CLASSIFY_MODELstill overrides it. Every model call now has a timeout of 60 seconds and one retry -
The meal register and the meal form follow the value-added tax status on each meal's own date when the status changed inside a year
-
The limit forecast no longer counts invoices that were unpaid at the end of a finished year as that year's turnover, and measures open invoices without the tax in them
-
Every year in which an invoice, a payment, a tax settlement or a status change is dated can be selected in the finance area
-
Lines the app computes itself (input tax, tax paid to the tax office) can no longer be chosen for a receipt by hand; a carried-in asset with a book value but no remaining life is reported instead of silently never being written off; a failed action is shown on every tab
-
A newly created Receipts table carries its company id from the first moment instead of waiting for the backfill script
-
A failed text recognition no longer loses the upload: the receipt is saved without text, flagged, for a retake or manual entry
-
Supermarket receipts are no longer filed as "Bewirtung" by the fallback rules; the default tax rate for a meal follows the receipt date (19 percent until the end of 2025, 7 percent from 2026)
-
Migrated from HTTP API layer to direct D1 adapter (
@marlinjai/data-table-adapter-d1) for database access -
Classification failures are soft — row saved with Pending status, no upload blocking
Fixed
- A page loaded just before or during a deployment no longer dies with "Application error": when it asks for a script file of the build that is gone, it reloads itself once and gets the new one. A second failure within a minute is left to show, so a file that is really missing cannot loop
- With a search active, a cell edited in the table kept showing its old value: a typed amount looked as if it had not been saved, although it had, and typing it again saved a second value. The search view now draws the rows as they are loaded, and a row deleted since is no longer shown
- "Belege prüfen" no longer offers another vendor for an invoice that has one: the text alone took headings, field labels or the addressee for the vendor ("Rechnungsnummer", "Order Summary"), and the stored name is the one a customer would use ("YouTube"). A vendor is still offered on a restaurant or bar receipt and where none is stored, and never when only the legal form differs
- "Seite neu lesen" keeps the vendor a receipt already has when the page shows the same name in another spelling (the legal form added, another case): "A2 Hosting" no longer becomes "A2 Hosting LLC" and falls out of its group. A vendor that is really another name is still corrected
- Retaking the photo of a receipt no longer resets its assignment or a business share somebody changed
- A receipt can no longer be saved or retaken with a file another workspace holds: the file id from the browser is checked against the session's workspace before the file is attached or read
- The reader finds the invoice date on invoices that print it as "20-NOV-2025", "8. March 2025", "26. Nov. 2025" or with the label alone on the line above (Adobe, Apple, Google, Microsoft). Every date is ranked by the label in front of it; a due, renewal, debit or expiry date is never taken as the invoice date
- The reader no longer stores the net as the total where the recognized text separates labels from amounts: net, tax and their printed sum settle the total in any currency, a total labelled further down beats a column heading, the "Total" line stands where a credit or balance is applied, and a long invoice reaches the model with its end as well as its start
- The first guess for "Zuordnung" is no longer "Privat" on invoices addressed to a person: the model answers only what the document shows and leaves the field empty otherwise, and the review list asks for it ("Zuordnung fehlt") unless the meal register, a tax decision or a vendor rule already settles who bears the cost
- A pub, Kneipe, Bierstube or taproom counts as a place that serves meals
- A refund linked to a receipt whose purchase is not linked counted the receipt as zero; it is now taken off the receipt's own amount. A payment received through PayPal in another currency was read as a refund; it is income
- Payments, from an independent review of the first version: a link to an invoice never counts more than the invoice has open and never money going out; an import and its automatic links happen together or not at all, and two imports or two confirmations at the same moment cannot store or use a payment twice; an import is undone newest first; an account stays with one source; an automatic link is judged against every unlinked payment, not only the new file's; amounts in a notation that cannot be read for certain ("1,234.56", "1.234") are refused; a PayPal purchase in another currency is the euro amount actually paid, under the purchase's name
- Receipt reading: the classifier had never run in production (it asked for an Anthropic key that is not set there, and the error was swallowed), so every receipt was filed by pattern matching alone. It now runs through the OpenRouter key production has, uses a valid model name and the current web search tool when a direct key exists, and a missing or failed classification is recorded on the receipt
- Receipt reading: a receipt number was read as the total (916,752.88 instead of 58.70) and tax amounts as the net (rates of 526 and 844 percent). Total, net and tax now come from the tax groups the receipt prints and are confirmed by its own arithmetic; a printed tip is split off the total; a total only a label vouches for is put up for a look; nothing is guessed from the largest number
- Receipt reading: the vendor was the first printed line (a slogan, an item extra, one word of a three-line name). Slogans, item lines, contact lines and misread logos are no longer names, a name set in several lines is joined, and a cropped head gives no name instead of a wrong one
- Receipt reading: a bar was filed as software and a taverna as "other". A table, a waiter, a tip line or the printed hospitality form now make a restaurant receipt, and vendor names in the category list match as whole words only
- Receipt reading: the receipt date depended on the server's time zone (a receipt of the 9th became the 8th at 23:00 UTC outside UTC)
- A page nothing could be read from was saved as an empty row named "Receipt". It is now named "Nicht lesbar: " and stands in the review list
- Deleting a receipt in the dashboard removed the row and left its stored file behind. It now deletes the stored file first and keeps the receipt complete, with a message, when the file store refuses; a file another receipt still shows is kept
- The dashboard deleted the selected receipts on Backspace without asking. It now asks first, in the page, and says what was deleted and what was kept
- The manifest, its icons, the service worker script and the offline page are public: behind the login they answered with a redirect, so the app could not be installed to the home screen
- The exchange-rate recompute filtered the date column with strings, which the database rejected; it now binds dates
- A saved occasion, place or host of a business meal read back empty while the page said "Gespeichert": two page loads had each created the meal columns, so the table held them twice, the save wrote one and the read took the other. Readers and writers now resolve a column name the same way, the next page load folds a doubled column back into one (every value is moved over first; a column holding a differing value is renamed, never deleted), and the columns are created under a per-workspace lock so two requests cannot both create one
- After a column was added to or removed from a table while the app was running, saves could fail with "row not found" until the next restart: Postgres refuses a prepared
SELECT *once the table's shape changed. The database client no longer keeps prepared statements
Removed
- The experiment behind
USE_MULTIMODAL_OCR(a language model in place of text recognition, its answer stored unchecked; never switched on in production) and its modulesrc/lib/multimodal-ocr.ts. The page reading above replaces it. Text recognition moved from the route intosrc/lib/text-recognition.ts, unchanged
[0.5.0] - 2026-02-28
Added
- Liquid Glass UI aesthetic with aurora background, glass-panel surfaces, and backdrop-filter blur
- Direct-to-DB receipt uploads — receipts persist immediately on upload
- Row selection with backspace/delete keyboard deletion
- Column alignment and keyboard navigation (arrow keys, Tab)
- Improved receipt name extraction with 3 item detection patterns (price-based, quantity-based, SKU-based)
- Broader German/English noise filtering and deduplication for receipt parsing
Changed
- Bumped data-table packages to ^0.2.0 for liquid glass UI and keyboard nav support
Fixed
- Refocus table on background click to restore keyboard navigation
- Single-click cell editing and show ungrouped rows (data-table-react 0.1.3)
Removed
- Receipt-store polling module — replaced by immediate database persistence
[0.4.0] - 2026-02-27
Added
- Database integration as persistent storage backend for all environments
- SKR03 accounting categories (Bewirtung, Reisekosten, Bürobedarf, etc.) for German Vorkontierung
- Konto column with SKR03 account numbers
- Notion-style grouping by category/konto in table views
- German vendor and keyword inference maps for category detection
Fixed
- Exclude clearify.config.ts from TypeScript build check
[0.3.0] - 2026-02-20
Added
- Server-side OCR API route using Google Cloud Vision
- Two-phase upload flow — Storage Brain upload then OCR via Vision API
- D1 adapter support with memory fallback for local dev
- OpenNext Cloudflare deployment configuration with D1 binding
- Storage Brain SDK workspace support (v0.5.0) with automatic workspace scoping
- Marketing landing page with hero, features, and how-it-works sections
- Custom domain route for receipts.lumitra.co
- Deployment to Cloudflare Workers
Changed
- Moved upload page from
/to/app, dashboard from/dashboardto/app/dashboard - Replaced file: links with published npm packages (data-table v0.1.x)
- Decoupled OCR types from Storage Brain SDK into app-owned types
Fixed
- Turbopack CSS import panic for file-linked packages
- Turbopack dev compatibility with dist/ CSS import path
- Updated storage-brain-sdk to v0.4.0, removed deprecated OcrResult type
[0.2.0] - 2026-02-19
Added
- Dashboard page with Data Table integration (Table, Board, Calendar views)
- Receipts table initialization with schema and views
- Upload-to-dashboard flow with receipt data ingestion
- Intelligent receipt field extraction pipeline (vendor, amount, date, category)
- Regex/heuristic parsing for US and European number/date formats
- Spatial vendor detection from OCR blocks
- Keyword-based category inference against ~50 known vendors
[0.1.0] - 2026-01-11
Added
- Initial Next.js application setup
- Drag & drop receipt upload component with progress tracking
- OCR result display with confidence score and copy functionality
- File preview for images and placeholder for PDFs
- Storage Brain SDK integration for file uploads with invoice OCR context
- File details panel with status indicators