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>
This commit is contained in:
Timo
2026-08-19 20:59:04 +02:00
parent 650a74da97
commit 84b9987c49
415 changed files with 96619 additions and 0 deletions

142
PROJECT.md Normal file
View File

@@ -0,0 +1,142 @@
# 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.