Files
scan-receipts/PROJECT.md
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
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>
2026-08-19 20:59:04 +02:00

143 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (`#F6F9FF` background, `#FFFFFF` surface cards, `#E2E8F0` borders, `#000000` accents, 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.25x5x), 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 R1R4 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`: Accepts `onComplete: (receipts: ProcessedReceipt[]) => void`, `isOpen: boolean`, `onClose: () => void`.
- `BatchUploadDropzone`: Accepts `onFilesSelected: (files: File[]) => void`.
- `GlobalDropzoneOverlay`: Listens to `window` drag events; triggers batch queue on file drop.
### M2 Inspector ↔ Table & Storage
- `ReceiptInspectorModal`: Accepts `receipt: ProcessedReceipt`, `isOpen: boolean`, `onClose: () => void`, `onSave: (updated: ProcessedReceipt) => void`, `onNavigate?: (direction: 'prev' | 'next') => void`.
- `ProcessedReceipt` schema additions:
- `paymentMethod?: string`
- `boundingBoxes?: 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`: Accepts `selectedIds: 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 via `TopNav` hamburger 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 for `Total Scanned`, `Monthly Spend`, `Pending Reviews`, and `Average 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 by `scripts/db-permissions.sql`).
LOGIN, no superuser, no CREATEDB/CREATEROLE. Granted exactly: CONNECT on the
database, USAGE on schema `public` (no CREATE), SELECT/INSERT/UPDATE/DELETE on
all tables, USAGE/SELECT on all sequences, and matching `ALTER DEFAULT
PRIVILEGES` so future tables/sequences created by the owner during migrations
are covered automatically. `CREATE` on `public` is additionally revoked from
the PUBLIC pseudo-role.
**How to apply.**
- Fresh volume (`docker compose up` with no existing data): the postgres service
mounts `scripts/db-permissions.sql` into
`/docker-entrypoint-initdb.d/10-db-permissions.sql`; the image runs it once as
`POSTGRES_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. Set `APP_DATABASE_PASSWORD` to 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 TABLE` attempt 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.