PaperNest Diagram-First Sequence Guide

This guide is diagram-first and focuses on OCR, save flow, and thread handoffs.

Primary implementation files:

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
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()
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.