dinary 0.0.1__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dinary-0.2.0/.env.example +5 -0
- {dinary-0.0.1 → dinary-0.2.0}/.github/workflows/ci.yml +23 -1
- {dinary-0.0.1 → dinary-0.2.0}/.gitignore +7 -0
- {dinary-0.0.1/.github → dinary-0.2.0}/.plans/architecture.md +214 -79
- dinary-0.2.0/.plans/deploy-oracle-no-db.md +168 -0
- dinary-0.2.0/.plans/frontend-evaluation.md +41 -0
- dinary-0.2.0/.plans/phase0.md +252 -0
- dinary-0.2.0/.plans/phase1.md +655 -0
- dinary-0.2.0/.plans/sql-vs-ibis-comparison.md +118 -0
- dinary-0.2.0/Dockerfile +17 -0
- dinary-0.2.0/PKG-INFO +117 -0
- dinary-0.2.0/README.md +73 -0
- dinary-0.2.0/docker-compose.yml +12 -0
- {dinary-0.0.1 → dinary-0.2.0}/docs/mkdocs.yml +11 -5
- dinary-0.2.0/docs/src/en/cloudflare-setup.md +112 -0
- dinary-0.2.0/docs/src/en/deploy-oracle.md +156 -0
- dinary-0.2.0/docs/src/en/deploy-selfhost.md +94 -0
- dinary-0.2.0/docs/src/en/google-sheets-setup.md +57 -0
- dinary-0.2.0/docs/src/en/index.md +25 -0
- dinary-0.2.0/docs/src/en/installation.md +29 -0
- dinary-0.2.0/docs/src/en/operations.md +87 -0
- dinary-0.2.0/docs/src/en/pwa-install.md +59 -0
- dinary-0.2.0/docs/src/ru/cloudflare-setup.md +112 -0
- dinary-0.2.0/docs/src/ru/deploy-oracle.md +156 -0
- dinary-0.2.0/docs/src/ru/deploy-selfhost.md +94 -0
- dinary-0.2.0/docs/src/ru/google-sheets-setup.md +57 -0
- dinary-0.2.0/docs/src/ru/index.md +21 -0
- dinary-0.2.0/docs/src/ru/installation.md +29 -0
- dinary-0.2.0/docs/src/ru/operations.md +87 -0
- dinary-0.2.0/docs/src/ru/pwa-install.md +59 -0
- dinary-0.2.0/package-lock.json +1567 -0
- dinary-0.2.0/package.json +14 -0
- {dinary-0.0.1 → dinary-0.2.0}/pyproject.toml +14 -6
- dinary-0.2.0/pytest.ini +1 -0
- dinary-0.2.0/src/dinary/__about__.py +1 -0
- dinary-0.2.0/src/dinary/api/__init__.py +0 -0
- dinary-0.2.0/src/dinary/api/categories.py +36 -0
- dinary-0.2.0/src/dinary/api/expenses.py +108 -0
- dinary-0.2.0/src/dinary/api/qr.py +32 -0
- dinary-0.2.0/src/dinary/config.py +34 -0
- dinary-0.2.0/src/dinary/main.py +106 -0
- dinary-0.2.0/src/dinary/migrations/README.md +15 -0
- dinary-0.2.0/src/dinary/migrations/budget/0001_initial_schema.rollback.sql +3 -0
- dinary-0.2.0/src/dinary/migrations/budget/0001_initial_schema.sql +25 -0
- dinary-0.2.0/src/dinary/migrations/config/0001_initial_schema.rollback.sql +8 -0
- dinary-0.2.0/src/dinary/migrations/config/0001_initial_schema.sql +54 -0
- dinary-0.2.0/src/dinary/services/__init__.py +0 -0
- dinary-0.2.0/src/dinary/services/category_store.py +42 -0
- dinary-0.2.0/src/dinary/services/db_migrations.py +107 -0
- dinary-0.2.0/src/dinary/services/duckdb_repo.py +407 -0
- dinary-0.2.0/src/dinary/services/exchange_rate.py +28 -0
- dinary-0.2.0/src/dinary/services/import_sheet.py +229 -0
- dinary-0.2.0/src/dinary/services/qr_parser.py +33 -0
- dinary-0.2.0/src/dinary/services/seed_config.py +206 -0
- dinary-0.2.0/src/dinary/services/sheets.py +357 -0
- dinary-0.2.0/src/dinary/services/sql_loader.py +70 -0
- dinary-0.2.0/src/dinary/services/sync.py +362 -0
- dinary-0.2.0/src/dinary/sql/__init__.py +0 -0
- dinary-0.2.0/src/dinary/sql/find_travel_event.sql +3 -0
- dinary-0.2.0/src/dinary/sql/get_existing_expense.sql +3 -0
- dinary-0.2.0/src/dinary/sql/get_month_expenses.sql +14 -0
- dinary-0.2.0/src/dinary/sql/insert_expense.sql +5 -0
- dinary-0.2.0/src/dinary/sql/list_sheet_categories.sql +3 -0
- dinary-0.2.0/src/dinary/sql/resolve_mapping.sql +3 -0
- dinary-0.2.0/src/dinary/sql/reverse_lookup_5d.sql +6 -0
- dinary-0.2.0/src/dinary/sql/reverse_lookup_travel.sql +3 -0
- dinary-0.2.0/src/dinary/sql/seed_load_categories.sql +1 -0
- dinary-0.2.0/src/dinary/sql/seed_load_groups.sql +1 -0
- dinary-0.2.0/src/dinary/sql/seed_load_members.sql +1 -0
- dinary-0.2.0/src/dinary/sql/seed_load_tags.sql +1 -0
- dinary-0.2.0/static/css/style.css +266 -0
- dinary-0.2.0/static/icons/icon-180.png +0 -0
- dinary-0.2.0/static/icons/icon-192.png +0 -0
- dinary-0.2.0/static/icons/icon-512.png +0 -0
- dinary-0.2.0/static/index.html +80 -0
- dinary-0.2.0/static/js/api.js +54 -0
- dinary-0.2.0/static/js/app.js +325 -0
- dinary-0.2.0/static/js/categories.js +66 -0
- dinary-0.2.0/static/js/offline-queue.js +79 -0
- dinary-0.2.0/static/js/qr-scanner-lib.js +31 -0
- dinary-0.2.0/static/js/qr-scanner-worker.min.js +98 -0
- dinary-0.2.0/static/js/qr-scanner.js +108 -0
- dinary-0.2.0/static/manifest.json +29 -0
- dinary-0.2.0/static/sw.js +57 -0
- dinary-0.2.0/tasks.py +427 -0
- dinary-0.2.0/tests/conftest.py +11 -0
- dinary-0.2.0/tests/js/no-data-loss.test.js +451 -0
- dinary-0.2.0/tests/js/offline-queue.test.js +168 -0
- dinary-0.2.0/tests/js/setup.js +1 -0
- dinary-0.2.0/tests/test_api.py +349 -0
- dinary-0.2.0/tests/test_dinary.py +9 -0
- dinary-0.2.0/tests/test_duckdb.py +599 -0
- dinary-0.2.0/tests/test_migrations.py +124 -0
- dinary-0.2.0/tests/test_seed_config.py +157 -0
- dinary-0.2.0/tests/test_services.py +119 -0
- dinary-0.2.0/tests/test_sheets.py +754 -0
- dinary-0.2.0/tests/test_sql_loader.py +134 -0
- dinary-0.2.0/tests/test_sync.py +476 -0
- dinary-0.2.0/uv.lock +1584 -0
- dinary-0.2.0/vitest.config.js +11 -0
- dinary-0.0.1/PKG-INFO +0 -83
- dinary-0.0.1/README.md +0 -46
- dinary-0.0.1/docs/src/en/index.md +0 -16
- dinary-0.0.1/docs/src/en/installation.md +0 -11
- dinary-0.0.1/docs/src/ru/index.md +0 -11
- dinary-0.0.1/docs/src/ru/installation.md +0 -11
- dinary-0.0.1/pytest.ini +0 -2
- dinary-0.0.1/src/dinary/__about__.py +0 -1
- dinary-0.0.1/src/dinary/main.py +0 -33
- dinary-0.0.1/tasks.py +0 -84
- dinary-0.0.1/tests/test_dinary.py +0 -14
- dinary-0.0.1/uv.lock +0 -1039
- {dinary-0.0.1 → dinary-0.2.0}/.coveragerc +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/.github/workflows/docs.yml +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/.github/workflows/pip_publish.yml +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/.github/workflows/static.yml +0 -0
- {dinary-0.0.1/.github → dinary-0.2.0}/.plans/task.md +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/.pre-commit-config.yaml +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/LICENSE.txt +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/activate.sh +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/docs/includes/install_pipx_macos.sh +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/docs/src/en/images/about.jpg +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/docs/src/en/reference.md +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/invoke.yml +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/scripts/__init__.py +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/scripts/build-docs.sh +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/scripts/build.sh +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/scripts/docs-render-config.sh +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/scripts/upload.sh +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/scripts/verup.sh +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/scripts/verup_action.sh +0 -0
- {dinary-0.0.1 → dinary-0.2.0}/src/dinary/__init__.py +0 -0
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
matrix-build:
|
|
30
30
|
strategy:
|
|
31
31
|
matrix:
|
|
32
|
-
python-version: [
|
|
32
|
+
python-version: [3.13, 3.14]
|
|
33
33
|
platform: [ubuntu-latest, macos-latest, windows-latest]
|
|
34
34
|
runs-on: ${{ matrix.platform }}
|
|
35
35
|
|
|
@@ -48,6 +48,17 @@ jobs:
|
|
|
48
48
|
- name: Install dependencies
|
|
49
49
|
run: uv sync --frozen
|
|
50
50
|
|
|
51
|
+
- name: Set up Node.js
|
|
52
|
+
uses: actions/setup-node@v4
|
|
53
|
+
with:
|
|
54
|
+
node-version: 22
|
|
55
|
+
|
|
56
|
+
- name: Install JS dependencies
|
|
57
|
+
run: npm ci
|
|
58
|
+
|
|
59
|
+
- name: Run JS tests
|
|
60
|
+
run: npm test
|
|
61
|
+
|
|
51
62
|
- name: Test with pytest
|
|
52
63
|
run: ${{ env.PYTEST_CMD }}
|
|
53
64
|
|
|
@@ -73,6 +84,17 @@ jobs:
|
|
|
73
84
|
- name: Install dependencies
|
|
74
85
|
run: uv sync --frozen
|
|
75
86
|
|
|
87
|
+
- name: Set up Node.js
|
|
88
|
+
uses: actions/setup-node@v4
|
|
89
|
+
with:
|
|
90
|
+
node-version: 22
|
|
91
|
+
|
|
92
|
+
- name: Install JS dependencies
|
|
93
|
+
run: npm ci
|
|
94
|
+
|
|
95
|
+
- name: Run JS tests with Allure
|
|
96
|
+
run: npm test
|
|
97
|
+
|
|
76
98
|
- name: Test with pytest and Allure report
|
|
77
99
|
run: "${{ env.PYTEST_CMD }} --alluredir=./allure-results"
|
|
78
100
|
|
|
@@ -11,10 +11,15 @@ prioritizing clean data model and scriptability over UI polish.
|
|
|
11
11
|
|
|
12
12
|
### Repositories
|
|
13
13
|
|
|
14
|
-
| Repository
|
|
15
|
-
|
|
16
|
-
| **dinary**
|
|
17
|
-
| **dinary
|
|
14
|
+
| Repository | Language | Role |
|
|
15
|
+
|--------------------|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
16
|
+
| **dinary-server** | Python (FastAPI + DuckDB) | Backend — REST API, data storage, rule-based classification, dashboards, Google Sheets sync. Also: PWA mobile frontend (in `static/`), user manuals (MkDocs in `docs/`), deployment configs. |
|
|
17
|
+
| **dinary** | Rust | Desktop app (macOS/Windows): daemon for background AI tasks via `claude -p`, and GUI for analysis parameters, interactive results view, and quick data entry (text/PDF receipt import). Communicates with dinary-server API. |
|
|
18
|
+
|
|
19
|
+
#### Documentation convention
|
|
20
|
+
|
|
21
|
+
- **`docs/`** in dinary-server is a **MkDocs site** with bilingual content (`docs/src/en/`, `docs/src/ru/`). All user-facing manuals (PWA install, deployment guides, Cloudflare setup) go here. Do not place standalone markdown files directly in `docs/` — they will break the MkDocs build.
|
|
22
|
+
- **`.plans/`** is for development docs (architecture, phase plans, evaluation notes). These are not published to the MkDocs site.
|
|
18
23
|
|
|
19
24
|
---
|
|
20
25
|
|
|
@@ -28,6 +33,21 @@ prioritizing clean data model and scriptability over UI polish.
|
|
|
28
33
|
- Python-native: `import duckdb` — no server, no driver, no ORM needed.
|
|
29
34
|
- At the expected scale (~30K item rows/year), every query completes in milliseconds.
|
|
30
35
|
|
|
36
|
+
### Server Memory Constraint
|
|
37
|
+
|
|
38
|
+
The production design must fit on an always-on VPS with **1 OCPU / 1 GB RAM**.
|
|
39
|
+
This is a hard architectural constraint, not just a deployment preference.
|
|
40
|
+
|
|
41
|
+
Implications:
|
|
42
|
+
|
|
43
|
+
- Prefer embedded/local components over additional server daemons. DuckDB is acceptable precisely because it runs in-process and avoids a separate database service.
|
|
44
|
+
- The backend must remain a **small FastAPI + DuckDB process**, not a multi-service stack.
|
|
45
|
+
- Do not require Docker in production on the 1 GB instance.
|
|
46
|
+
- Do not run AI/LLM workloads, heavy batch classification, or other memory-hungry jobs on the server. Those stay on the laptop-side `dinary` agent.
|
|
47
|
+
- Keep background work serialized and bounded: no fan-out workers, no parallel sync pipelines, no large in-memory queues.
|
|
48
|
+
- Google Sheets sync should operate on **dirty months / targeted aggregates**, not full-sheet or full-history recomputation on every request.
|
|
49
|
+
- Caches must stay small and optional. Correctness must not depend on large resident in-memory datasets.
|
|
50
|
+
|
|
31
51
|
### Partitioning Strategy
|
|
32
52
|
|
|
33
53
|
One DuckDB file per year:
|
|
@@ -316,32 +336,35 @@ For expenses without QR codes (cafés, services, cash payments, foreign purchase
|
|
|
316
336
|
|
|
317
337
|
The specific mobile client is a build-time decision.
|
|
318
338
|
The architecture is agnostic — the input layer is a thin client that sends structured data to the backend via a simple REST API.
|
|
319
|
-
Key functional requirements regardless of the chosen tool:
|
|
320
339
|
|
|
321
|
-
|
|
322
|
-
|
|
340
|
+
**Phase 0 (MVP) requirements:**
|
|
341
|
+
|
|
342
|
+
- Camera access for QR scanning. In the implemented MVP the browser decodes the Serbian fiscal QR locally with `zbar-wasm`, and the client can extract amount/date from the QR URL path without waiting for a backend roundtrip.
|
|
343
|
+
- Fast manual entry: amount + group selector + category selector + optional comment, one tap to submit. Entry saves instantly to IndexedDB first; network send happens only after local persistence is secured.
|
|
344
|
+
- Offline data persistence via IndexedDB (reliable for installed PWAs — iOS Safari eviction only affects non-installed sites). `navigator.storage.persist()` for additional protection.
|
|
345
|
+
- QR scan with parallel processing: while user selects group/category, the app finishes local QR parsing and can still fall back to backend parsing when needed.
|
|
346
|
+
|
|
347
|
+
**Full requirements (Phase 3 target):**
|
|
348
|
+
|
|
349
|
+
- All Phase 0 capabilities, plus:
|
|
350
|
+
- Confirmation screen after QR scan: shows parsed line items, allows quick category corrections before saving.
|
|
323
351
|
- Event selector: if the expense date falls within an active event's date range, auto-suggest it. If multiple active events overlap, show a dropdown. Allow manual assignment/removal.
|
|
324
352
|
- Beneficiary selector: defaults to "семья", quick switch to a specific family member.
|
|
325
|
-
- Confirmation screen after QR scan: shows parsed items, allows quick category corrections before saving.
|
|
326
353
|
|
|
327
|
-
#### Frontend Tool Evaluation
|
|
354
|
+
#### Frontend Tool Evaluation
|
|
328
355
|
|
|
329
|
-
|
|
330
|
-
The list is a starting point, not exhaustive — the no-code/low-code landscape changes rapidly and there may be newer or niche tools
|
|
331
|
-
that satisfy the requirements better than any of these.
|
|
332
|
-
|
|
333
|
-
Build a minimal MVP with the most promising 1-2 candidates to compare real-world UX before committing.
|
|
356
|
+
**Evaluation result**: .plans/frontend-evaluation.md
|
|
334
357
|
|
|
335
358
|
**Initial candidate list:**
|
|
336
359
|
|
|
337
360
|
| Tool | Type | Evaluate for |
|
|
338
361
|
|------|------|-------------|
|
|
339
|
-
|
|
|
362
|
+
| ~~**Telegram Bot**~~ | Chat-based UI | **Disqualified:** does not work offline (fails must-have #1). Lowest dev effort otherwise. Native camera for QR photo/URL sharing. Inline keyboards for category selection. No app install needed. Limitation: no true "form" UX — interaction is sequential, not a single screen. |
|
|
340
363
|
| **Glide Apps** | No-code app builder (Google Sheets/SQL backend) | Can it connect to a custom REST API or DuckDB directly? Does it support camera/QR scanning? Free tier limits? Good for rapid prototyping if it can talk to our backend. Check offline support. |
|
|
341
364
|
| **Retool** | Low-code internal tool builder | Strong on forms, tables, and API integration. Mobile-responsive. Free tier (5 users) is sufficient. Can it do QR scanning natively or via a component? Overkill for input-only, but could double as an admin/review UI for classifications. Check offline support — likely none. |
|
|
342
365
|
| **Appsmith** | Open-source Retool alternative | Self-hostable (important for data ownership). Same evaluation criteria as Retool. Check: mobile UX quality, QR scanning support, DuckDB/REST connectivity, offline mode. |
|
|
343
|
-
|
|
|
344
|
-
|
|
|
366
|
+
| ~~**Appgyver (SAP Build Apps)**~~ | No-code native app builder | **Likely disqualified:** produces native mobile apps that require App Store / Google Play publishing (fails must-have #0). QR scanning is a built-in component. Free tier available. Has offline data storage capabilities. Only viable if it supports a web/PWA deployment mode that bypasses store publishing — verify before evaluating further. |
|
|
367
|
+
| ~~**Tally / Typeform**~~ | Form builders | **Disqualified:** no offline support (fails must-have #1), no QR scanning (fails must-have #6). Good for quick data capture otherwise. Tally is free and supports webhooks. Likely too rigid for the QR→review→confirm flow. |
|
|
345
368
|
| **PWA (custom)** | Self-built Progressive Web App | Maximum control. Camera API for QR scanning (via `navigator.mediaDevices`). Full offline support via Service Workers + IndexedDB. Requires actual frontend development. Best long-term option if no-code tools don't fit. Works on both Android and iOS via browser. |
|
|
346
369
|
|
|
347
370
|
**Evaluation criteria:**
|
|
@@ -354,9 +377,9 @@ Must-have (tool is disqualified if it fails any of these):
|
|
|
354
377
|
3. **API connectivity** — must be able to POST structured data to a custom REST endpoint.
|
|
355
378
|
4. **Free for expected load** — sustainable at zero cost for a single user with 10-20 entries/day. No "free trial" that expires.
|
|
356
379
|
5. **Longevity / sustainability** — the tool must have a credible future. For open-source: sufficient community (contributors, stars, release cadence). For commercial: a clear business model and track record suggesting the free tier won't be killed. Tools that have recently been acquired, pivoted, or deprecated their free tier are high-risk.
|
|
380
|
+
6. **QR scanning** — can the tool access the camera and scan a QR code to extract the URL? Required from Phase 0 (total-only extraction) through Phase 3b (full line-item flow).
|
|
357
381
|
|
|
358
382
|
Important:
|
|
359
|
-
6. **QR scanning** — can the tool scan a QR code and extract the URL? (must-have for Phase 3b, not required for MVP)
|
|
360
383
|
7. **Speed of entry** — how many taps/screens for a manual expense? (critical for daily use adoption)
|
|
361
384
|
8. **Dev effort for MVP** — how fast can a working prototype be built?
|
|
362
385
|
|
|
@@ -370,16 +393,14 @@ Nice-to-have:
|
|
|
370
393
|
|
|
371
394
|
### Three-tier Classification
|
|
372
395
|
|
|
373
|
-
**Tier 1:
|
|
374
|
-
Example: pattern `MLEKO` matches category "Dairy", pattern `SREDSTVO ZA` matches "Household chemicals".
|
|
375
|
-
Rules are applied immediately when items are ingested. This handles the majority of repeat purchases after an initial learning period.
|
|
396
|
+
**Tier 1: Fuzzy ML based classification like in other personal expense tracking apps.
|
|
376
397
|
|
|
377
398
|
**Tier 2: AI batch classification (deferred, economical).** Unclassified items (`classification_status = 'pending'`) accumulate on dinary-server throughout the day.
|
|
378
|
-
When the user runs dinary
|
|
399
|
+
When the user runs dinary (manually or via scheduler), it fetches pending items from the server API and classifies them using `claude -p`:
|
|
379
400
|
|
|
380
401
|
```bash
|
|
381
|
-
# dinary
|
|
382
|
-
dinary
|
|
402
|
+
# dinary fetches pending items from dinary-server
|
|
403
|
+
dinary classify
|
|
383
404
|
|
|
384
405
|
# Under the hood:
|
|
385
406
|
# 1. GET https://server/api/tasks/pending-classifications
|
|
@@ -442,12 +463,12 @@ The dashboard is a view layer, not a data entry point.
|
|
|
442
463
|
|
|
443
464
|
**Purpose:** "What should I pay attention to? What can I optimize?"
|
|
444
465
|
|
|
445
|
-
**Trigger:** On demand, when the user runs dinary
|
|
466
|
+
**Trigger:** On demand, when the user runs dinary. Not automated — the user decides when to run it.
|
|
446
467
|
|
|
447
468
|
**Flow:**
|
|
448
|
-
1. dinary
|
|
469
|
+
1. dinary fetches aggregated data from dinary-server:
|
|
449
470
|
```bash
|
|
450
|
-
dinary
|
|
471
|
+
dinary analyze --period 2026-Q1
|
|
451
472
|
```
|
|
452
473
|
2. Under the hood: fetches data from server API, feeds to `claude -p`, pushes the report back to dinary-server.
|
|
453
474
|
3. The report is stored on the server and optionally displayed in the dashboard.
|
|
@@ -477,12 +498,15 @@ The sync script is idempotent — running it twice produces the same result.
|
|
|
477
498
|
|
|
478
499
|
The system is split into two parts: an always-on **backend** (VPS) that handles data ingestion and serves dashboards, and a **local agent**
|
|
479
500
|
(user's laptop) that runs expensive AI tasks using the existing Claude subscription via `claude -p`.
|
|
480
|
-
|
|
501
|
+
|
|
502
|
+
**Note on source of truth:** In Phase 0, Google Sheets is the single source of truth (the backend writes directly to it).
|
|
503
|
+
Starting from Phase 1, DuckDB on the backend becomes the single source of truth, and Google Sheets becomes a read-only view layer synced from DuckDB.
|
|
504
|
+
|
|
481
505
|
The local agent is stateless — it fetches tasks, processes them, and pushes results back.
|
|
482
506
|
|
|
483
507
|
```
|
|
484
508
|
┌──────────────┐ ┌─────────────────────────────────────┐
|
|
485
|
-
│ dinary-app │────────▶│ dinary (VPS) │
|
|
509
|
+
│ dinary-app │────────▶│ dinary-server (VPS) │
|
|
486
510
|
│ (mobile) │ │ │
|
|
487
511
|
│ │◀────────│ FastAPI + DuckDB │
|
|
488
512
|
└──────────────┘ │ - receives expenses from mobile │
|
|
@@ -496,18 +520,26 @@ The local agent is stateless — it fetches tasks, processes them, and pushes re
|
|
|
496
520
|
task queue API (REST)
|
|
497
521
|
│
|
|
498
522
|
┌──────────────▼──────────────────────┐
|
|
499
|
-
│ dinary
|
|
523
|
+
│ dinary (user's laptop) │
|
|
500
524
|
│ │
|
|
501
|
-
│
|
|
502
|
-
│
|
|
503
|
-
│ -
|
|
504
|
-
│ -
|
|
505
|
-
│ - push results
|
|
506
|
-
│
|
|
525
|
+
│ ┌─ daemon (background) ──────────┐ │
|
|
526
|
+
│ │ Rust + claude -p │ │
|
|
527
|
+
│ │ - batch classification │ │
|
|
528
|
+
│ │ - spending analysis │ │
|
|
529
|
+
│ │ - push results to server API │ │
|
|
530
|
+
│ └────────────────────────────────┘ │
|
|
531
|
+
│ ┌─ GUI (interactive) ────────────┐ │
|
|
532
|
+
│ │ Rust + GUI framework │ │
|
|
533
|
+
│ │ - analysis params & results │ │
|
|
534
|
+
│ │ - quick manual entry │ │
|
|
535
|
+
│ │ - paste text/PDF → AI API │ │
|
|
536
|
+
│ │ → extract & store expense │ │
|
|
537
|
+
│ │ - review AI suggestions │ │
|
|
538
|
+
│ └────────────────────────────────┘ │
|
|
507
539
|
└─────────────────────────────────────┘
|
|
508
540
|
```
|
|
509
541
|
|
|
510
|
-
### dinary (VPS)
|
|
542
|
+
### dinary-server (VPS)
|
|
511
543
|
|
|
512
544
|
**What it does:**
|
|
513
545
|
- Accepts expenses from dinary-app (REST API).
|
|
@@ -522,26 +554,58 @@ The local agent is stateless — it fetches tasks, processes them, and pushes re
|
|
|
522
554
|
- `POST /api/tasks/analysis-report` — stores the AI-generated report.
|
|
523
555
|
|
|
524
556
|
**What it does NOT do:**
|
|
525
|
-
- Any AI/LLM calls. All AI work is delegated to dinary
|
|
557
|
+
- Any AI/LLM calls. All AI work is delegated to dinary.
|
|
526
558
|
|
|
527
|
-
**Hosting
|
|
559
|
+
**Hosting (free, always-on options):**
|
|
528
560
|
|
|
529
|
-
**
|
|
561
|
+
- **Oracle Cloud Free Tier** — AMD Micro VM (1 OCPU, 1 GB RAM, always available) is recommended for reliability. ARM A1 Flex (up to 4 OCPU, 24 GB RAM) is more powerful but often unavailable due to shared capacity pool. Run directly with uvicorn as a systemd service (no Docker — saves RAM on 1 GB instances). Docker available for local development.
|
|
562
|
+
- **Self-hosted (Mac/PC)** — run locally, expose via Tailscale Serve (tailnet-only) or Cloudflare Tunnel (custom domain + Cloudflare Access). Aligns with Phase 4 architecture (dinary desktop app on the same machine).
|
|
530
563
|
|
|
531
|
-
|
|
564
|
+
**Important:** sleeping/serverless hosting (Render free tier, AWS Lambda, etc.) is **not suitable** — the PWA on iOS cannot run background sync, so the server must respond within 1-2 seconds while the user still has the app open.
|
|
532
565
|
|
|
533
|
-
**
|
|
534
|
-
|
|
566
|
+
**Accessibility:** API served via Cloudflare Tunnel or Tailscale Serve. For the current MVP, Tailscale Serve is the preferred default because it avoids public internet exposure.
|
|
567
|
+
|
|
568
|
+
#### 1 GB Server Rules
|
|
569
|
+
|
|
570
|
+
Because the reference production target is the Oracle AMD Micro instance, the server-side implementation must follow these rules:
|
|
571
|
+
|
|
572
|
+
- Run a single app process by default. Do not scale by adding multiple uvicorn workers on the 1 GB host.
|
|
573
|
+
- Avoid colocating extra infrastructure on the VPS: no separate Postgres, Redis, Celery, message broker, or background analytics service in Phase 1.
|
|
574
|
+
- Treat Google Sheets sync as lightweight projection work, not as a second analytics engine.
|
|
575
|
+
- Prefer on-demand or dirty-month scoped recomputation over broad periodic rebuilds.
|
|
576
|
+
- Any future feature that materially increases steady-state RAM use must be designed to run off-box (for example on the laptop-side agent) or be explicitly deferred until a larger host is available.
|
|
577
|
+
|
|
578
|
+
### dinary (User's Laptop)
|
|
579
|
+
|
|
580
|
+
A desktop application with two components: a **daemon** for background AI processing and a **GUI** for interactive use.
|
|
581
|
+
|
|
582
|
+
**Daemon (background service):**
|
|
583
|
+
- Runs continuously (or on schedule) when the user is at the computer.
|
|
535
584
|
- Fetches pending tasks from the dinary-server API.
|
|
536
585
|
- Processes them using `claude -p` (Claude Code CLI, non-interactive mode) under the user's existing subscription — no API token costs.
|
|
537
586
|
- Pushes results back to the dinary-server API.
|
|
587
|
+
- Handles all heavy/batch AI work that can be deferred.
|
|
588
|
+
|
|
589
|
+
**GUI (interactive desktop app):**
|
|
590
|
+
- Set analysis parameters (time range, grouping, filters) and view interactive analysis results.
|
|
591
|
+
- Quick manual entry: hot-key to enter an expense, import from email/messages like bank notifications of internet payments.
|
|
592
|
+
- Paste text or PDF with a receipt — the app responsively extracts payment data and stores it (uses AI API directly for fast turnaround; see "AI processing modes" below).
|
|
593
|
+
- Review and confirm AI classification suggestions.
|
|
594
|
+
|
|
595
|
+
**AI processing modes:**
|
|
596
|
+
|
|
597
|
+
The desktop app uses two distinct AI channels depending on latency requirements:
|
|
538
598
|
|
|
539
|
-
**
|
|
599
|
+
1. **`claude -p` (daemon, batch)** — for tasks where latency is not critical: batch classification, spending analysis, report generation. Runs under the existing Claude subscription at zero API cost. This is the primary AI channel.
|
|
600
|
+
|
|
601
|
+
2. **AI API (GUI, interactive)** — for tasks that must feel responsive to the user: when the user pastes text or a PDF with a receipt, the app calls an AI API directly to extract payment data (amount, date, store, items) in real time. The user should not wait seconds for `claude -p` to spin up. This is a lightweight, targeted use — simple extraction prompts with small payloads, minimal API cost.
|
|
602
|
+
|
|
603
|
+
**Task types (daemon):**
|
|
540
604
|
|
|
541
605
|
1. **Batch classification** (daily or on demand):
|
|
542
606
|
```bash
|
|
543
607
|
# Fetch unclassified items from dinary-server
|
|
544
|
-
dinary
|
|
608
|
+
dinary classify
|
|
545
609
|
|
|
546
610
|
# Under the hood:
|
|
547
611
|
# 1. GET https://server/api/tasks/pending-classifications → pending.json
|
|
@@ -551,7 +615,7 @@ The local agent is stateless — it fetches tasks, processes them, and pushes re
|
|
|
551
615
|
|
|
552
616
|
2. **Spending analysis** (weekly/monthly/on demand):
|
|
553
617
|
```bash
|
|
554
|
-
dinary
|
|
618
|
+
dinary analyze --period 2026-Q1
|
|
555
619
|
|
|
556
620
|
# Under the hood:
|
|
557
621
|
# 1. GET https://server/api/tasks/analysis-export?period=2026-Q1 → data.json
|
|
@@ -559,9 +623,10 @@ The local agent is stateless — it fetches tasks, processes them, and pushes re
|
|
|
559
623
|
# 3. POST https://server/api/tasks/analysis-report ← report
|
|
560
624
|
```
|
|
561
625
|
|
|
562
|
-
3. **Future AI tasks** — any new AI-intensive operation follows the same pattern: dinary-server exposes a task endpoint, dinary
|
|
626
|
+
3. **Future AI tasks** — any new AI-intensive operation follows the same pattern: dinary-server exposes a task endpoint, dinary fetches, processes with `claude -p`, pushes results back.
|
|
563
627
|
|
|
564
|
-
**Built
|
|
628
|
+
**Built in Rust** — targeting macOS and Windows. Packaging model (single binary vs. app bundle, installer type, tray integration,
|
|
629
|
+
daemon lifecycle management) depends on the GUI framework choice and will be determined during the Phase 4 GUI framework POC.
|
|
565
630
|
|
|
566
631
|
### Backup Strategy
|
|
567
632
|
|
|
@@ -572,75 +637,145 @@ The local agent is stateless — it fetches tasks, processes them, and pushes re
|
|
|
572
637
|
|
|
573
638
|
### Security
|
|
574
639
|
|
|
575
|
-
- dinary-server API protected by
|
|
576
|
-
- Cloudflare Tunnel provides HTTPS without exposing the
|
|
640
|
+
- dinary-server API protected by Cloudflare Access (if using Cloudflare Tunnel) or by tailnet membership (if using Tailscale Serve). Single user, no need for an in-app auth system.
|
|
641
|
+
- Cloudflare Tunnel or Tailscale Serve provides HTTPS without exposing the application port directly to the internet.
|
|
577
642
|
- DuckDB files are not accessible from the internet — only through the dinary-server API.
|
|
578
643
|
|
|
579
644
|
---
|
|
580
645
|
|
|
581
646
|
## Build Plan (Incremental Phases)
|
|
582
647
|
|
|
583
|
-
### Phase 0: MVP — Manual Entry → Google Sheets (
|
|
648
|
+
### Phase 0: MVP — Manual Entry + QR Total → Google Sheets (completed)
|
|
584
649
|
|
|
585
|
-
The fastest path to replacing manual spreadsheet editing
|
|
650
|
+
The fastest path to replacing manual spreadsheet editing, with early validation of QR scanning.
|
|
651
|
+
No new database, no line-item parsing — just a mobile frontend that writes directly to the existing Google Sheets structure.
|
|
586
652
|
|
|
587
653
|
**Scope:**
|
|
588
|
-
|
|
589
|
-
- A
|
|
590
|
-
- **
|
|
591
|
-
-
|
|
592
|
-
-
|
|
654
|
+
|
|
655
|
+
- A mobile frontend (implemented as a PWA) with a simple form: amount (RSD) + group dropdown + category dropdown + optional comment. This matches the existing spreadsheet model better than a single huge selector.
|
|
656
|
+
- **QR scanning with parallel processing:** the user scans a Serbian fiscal receipt QR code on the phone. The QR code is decoded on the device (fully offline — client-side image processing) using `zbar-wasm`. The client extracts amount/date from the receipt URL immediately and shows the form without waiting for the backend. Backend QR parsing remains as a fallback/API capability. No line-item parsing, no store extraction in Phase 0.
|
|
657
|
+
- A FastAPI backend that receives the entry and writes it to the existing Google Sheets spreadsheet via the Sheets API. FastAPI (not serverless) because it carries forward into Phase 1 (DuckDB) and Phase 4 (AI agent API) without rewriting.
|
|
658
|
+
- **Auto-month creation:** if the backend detects that rows for the current month don't exist yet in the sheet, it automatically creates the full block of category rows for the new month by copying the previous block, preserving spreadsheet formulas, zeroing RSD values, and inserting the new month at the top of the yearly sheet.
|
|
659
|
+
- **Currency conversion:** the EUR/RSD exchange rate is stored in the sheet itself, on the first row of each month block. When the backend creates a new month or writes the first expense of the month, it checks that month header row and writes the rate only there if missing.
|
|
660
|
+
- **Offline queue:** entries are stored in IndexedDB on the device before any network call. When connectivity is restored, the queue is flushed automatically on app open, on `online`, and after successful user actions when pending items exist. The user must never lose an entry due to network or server failure.
|
|
661
|
+
- **Always-on server required:** PWA on iOS cannot run background sync — sync only happens while the app is open. The server must respond within 1-2 seconds. Sleeping/serverless hosting (Render free tier, Lambda) is not suitable. Use Oracle Cloud Free Tier (AMD Micro, always on) or self-hosted Mac/PC with Tailscale Serve / Cloudflare Tunnel.
|
|
662
|
+
- No line-item parsing, no store extraction, no DuckDB, no AI. The user picks the category manually, just as they do now — but from a phone instead of editing a spreadsheet. QR scanning only extracts the receipt total amount and date, not individual items or store.
|
|
593
663
|
|
|
594
664
|
**What this validates:**
|
|
665
|
+
|
|
595
666
|
- The chosen mobile frontend tool works for daily data entry (offline persistence, speed, UX).
|
|
667
|
+
- **QR scanning works reliably** with the chosen frontend tool (camera access, code extraction, end-to-end flow).
|
|
596
668
|
- The Google Sheets API integration is reliable.
|
|
597
669
|
- The user actually adopts phone-based entry over direct spreadsheet editing.
|
|
598
670
|
|
|
599
|
-
**
|
|
671
|
+
**Deliverables**
|
|
672
|
+
|
|
673
|
+
- PWA frontend (in `static/`), backend, manuals, deployment scripts — all in the dinary-server repo
|
|
674
|
+
- The `dinary` repo is not used in Phase 0 (reserved for the Rust desktop app, Phase 4+)
|
|
675
|
+
|
|
676
|
+
**Operational conventions introduced by the completed MVP:**
|
|
677
|
+
|
|
678
|
+
- Local/CI regression entry point is `inv test`, which runs both pytest and Vitest and writes a shared `allure-results/` directory.
|
|
679
|
+
- New tests must preserve the existing Allure taxonomy unless there is an explicit architecture-level reason to extend it.
|
|
680
|
+
- Phase 0 approved Allure epics are: `Data Safety`, `Google Sheets`, `API`, `Services`, `Build`.
|
|
681
|
+
- Phase 0 approved features are:
|
|
682
|
+
- `Data Safety`: `Formula Preservation`, `Comment Preservation`, `Column Protection`, `Offline Queue`, `No Data Loss`
|
|
683
|
+
- `Google Sheets`: `Read Categories`, `Write Expense`, `Exchange Rate`, `Month Creation`, `Helpers`
|
|
684
|
+
- `API`: `Health`, `Categories`, `Expenses`, `QR Parse`
|
|
685
|
+
- `Services`: `Category Store`, `Exchange Rate`, `QR Parser`
|
|
686
|
+
- `Build`: `Version`
|
|
687
|
+
|
|
688
|
+
**Exit criteria for Phase 0:**
|
|
689
|
+
- The user has used the system daily for 2+ weeks and no longer opens the spreadsheet to enter data manually.
|
|
690
|
+
- QR scanning has been used successfully on real receipts (camera → URL extraction → total + date pre-fill) and is confirmed to work reliably with the chosen frontend tool.
|
|
691
|
+
|
|
692
|
+
### Phase 1: Data Foundation & Idempotent Ingestion (dinary-server) ✓ IMPLEMENTED
|
|
693
|
+
|
|
694
|
+
Detailed plan: [phase1.md](phase1.md)
|
|
695
|
+
|
|
696
|
+
- DuckDB with the **full 5-dimensional classification schema** (category, beneficiary, event, tags, store) from day one.
|
|
697
|
+
- **sheet-to-5D mapping table** (`sheet_category_mapping`) decomposes the current Google Sheet's flat `(Расходы, Конверт)` pairs into proper 5D assignments.
|
|
698
|
+
- **PWA unchanged** -- sends `(category, group)` as in Phase 0; server resolves to 5D via mapping table.
|
|
699
|
+
- DuckDB-backed expense ingestion with idempotent deduplication via `expenses.id PRIMARY KEY`.
|
|
700
|
+
- Google Sheets is a derived read-only view: sync layer projects 5D DuckDB data back into sheet format.
|
|
701
|
+
- Client generates `expense_id = crypto.randomUUID()` at enqueue time; server returns `200 created`, `200 duplicate`, or `409 Conflict`.
|
|
702
|
+
- Allure test suite covers: `Data Safety / Deduplication` (Python + JS), `DuckDB / Bootstrap`, `DuckDB / Mapping`, `DuckDB / Travel Events`, `DuckDB / Reverse Mapping`, `DuckDB / Year Boundary`.
|
|
600
703
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
-
|
|
704
|
+
**Historical data migration is NOT part of Phase 1.** After Phase 1 cutover, DuckDB holds only new expenses; historical data remains in Google Sheets until Phase 1.5.
|
|
705
|
+
|
|
706
|
+
### Phase 1.5: Historical Data Migration
|
|
707
|
+
|
|
708
|
+
The existing Google Sheets contain ~10 years of data. Nearly every year used a slightly different category system (different category names, different envelope groupings) and even different column layouts. This makes bulk import impractical -- each year requires individual analysis and its own mapping.
|
|
709
|
+
|
|
710
|
+
- Analyze each yearly Google Sheets tab individually: identify that year's category/envelope structure, column layout, and how it differs from other years.
|
|
711
|
+
- Build per-year mapping from that year's flat `(category, envelope)` pairs to the 5D classification model, handling cases where the same category name meant different things in different years.
|
|
712
|
+
- For "путешествия" envelopes: create a per-year synthetic event "отпуск-YYYY" (`date_from = YYYY-01-01`, `date_to = YYYY-12-31`) and map all travel rows to it (same approach as Phase 1 uses for the current year). Once the PWA switches to native 5D input (Phase 2+), set `date_to` of the last synthetic travel event to the release date of the 5D PWA. From that date forward, the user creates specific named trips instead of a per-year umbrella, and the auto-attach rule for `sheet_group = "путешествия"` is retired.
|
|
713
|
+
- Build per-year import scripts that create synthetic expense rows in `budget_YYYY.duckdb` with `source = 'legacy_import'`.
|
|
714
|
+
- Reconcile imported totals against original sheet totals.
|
|
715
|
+
- After successful import, run DuckDB -> Google Sheets sync to verify the rebuilt sheet matches legacy data.
|
|
608
716
|
|
|
609
717
|
### Phase 2: Receipt Parser
|
|
610
718
|
- Integrate or adapt sr-invoice-parser for fetching and parsing Serbian fiscal receipts from SUF PURS URLs.
|
|
611
719
|
- Build the ingestion pipeline: URL → fetch HTML → parse line items → insert into `expenses` table in DuckDB.
|
|
612
|
-
- Implement
|
|
720
|
+
- Implement fuzzy ML / AI auto-classification that produces 5D classification directly (category, beneficiary, event, tags, store).
|
|
721
|
+
- Change the PWA so it no longer works in Google Sheets terms for new receipt/manual flows. From Phase 2 onward, the PWA should use the native 5D classification model directly instead of asking the user for `(Расходы, Конверт)` from the spreadsheet.
|
|
722
|
+
- Google Sheets sync uses the `sheet_category_mapping` table in reverse: from the 5D classification produced by AI/rules, determine the target `(Расходы, Конверт)` row in the sheet.
|
|
613
723
|
|
|
614
724
|
### Phase 3: Mobile Input — Full Version (dinary-app)
|
|
615
|
-
|
|
725
|
+
**Done as part of MVP**
|
|
726
|
+
|
|
727
|
+
- **3a: Frontend tool evaluation.
|
|
728
|
+
|
|
729
|
+
- ** Research the candidate tools from the evaluation table (see "Frontend Tool Evaluation" section) **and any other tools discovered during research**.
|
|
730
|
+
- Build a minimal MVP (scan QR → send URL → see parsed items with line-item detail) with 1-2 top candidates.
|
|
731
|
+
- Compare: QR scanning reliability, offline data persistence, speed of manual entry, API connectivity, cross-platform behavior (Android + iOS), overall UX on phone.
|
|
732
|
+
- Decide on the tool. Note: QR scanning and basic UX are already validated in Phase 0. This step focuses on whether the Phase 0 tool also handles the full line-item review flow, or whether a different tool is needed for Phase 3b.
|
|
733
|
+
|
|
616
734
|
- **3b: Build the full mobile input layer** with the chosen tool.
|
|
735
|
+
|
|
617
736
|
- QR scan → send URL → parse → store.
|
|
618
737
|
- Manual entry for non-QR expenses.
|
|
619
738
|
- Event auto-suggestion and selection.
|
|
620
739
|
- Beneficiary selector.
|
|
621
740
|
- Offline queue with sync-on-reconnect.
|
|
622
741
|
|
|
623
|
-
### Phase 4: AI Classification (dinary
|
|
624
|
-
|
|
625
|
-
-
|
|
626
|
-
-
|
|
627
|
-
-
|
|
628
|
-
-
|
|
742
|
+
### Phase 4: AI Classification & Desktop App (dinary)
|
|
743
|
+
|
|
744
|
+
- **4a: GUI framework POC.**
|
|
745
|
+
- Evaluate Rust GUI frameworks for the desktop app (e.g., Tauri, egui/eframe, Slint, Dioxus, Iced).
|
|
746
|
+
- Build a minimal POC: a window with a form (analysis parameters), a results view, and a paste-to-extract flow (paste text → call AI API → display extracted data).
|
|
747
|
+
- Evaluate: cross-platform support (macOS + Windows), native look and feel, ease of iteration, maturity/community, integration with async Rust for API calls.
|
|
748
|
+
- Decide on the framework.
|
|
749
|
+
|
|
750
|
+
- **4b: Build dinary daemon + GUI.**
|
|
751
|
+
- Daemon: background service that fetches pending tasks from dinary-server, processes with `claude -p`, pushes results back.
|
|
752
|
+
- Implement the task queue API on dinary-server (`/api/tasks/*`).
|
|
753
|
+
- Build the batch classification flow: fetch pending → `claude -p` → push results.
|
|
754
|
+
- GUI: interactive AI API calls for responsive receipt extraction (paste text/PDF → AI API → extract amount, date, items → store via server API).
|
|
755
|
+
- After AI classification of receipt line items is available, change the PWA receipt flow so scanning a receipt submits it immediately without waiting for a manual `Save` press. The scan should create the receipt/import job right away; later user interaction is only for review/correction, not for the initial submission.
|
|
756
|
+
- Implement the review/confirm flow (via GUI or CLI).
|
|
757
|
+
- Wire up rule learning (confirmed classifications → new rules in `category_rules`).
|
|
629
758
|
|
|
630
759
|
### Phase 5: Dashboards (dinary-server)
|
|
631
760
|
- Operational dashboard (static HTML, current month snapshot).
|
|
632
761
|
- Analytical dashboard (interactive SPA with time range selector and breakdowns).
|
|
633
762
|
|
|
634
|
-
### Phase 6: AI Analysis & Google Sheets Sync (dinary
|
|
763
|
+
### Phase 6: AI Analysis & Google Sheets Sync (dinary + dinary-server)
|
|
635
764
|
- Add analysis export endpoint to dinary-server API.
|
|
636
|
-
- Build the dinary
|
|
765
|
+
- Build the dinary analysis flow: fetch aggregates → `claude -p` → push report.
|
|
637
766
|
- Build the Google Sheets sync script on dinary-server (if not already done in Phase 1).
|
|
638
767
|
- Set up scheduled runs on the VPS (sync, dashboard regeneration).
|
|
639
768
|
|
|
640
769
|
Each phase is independently useful.
|
|
641
770
|
|
|
642
|
-
- Phase 0 alone eliminates manual spreadsheet editing and validates the mobile input tool.
|
|
643
|
-
- Phase 1 establishes the proper data foundation.
|
|
771
|
+
- Phase 0 alone eliminates manual spreadsheet editing, validates QR scanning, and validates the mobile input tool.
|
|
772
|
+
- Phase 1 establishes the proper data foundation with idempotent ingestion and deduplication.
|
|
773
|
+
- Phase 1.5 migrates historical Google Sheets data into DuckDB (complex, per-year analysis required).
|
|
644
774
|
- Phase 2 solves the supermarket opacity problem.
|
|
645
|
-
- Phase 3 adds QR
|
|
646
|
-
-
|
|
775
|
+
- Phase 3 adds full line-item QR flow and complete mobile input.
|
|
776
|
+
- Phase 4 builds the desktop app (daemon + GUI) with AI classification and responsive receipt extraction.
|
|
777
|
+
- Phases 5-6 add dashboards, AI analysis, and Google Sheets sync.
|
|
778
|
+
|
|
779
|
+
## Open questions
|
|
780
|
+
|
|
781
|
+
- **Cross-year events**: events (e.g. a trip) can span a year boundary (start in December, end in January). Since `expenses` are partitioned into yearly `budget_YYYY.duckdb` files but `events` live in the shared `config.duckdb`, this works at the data level -- expenses in both years reference the same `event_id`. However, reporting and sync need to handle the case where a single event's expenses are split across two yearly DB files. Decide whether to query both years when summarizing an event, or accept per-year totals as sufficient.
|