Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 KiB
Project: Receipt Scanner Web Application UI/UX Upgrade
Architecture
- Framework & Runtime: Next.js 15 App Router, React 19, TypeScript 5.7 (strict), Tailwind CSS 3.4
- Design System: Zenith Silver (
#F6F9FFbackground,#FFFFFFsurface cards,#E2E8F0borders,#000000accents, crisp 0px sharp corners, Hanken Grotesk / Inter / JetBrains Mono typography) - Data & Storage: Local-first IndexedDB (
idb) with PostgreSQL / Drizzle ORM schema compatibility - Export Formats: Dual-sheet
.xlsx(ExcelJS), DATEV-compliant.csv(UTF-8 BOM), structured.json
Feature Inventory
| # | Feature | Description | Milestone | Source | Status |
|---|---|---|---|---|---|
| 1 | Full-page Drag & Drop Overlay | High-visibility global drag-and-drop backdrop overlay across dashboard | M1 | ORIGINAL_REQUEST R1 | DONE |
| 2 | Dedicated Dropzone Component | Visual upload card supporting PDF, PNG, JPEG, WebP with laser scanline | M1 | ORIGINAL_REQUEST R1 | DONE |
| 3 | Batch Upload Drawer & Queue | Multi-file batch queue with instant thumbnail previews, live progress bars | M1 | ORIGINAL_REQUEST R1 | DONE |
| 4 | Error Boundaries & File Retry | Isolated per-file failure handling, corrupt file rejection without halting queue, quick retry/remove | M1 | ORIGINAL_REQUEST R1 | DONE |
| 5 | Side-by-Side Dual Pane Modal | 50/50 split review modal with document on LEFT and editable fields on RIGHT | M2 | ORIGINAL_REQUEST R2 | DONE |
| 6 | Interactive Document Viewer | CSS transform Zoom (0.25x–5x), Pan (grab/trackpad), Rotate, Fit-to-Page | M2 | ORIGINAL_REQUEST R2 | DONE |
| 7 | Bounding-Box Visual Cues | 2-way highlight synchronization between image bounding boxes and extracted form fields | M2 | ORIGINAL_REQUEST R2 | DONE |
| 8 | Dynamic Line Items Editor | Inline-editable table for line items (description, quantity, price, total) with add/delete row controls | M2 | ORIGINAL_REQUEST R2 | DONE |
| 9 | Field Audit & Immediate Save | "AI Extracted" vs "Manually Edited" indicators, 1-click revert, debounced IndexedDB auto-save, Prev/Next navigation | M2 | ORIGINAL_REQUEST R2 | DONE |
| 10 | Payment Method & Schema Extension | Added payment method selection and extended receipt schema | M2 | ORIGINAL_REQUEST R2 | DONE |
| 11 | Editable Table Affordances | Clear dotted underlines, hover edit cues, human-edited flags in LiveTable | M3 | ORIGINAL_REQUEST R3 | DONE |
| 12 | 3-Tier Status Badges | Standardized badges: Scanned (Emerald), Pending Review (Amber), Confirmed (Slate/Blue) | M3 | ORIGINAL_REQUEST R3 | DONE |
| 13 | Floating Batch Actions Toolbar | Multi-select toolbar for Bulk Export (Selected Only to XLSX/CSV/JSON), Bulk Categorize, Bulk Status Update, Bulk Delete | M3 | ORIGINAL_REQUEST R3 | DONE |
| 14 | Quick Search & Filter Chips Bar | Clickable chip filters for temporal ranges (Today/Week/Month/Year), status counters, categories, amount brackets | M3 | ORIGINAL_REQUEST R3 | DONE |
| 15 | Responsive Dashboard Navigation | Mobile-friendly sidebar drawer / bottom nav (hidden md:flex) eliminating horizontal clipping on <768px |
M4 | ORIGINAL_REQUEST R4 | DONE |
| 16 | Interactive KPI Statistics Cards | Total Scanned, Monthly Spend, Pending Reviews, Average Accuracy cards with click-to-filter micro-interactions | M4 | ORIGINAL_REQUEST R4 | DONE |
| 17 | Accessible Information Hierarchy | WCAG AA contrast, crisp typography, clean header layout, zero horizontal overflow | M4 | ORIGINAL_REQUEST R4 | DONE |
| 18 | E2E & Unit Test Coverage | Comprehensive tests verifying R1–R4 features, build integrity, and typechecks | M5 | ORIGINAL_REQUEST AC | DONE |
Milestones
| # | Name | Scope | Dependencies | Status |
|---|---|---|---|---|
| M1 | Ingestion & Batch Upload (R1) | Global dropzone, batch queue drawer, thumbnails, progress, retry/remove | none | DONE |
| M2 | Side-by-Side Inspector & Split Review (R2) | Dual-pane modal, DocumentViewer (zoom/pan), BBox sync, LineItemsEditor, audit badges | none | DONE |
| M3 | Interactive Table & Batch Operations (R3) | Cell edit affordances, status badges, BatchActionBar, FilterChipsBar, selection hooks | none | DONE |
| M4 | Accessible Hierarchy & Responsive Shell (R4) | Responsive Sidebar/TopNav, interactive KPI cards, mobile drawer, zero-overflow | M1, M2, M3 | DONE |
| M5 | E2E Test Suite & Final Verification | Test infra, Tier 1-5 tests, adversarial tests, npm run build, npx tsc --noEmit |
M1, M2, M3, M4 | DONE |
Interface Contracts
M1 Ingestion ↔ Dashboard
BatchUploadDrawer: AcceptsonComplete: (receipts: ProcessedReceipt[]) => void,isOpen: boolean,onClose: () => void.BatchUploadDropzone: AcceptsonFilesSelected: (files: File[]) => void.GlobalDropzoneOverlay: Listens towindowdrag events; triggers batch queue on file drop.
M2 Inspector ↔ Table & Storage
ReceiptInspectorModal: Acceptsreceipt: ProcessedReceipt,isOpen: boolean,onClose: () => void,onSave: (updated: ProcessedReceipt) => void,onNavigate?: (direction: 'prev' | 'next') => void.ProcessedReceiptschema additions:paymentMethod?: stringboundingBoxes?: Record<string, { x: number; y: number; width: number; height: number }>editedFields?: Record<string, boolean>originalExtraction?: Partial<ProcessedReceipt>
M3 LiveTable ↔ BatchActionBar & FilterChipsBar
useReceiptFilters: returns{ filteredReceipts, activePeriod, setPeriod, activeStatus, setStatus, activeCategory, setCategory, searchQuery, setSearchQuery, amountRange, setAmountRange, resetFilters }.useTableSelection: returns{ selectedIds, isSelected, toggleSelect, toggleSelectAll, clearSelection, selectAll, count }.BatchActionBar: AcceptsselectedIds: string[],receipts: ProcessedReceipt[],onBulkDelete,onBulkCategorize,onBulkStatusUpdate,onBulkExport,onClearSelection.
M4 Responsive Shell ↔ Dashboard Layout
Sidebar: Desktop fixed sidebar (hidden md:flex) and mobile drawer (block md:hidden) triggered viaTopNavhamburger button with smooth backdrop transition and zero horizontal overflow.TopNav: Header with breadcrumb/view indicator, quick search trigger, user/workspace menu, and mobile drawer toggle button.- KPI Statistics Cards in
page.tsx: Interactive cards forTotal Scanned,Monthly Spend,Pending Reviews, andAverage Accuracy(calculated dynamically from AI confidence/status with micro-interaction hover states and click-to-filter triggers).
Code Layout
src/
├── app/
│ ├── dashboard/
│ │ ├── layout.tsx # Responsive shell (TopNav + mobile drawer + desktop Sidebar)
│ │ ├── page.tsx # Overview with KPI cards + Ingestion dropzone + LiveTable
│ │ ├── activity/page.tsx # Activity archive with FilterChipsBar + LiveTable + BatchActionBar
│ │ ├── export/page.tsx # Export hub with summary statistics & date filters
│ │ └── settings/page.tsx # Settings & preferences
├── components/
│ ├── dashboard/
│ │ ├── BatchUploadDropzone.tsx # [M1] Dedicated ingestion dropzone card (DONE)
│ │ ├── BatchUploadDrawer.tsx # [M1] Multi-file upload queue & progress modal/drawer (DONE)
│ │ ├── GlobalDropzoneOverlay.tsx # [M1] Full-screen dragover overlay (DONE)
│ │ ├── ReceiptInspectorModal.tsx # [M2] 50/50 Dual-pane review modal (DONE)
│ │ ├── DocumentViewer.tsx # [M2] Zoom, pan, rotate & bounding-box canvas (DONE)
│ │ ├── LineItemsEditor.tsx # [M2] Line item table & editor (DONE)
│ │ ├── StatusBadge.tsx # [M3] 3-tier status badges (DONE)
│ │ ├── BatchActionBar.tsx # [M3] Multi-select batch operations floating bar (DONE)
│ │ ├── FilterChipsBar.tsx # [M3] Responsive filter chips bar (DONE)
│ │ ├── LiveTable.tsx # [M3] Spreadsheet table with cell edit indicators (DONE)
│ │ ├── Sidebar.tsx # [M4] Responsive sidebar & mobile drawer (DONE)
│ │ ├── TopNav.tsx # [M4] Header with search, mobile trigger, status (DONE)
│ │ └── KPICards.tsx # [M4] Interactive KPI cards with micro-interactions (DONE)
└── lib/
├── hooks/
│ ├── useReceiptFilters.ts # [M3] Filtering hook (DONE)
│ └── useTableSelection.ts # [M3] Selection hook (DONE)
├── schema/
│ └── receipt.ts # [M2] Extended receipt schema (DONE)
└── utils/
└── boundingBoxes.ts # [M2] Bounding box calculation & heuristics (DONE)
Appendix: Database Least Privilege
Why. The app must connect to PostgreSQL with only the rights it actually
needs — not superuser. The docker-compose defaults make receipt_user the
database superuser (POSTGRES_USER). If the application is compromised, an
attacker holding the app's credentials would otherwise get full control of the
database: read every user's receipts, drop or alter tables, or grant themselves
rights. A restricted runtime role limits the blast radius to reading and
modifying rows.
Two roles.
receipt_user— owner / migration role. Keeps full rights (DDL) and is used for migrations and schema init (src/lib/db/init.ts). Never the runtime connection in production.receipt_app— runtime role (created byscripts/db-permissions.sql). LOGIN, no superuser, no CREATEDB/CREATEROLE. Granted exactly: CONNECT on the database, USAGE on schemapublic(no CREATE), SELECT/INSERT/UPDATE/DELETE on all tables, USAGE/SELECT on all sequences, and matchingALTER DEFAULT PRIVILEGESso future tables/sequences created by the owner during migrations are covered automatically.CREATEonpublicis additionally revoked from the PUBLIC pseudo-role.
How to apply.
- Fresh volume (
docker compose upwith no existing data): the postgres service mountsscripts/db-permissions.sqlinto/docker-entrypoint-initdb.d/10-db-permissions.sql; the image runs it once asPOSTGRES_USER(superuser) before the app starts. - Existing database (e.g. the local dev DB):
node scripts/apply-db-permissions.mjs— connects with the owner URL from.env.local, executes the same SQL, safe to re-run. SetAPP_DATABASE_PASSWORDto override the documented default password (also rotates it on an existing role).
How to verify. node scripts/verify-db-permissions.mjs connects both as the
owner and as receipt_app and asserts: no superuser/CREATEDB/CREATEROLE, CONNECT
- schema USAGE, DML on all tables/sequences, no schema CREATE, no DDL (a real
CREATE TABLEattempt is denied), default privileges are in place, and the owner can still run DDL. Prints PASS/FAIL and exits 1 on failure.
Limitation. The runtime role has no DDL, so migrations and schema init must
use the owner URL. In production: run migrations with DATABASE_URL set to the
owner URL, then run the app with DATABASE_URL (or APP_DATABASE_URL, plumbed
through docker-compose) set to the restricted receipt_app URL.