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

11 KiB
Raw Blame History

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_userowner / 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_appruntime 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.