PaperNest Diagram-First Sequence Guide
PaperNest Diagram-First Sequence Guide
This guide is diagram-first and focuses on OCR, save flow, and thread handoffs.
Primary implementation files:
- receipt_app/views/screens/add_receipt/add_receipt_screen.py
- receipt_app/business/receipt_service.py
- receipt_app/business/ocr_service.py
- receipt_app/views/screens/gallery/gallery_screen.py
1) App startup and dependency injection
sequenceDiagram
participant Main as main.py
participant App as ReceiptTrackerApp
participant Repo as ReceiptRepository
participant Img as ImageStorageService
participant Csv as CsvExportService
participant Ocr as OCRService
participant Svc as ReceiptService
participant Root as ReceiptRoot
Main->>App: ReceiptTrackerApp().run()
App->>App: build()
App->>Repo: ReceiptRepository(db_path)
App->>Img: ImageStorageService(image_dir)
App->>Csv: CsvExportService(export_dir)
App->>Ocr: OCRService()
App->>Svc: ReceiptService(repo, img, ocr, csv)
App->>App: _load_kv_files()
App->>Root: ReceiptRoot(receipt_service, image_storage)
Root->>Root: on_kv_post()
Root->>Root: configure add/timeline/gallery screens
2) OCR scan flow with worker-thread handoff
sequenceDiagram
participant User
participant UI as AddReceiptScreen (UI thread)
participant Worker as _scan_receipts_worker (worker thread)
participant Svc as ReceiptService
participant Img as ImageStorageService
participant Ocr as OCRService
participant Clock as Kivy Clock
User->>UI: run_ocr(scan_all=True/False)
UI->>UI: _scan_generation += 1
UI->>UI: s(True, "Analyzing...")
UI->>Worker: start Thread(daemon=True)
Worker->>Worker: _attach_android_jnius_thread() (Android)
loop each selected image
Worker->>Svc: suggest_fields_from_image(image_path)
Svc->>Img: validate_captured_image(image_path)
Svc->>Ocr: extract_suggestions(valid_path)
Ocr-->>Svc: OCRSuggestion(date/amount/candidates)
Svc-->>Worker: suggestion
Worker->>Worker: _release_android_memory_pressure() (Android)
end
Worker->>Clock: schedule_once(_apply_scan_results_safely)
Worker->>Worker: _detach_android_jnius_thread() (finally)
Clock->>UI: _apply_scan_results_safely(generation, results)
UI->>UI: _apply_scan_results(...)
UI->>UI: generation guard (ignore stale runs)
UI->>UI: update drafts, status, preview widgets
UI->>UI: s(False)
Thread boundary notes:
- Start boundary: run_ocr
- End boundary: worker returns after scheduling UI callback
- UI re-entry:
Clock.schedule_once(...)ensures safe main-thread UI updates
3) Save receipt flow with worker-thread handoff
sequenceDiagram
participant User
participant UI as AddReceiptScreen (UI thread)
participant Worker as _save_receipt_worker (worker thread)
participant Svc as ReceiptService
participant Img as ImageStorageService
participant Repo as ReceiptRepository
participant DB as SQLite
participant Clock as Kivy Clock
User->>UI: save_receipt()
UI->>UI: validate current draft fields
UI->>UI: s(True, "Saving receipt...")
UI->>Worker: start Thread(daemon=True)
Worker->>Svc: add_receipt(raw_date, raw_amount, image_path, captured)
Svc->>Svc: _parse_date + _parse_amount
alt captured_image=True
Svc->>Img: validate_captured_image(path)
else captured_image=False
Svc->>Img: store_existing_image(path)
end
Svc->>Repo: add_receipt(receipt_date, amount, image_path, created_at)
Repo->>DB: INSERT INTO receipts
DB-->>Repo: lastrowid
Repo-->>Svc: receipt_id
Svc-->>Worker: Receipt(...)
Worker->>Clock: schedule_once(_apply_saved_receipt)
Clock->>UI: _apply_saved_receipt(save_item)
UI->>UI: s(False)
UI->>UI: remove saved draft from manager
UI->>UI: _notify_data_changed() -> refresh timeline + gallery
Error path handoff:
- Worker catches validation/repository/storage errors
- Worker schedules
_handle_save_error(...)on UI thread - UI thread clears loading and shows popup
4) Gallery refresh flow with worker-thread handoff
sequenceDiagram
participant User
participant UI as GalleryScreen (UI thread)
participant Worker as _refresh_gallery_worker (worker thread)
participant Svc as ReceiptService
participant Repo as ReceiptRepository
participant Clock as Kivy Clock
User->>UI: refresh_gallery()
UI->>UI: _set_loading(True)
UI->>Worker: start Thread(daemon=True)
Worker->>Svc: get_gallery_grouped_by_year_month()
Svc->>Repo: get_all_receipts()
Repo-->>Svc: receipts
Svc-->>Worker: grouped dict
Worker->>Clock: schedule_once(_apply_refreshed_gallery)
Clock->>UI: _apply_refreshed_gallery(grouped)
UI->>UI: _set_loading(False)
UI->>UI: _render_gallery()
5) Gallery delete flow with worker-thread handoff
sequenceDiagram
participant User
participant UI as GalleryScreen (UI thread)
participant Worker as _delete_receipt_worker (worker thread)
participant Svc as ReceiptService
participant Repo as ReceiptRepository
participant Img as ImageStorageService
participant Clock as Kivy Clock
User->>UI: delete_receipt(receipt_id)
UI->>UI: _set_loading(True, "Deleting receipt...")
UI->>Worker: start Thread(daemon=True)
Worker->>Svc: delete_receipt(receipt_id)
Svc->>Repo: delete_receipt(receipt_id)
Repo-->>Svc: deleted receipt
Svc->>Repo: count_by_image_path(deleted.image_path)
alt remaining references == 0
Svc->>Img: delete_image(path) (best effort)
end
Worker->>Clock: schedule_once(_on_delete_succeeded)
Clock->>UI: _on_delete_succeeded()
UI->>UI: _set_loading(False)
UI->>UI: refresh_gallery(silent=True)
UI->>UI: timeline.refresh_summary(silent=True)
6) Thread lifecycle summary table
| Flow | Thread Start | Worker Function | UI Re-entry | Worker End |
|---|---|---|---|---|
| OCR scan | run_ocr |
_scan_receipts_worker |
_apply_scan_results_safely via Clock.schedule_once |
after processing batch and scheduling callback |
| Save receipt | save_receipt |
_save_receipt_worker |
_apply_saved_receipt or _handle_save_error via Clock.schedule_once |
after one save attempt |
| Gallery refresh | refresh_gallery |
_refresh_gallery_worker |
_apply_refreshed_gallery or _on_gallery_refresh_error |
after one refresh attempt |
| Gallery delete | delete_receipt |
_delete_receipt_worker |
_on_delete_succeeded or _on_delete_failed |
after one delete attempt |
7) Why these handoffs are safe
- UI mutations run only on main thread.
- Worker threads perform I/O and service calls, then return control via Clock callbacks.
- SQLite access is per-operation connection in repository, reducing cross-thread connection contention.
- Android OCR worker explicitly attaches/detaches pyjnius thread for JVM safety.