dinary 1.2.4__tar.gz → 1.3.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-1.2.4 → dinary-1.3.0}/CLAUDE.md +1 -0
- {dinary-1.2.4 → dinary-1.3.0}/PKG-INFO +1 -1
- {dinary-1.2.4 → dinary-1.3.0}/specs/plans/analytics-ai.md +24 -17
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/analytics-ai.md +22 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/architecture.md +1 -0
- dinary-1.3.0/specs/reference/pwa-analytics.md +43 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/ui/components.md +3 -2
- {dinary-1.2.4 → dinary-1.3.0}/specs/ui/design-language.md +11 -6
- {dinary-1.2.4 → dinary-1.3.0}/specs/ui/screens.md +32 -26
- dinary-1.3.0/src/dinary/__about__.py +1 -0
- dinary-1.3.0/src/dinary/api/analytics.py +80 -0
- dinary-1.3.0/src/dinary/db/sql/analytics_auto_trends.sql +58 -0
- dinary-1.3.0/src/dinary/db/sql/analytics_events.sql +12 -0
- dinary-1.3.0/src/dinary/db/sql/analytics_summary.sql +8 -0
- dinary-1.3.0/src/dinary/db/sql/analytics_ytd_income.sql +3 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/main.py +2 -0
- dinary-1.3.0/tests/api/test_api_analytics.py +192 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/App.vue +40 -43
- dinary-1.3.0/webapp/src/api/analytics.js +5 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/assets/base.css +10 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/ExpenseRow.vue +2 -0
- dinary-1.3.0/webapp/src/components/HeaderSegmented.vue +96 -0
- dinary-1.3.0/webapp/src/stores/analytics.js +49 -0
- dinary-1.3.0/webapp/src/views/AnalyticsView.vue +383 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/views/ReviewView.vue +108 -3
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-app.test.js +5 -5
- dinary-1.3.0/webapp/tests/component-header-segmented.test.js +74 -0
- dinary-1.2.4/specs/plans/analytics-pwa.md +0 -78
- dinary-1.2.4/src/dinary/__about__.py +0 -1
- dinary-1.2.4/webapp/src/components/HeaderSegmented.vue +0 -255
- dinary-1.2.4/webapp/tests/component-header-segmented.test.js +0 -146
- {dinary-1.2.4 → dinary-1.3.0}/.claudeignore +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.coveragerc +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.deploy.example/.env +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.deploy.example/README.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.deploy.example/import_sources.json +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.deploy.example/llm_providers.toml +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.github/workflows/ci.yml +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.github/workflows/docs.yml +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.github/workflows/pip_publish.yml +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.github/workflows/static.yml +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.gitignore +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/.pre-commit-config.yaml +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/AGENTS.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/Dockerfile +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/LICENSE +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/README.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/activate.sh +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docker-compose.yml +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/mkdocs.yml +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/common/images/about.jpg +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/common/reference.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/en/analytics.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/en/cloudflare-setup.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/en/deploy-oracle.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/en/deploy-selfhost.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/en/development.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/en/google-sheets-setup.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/en/index.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/en/installation.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/en/operations.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/en/pwa-install.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/analytics.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/cloudflare-setup.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/deploy-oracle.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/deploy-selfhost.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/development.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/google-sheets-setup.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/index.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/installation.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/operations.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/pwa-install.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/docs/src/ru/taxonomy.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/invoke.yml +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/pyproject.toml +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/pytest.ini +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/scripts/verup.sh +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/README.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/catalog-api.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/classification-pipeline.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/currencies.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/frontend-cache.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/income-import.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/llm-providers.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/pwa-offline.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/receipt-fetching.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/sheets.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/sql-tool.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/stores.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/reference/timestamps.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/ui/README.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/ui/future-screens-guide.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/specs/ui/patterns.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/README.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/__init__.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/adapters/exchange_rates.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/adapters/llm_storage.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/adapters/llmbroker.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/adapters/nbp.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/adapters/nbs.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/adapters/rate_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/adapters/serbian_receipt_parser.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/adapters/sheets_client.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/catalog.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/catalog.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_categories.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_errors.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_events.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_groups.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/expense_corrections.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/expenses.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/income.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/llm.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/qr_parser.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/controllers/rules.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/currencies.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/expense_corrections.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/expenses.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/income.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/llm.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/qr.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/receipts.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/api/rules.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/classification/item_normalizer.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/classification/persist.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/classification/receipt_classifier.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/classification/store_resolver.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/classification/task.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/rate_prefetch/task.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/sheet_logging/income_sheet_logging.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/sheet_logging/logging_jobs.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/sheet_logging/sheet_logging.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/sheet_logging/sheets_write.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/background/sheet_logging/task.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/config.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/catalog.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/classification_rules.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/currencies.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/db_migrations.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/expenses.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/income.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/0001_initial_schema.rollback.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/0001_initial_schema.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/0002_exchange_rates_source_target.rollback.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/0002_exchange_rates_source_target.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/0003_app_currencies.rollback.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/0003_app_currencies.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/0004_receipt_pipeline.rollback.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/0004_receipt_pipeline.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/0005_income_logging.rollback.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/0005_income_logging.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/migrations/README.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/receipts.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/__init__.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/get_category_by_name.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/get_existing_expense.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/get_month_expenses.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/insert_expense.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/insert_income.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/list_categories.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/list_incomes.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/logging_projection.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/resolve_mapping_for_year.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql/seed_load_categories.sql +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/sql_loader.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/db/storage.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/sheets/sheet_mapping.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary/sheets/sheets.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary_analytics/__init__.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary_analytics/backup.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary_analytics/charts.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary_analytics/connection.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary_analytics/mcp_server.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary_analytics/notebooks/dashboard.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/src/dinary_analytics/settings.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/__init__.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/analytics.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/backups/analytics_backup.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/backups/backup_retention.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/backups/backup_snapshots.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/backups/backups_replica.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/backups/backups_restore.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/backups/backups_status.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/backups/backups_yandex.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/backups/restore_utils.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/db.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/deploy.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/devtools/build_docs.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/devtools/constants.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/devtools/dev.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/devtools/env.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/healthcheck.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/README.md +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/expense_import.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/import_tasks.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/income_extract.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/income_import.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/report_2d_3d.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/seed.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/seed_config.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/seed_derivation.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/verify_equivalence.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/imports/verify_income.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/receipt.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/reports/expenses.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/reports/income.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/reports/report_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/reports/report_tasks.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/reports/verify_budget.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/reports/verify_income.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/server.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/setup.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/sql.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tasks/ssh_utils.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/analytics/__init__.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/analytics/test_backup.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/analytics/test_connection.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/analytics/test_dashboard.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/analytics/test_mcp_server.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/analytics/test_settings.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/_admin_catalog_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/_api_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_admin_catalog_add.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_admin_catalog_delete.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_admin_catalog_meta.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_admin_catalog_patch.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_admin_llm.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_catalog.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_concurrency.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_conflict.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_currencies.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_delete_expense.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_delete_receipt.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_expenses_recent_patch.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_get_expenses.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_income.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_post_expense.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_receipts.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_rules_approve.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_api_validation.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_receipt_pipeline_e2e.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_receipt_review.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/api/test_review_page_ux.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/conftest.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/currency/_currency_rates_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/currency/test_currency_rates_misc.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/currency/test_currency_rates_nbp.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/currency/test_currency_rates_resolve.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/currency/test_rate_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/currency/test_rate_prefetch_task.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/imports/test_expense_import.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/imports/test_income_extract.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/imports/test_seed_config.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/_catalog_writer_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/_ledger_repo_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/test_catalog_writer_invariants.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/test_catalog_writer_patch.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/test_income_db.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/test_ledger_repo_catalog.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/test_ledger_repo_expenses_insert.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/test_ledger_repo_expenses_lookup.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/test_ledger_repo_expenses_race.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/test_ledger_repo_jobs.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/test_ledger_repo_logging_projection.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/ledger/test_migrations.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/reports/_report_2d_3d_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/reports/test_report_2d_3d.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/reports/test_report_2d_3d_aggregate.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/reports/test_report_2d_3d_render.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/reports/test_report_2d_3d_resolve.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/reports/test_reports_expenses.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/reports/test_reports_income.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/reports/test_reports_verify_budget.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/reports/test_reports_verify_income.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/services/test_classification_rules.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/services/test_item_normalizer.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/services/test_llm_storage.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/services/test_llmbroker.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/services/test_qr_parser.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/services/test_receipt_classification.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/services/test_receipt_classifier.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/services/test_receipt_parser.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/services/test_sql_loader.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/services/test_store_resolver.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/_sheet_logging_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/_sheet_mapping_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/_sheets_helpers.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/test_income_drain.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/test_sheet_logging.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/test_sheet_logging_derive.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/test_sheet_logging_drain.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/test_sheet_logging_drain_one.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/test_sheet_mapping_parse.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/test_sheet_mapping_reload.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/test_sheet_mapping_resolve.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/test_sheets_read.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/sheets/test_sheets_rows.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_receipt_drain.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_receipt_pipeline.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_reclassify_receipts.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_backups_restore.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_backups_retention.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_backups_status.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_backups_yandex_setup.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_db.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_deploy.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_dev.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_imports.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_reports.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_restore_utils.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_server.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_server_receipt.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_setup_replica.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_ssh_utils.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tasks_ssh_utils_scripts.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/tasks/test_tools_sql.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/test_config.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/test_main.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/tests/test_webapp_api_contract.py +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/uv.lock +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/index.html +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/package-lock.json +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/package.json +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/public/apple-touch-icon-precomposed.png +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/public/apple-touch-icon.png +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/public/favicon.ico +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/public/icons/icon-180.png +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/public/icons/icon-192.png +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/public/icons/icon-512.png +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/public/manifest.json +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/api/_request.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/api/adminLlm.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/api/catalog.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/api/currencies.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/api/expenseCorrections.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/api/expenses.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/api/income.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/api/receipts.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/api/review.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/BaseModal.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/BaseSheet.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/CatalogSelectField.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/CategoryQuickPicks.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/CategorySheet.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/ConfirmDeleteSheet.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/CorrectionSheet.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/CurrencyAmountRow.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/CurrencyPicker.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/ExpenseEditSheet.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/ExpenseForm.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/HealthSummaryCard.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/IconBtn.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/IncomeEditSheet.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/IncomeForm.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/IncomeRow.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/InlineCreateEvent.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/InlineCreateRow.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/KeyboardSaveBar.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/ManageList.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/ProviderCard.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/ProviderSheet.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/QrScanner.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/QueueModal.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/ReceiptCascadeCard.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/RuleRow.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/ScopeSelector.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/StatusDot.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/components/TagPicker.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/addResult.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/catalogManage.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/flushQueue.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/flushReceiptQueue.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/receipt.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/useExpenseDeleteFlow.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/useKeyboardVisible.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/useOnline.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/useStaleCache.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/useSwipeRow.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/composables/zbar.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/legacy-pwa-cleanup.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/main.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/modals/EditModal.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/stores/catalog.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/stores/currency.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/stores/frequentCategories.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/stores/income.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/stores/llm.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/stores/queue.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/stores/receiptQueue.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/stores/review.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/stores/toast.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/views/AddView.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/views/IncomeView.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/src/views/LLMView.vue +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/BaseSheet.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/CategoryQuickPicks.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/CategorySheet.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/CurrencyAmountRow.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/ExpenseEditSheet.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/ExpenseRow.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/ReceiptCascadeCard.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/ScopeSelector.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/api-adminLlm.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/api-catalog.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/api-currencies.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/api-expenses.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/api-income.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/api-receipts.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/api-request.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/api-review.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-catalog-select-field.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-correction-sheet.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-currency-picker.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-edit-modal.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-expense-edit-sheet.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-expense-form.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-health-summary-card.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-inline-create-event.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-inline-create-row.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-manage-list.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-provider-card.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-provider-sheet.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-queue-modal.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-review-view.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-rule-row.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-status-dot.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/component-tag-picker.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/composable-add-result.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/composable-catalog-manage.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/composable-flush-queue.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/composable-flush-receipt-queue.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/composable-keyboard-visible.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/composable-receipt.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/composable-stale-cache.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/composable-use-online.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/frequentCategories.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/legacy-pwa-cleanup.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/setup.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/store-catalog.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/store-currency.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/store-income.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/store-llm.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/store-queue.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/store-receipt-queue-durability.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/store-receipt-queue.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/store-review.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/store-toast.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/useExpenseDeleteFlow.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/tests/useSwipeRow.test.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/vite.config.js +0 -0
- {dinary-1.2.4 → dinary-1.3.0}/webapp/vitest.config.js +0 -0
|
@@ -57,6 +57,7 @@ These rules come from `AGENTS.md` and supplement the defaults in this file.
|
|
|
57
57
|
- Correct spec content: "every expense created from a receipt must have a matching rule", "retry every 15 minutes on day 1, then once a day indefinitely."
|
|
58
58
|
- Never put function signatures, argument lists, field names, or internal class structure in specs. The code is the source of truth for those.
|
|
59
59
|
- Specs describe **current state only**. Motivation, experiments, and rationale are welcome. Never track implementation changes ("previously X, now Y", "approach Z was removed") — state only the current rule. Git history records the evolution.
|
|
60
|
+
- **Specs must never link to plan files.** `specs/reference/` and `specs/ui/` may only link to other spec files.
|
|
60
61
|
|
|
61
62
|
### Tests
|
|
62
63
|
- Every new function needs tests in the same session. Never skip.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dinary
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Server for [Dinary - your dinar diary](https://github.com/andgineer/dinary). Track expenses, scan receipts, analyze spending with AI
|
|
5
5
|
Project-URL: Homepage, https://andgineer.github.io/dinary/
|
|
6
6
|
Project-URL: Documentation, https://andgineer.github.io/dinary/
|
|
@@ -15,34 +15,29 @@ strategy, invariants, and Analytics Views design.
|
|
|
15
15
|
|
|
16
16
|
4. `get_config(key)` and `set_config(key, value)` tools in `mcp_server.py`.
|
|
17
17
|
|
|
18
|
-
### PWA sync
|
|
19
|
-
|
|
20
|
-
5. `PUT /api/analytics/config` endpoint in dinary server + `analytics_pwa_config`
|
|
21
|
-
migration (coordinated with `analytics-pwa.md` work).
|
|
22
|
-
|
|
23
18
|
---
|
|
24
19
|
|
|
25
20
|
### AI Views feature
|
|
26
21
|
|
|
27
|
-
|
|
22
|
+
5. **`queries/spending_summary.sql`** — aggregates last-12-months expenses into
|
|
28
23
|
three result sets: events (id, name, total_amount, date_from, date_to),
|
|
29
24
|
tags (id, name, expense_count, total_amount), category groups (id, name,
|
|
30
25
|
total_amount). Used by the LLM before proposing a new view.
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
6. **`queries/view_data.sql`** — given a basket config passed as a JSON parameter,
|
|
33
28
|
assigns each expense to its first-matching basket (event match checked before
|
|
34
29
|
tag match, unmatched → default basket name), then aggregates by
|
|
35
30
|
(basket_name, year_month, group_name). Returns one row per
|
|
36
31
|
(basket, month, group) triple.
|
|
37
32
|
|
|
38
|
-
|
|
33
|
+
7. **`settings.py` extensions** — `list_view_ids() → list[str]`, `get_view(id)`,
|
|
39
34
|
`save_view(config: dict)`, `delete_view(id)`. Keys in LMDB: `view:<uuid>`.
|
|
40
35
|
|
|
41
|
-
|
|
36
|
+
8. **MCP server** — expose `list_views`, `get_view(id)`, `save_view(config)`,
|
|
42
37
|
`delete_view(id)` tools so Claude Desktop / Claude Code can manage views
|
|
43
38
|
externally.
|
|
44
39
|
|
|
45
|
-
|
|
40
|
+
9. **In-session LLM tools in `dashboard.py`** (Gemini chat tool definitions):
|
|
46
41
|
- `query_spending_summary()` → runs `spending_summary.sql`, returns JSON
|
|
47
42
|
- `propose_view(baskets, default_basket, chart_type)` → sets the in-memory
|
|
48
43
|
draft view config and triggers chart re-render; does not save
|
|
@@ -52,19 +47,31 @@ strategy, invariants, and Analytics Views design.
|
|
|
52
47
|
- `save_current_view(name)` → persists draft via `settings.save_view()`
|
|
53
48
|
- `delete_view(id)` → removes a saved view via `settings.delete_view()`
|
|
54
49
|
|
|
55
|
-
|
|
50
|
+
10. **Altair chart for basket views** — stacked bar: X = year_month, Y = amount,
|
|
56
51
|
color = basket_name. On click of a bar segment: filter to that basket + period
|
|
57
52
|
and show a secondary stacked bar by group_name as a drill-down panel below.
|
|
58
53
|
Use `alt.selection_point` on basket + month for the drill-down interaction.
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
11. **View selector UI in `dashboard.py`** — `mo.ui.dropdown` populated from
|
|
61
56
|
`settings.list_view_ids()` + labels from stored configs; "New view" button
|
|
62
57
|
clears the draft and triggers the LLM with the `query_spending_summary()`
|
|
63
58
|
result plus instructions to propose baskets with chart. Period selector
|
|
64
59
|
(year / custom range) shown alongside the chart.
|
|
65
60
|
|
|
66
|
-
|
|
67
|
-
`query_spending_summary()` first, (b)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
12. **"New view" LLM prompt** — system prompt instructs the LLM: (a) call
|
|
62
|
+
`query_spending_summary()` first, (b) aim for 5–10 top-level baskets that reveal
|
|
63
|
+
non-obvious actionable patterns — not obvious dominant items like rent; start from
|
|
64
|
+
PWA category groups but reorganise freely, extracting cross-cutting baskets by
|
|
65
|
+
event or tag (e.g. travel tag → one basket, relocation event → one basket),
|
|
66
|
+
merging negligible items into the default basket; (c) produce a `propose_view()`
|
|
67
|
+
call immediately with a concrete basket set; (d) justify each basket with concrete
|
|
68
|
+
numbers from the summary; (e) call `update_suggestions()` with 3–5 follow-up
|
|
69
|
+
questions based on what the data shows; (f) invite the user to react — never ask
|
|
70
|
+
them to name categories or tags.
|
|
71
|
+
|
|
72
|
+
13. **Suggested questions feature** — add `update_suggestions(questions: list[str])`
|
|
73
|
+
to in-session LLM tools: stores the list in a reactive Marimo state variable.
|
|
74
|
+
Dashboard renders a `mo.hstack` of `mo.ui.button` chips above the chat input;
|
|
75
|
+
clicking a chip sets the chat input value and submits. On dashboard load, after
|
|
76
|
+
`query_spending_summary()` completes, send a lightweight prompt to generate the
|
|
77
|
+
initial suggestion set.
|
|
@@ -129,6 +129,22 @@ against current ledger data. The user selects the time period at open time.
|
|
|
129
129
|
|
|
130
130
|
Multiple views can be saved, renamed, copied, and deleted.
|
|
131
131
|
|
|
132
|
+
### Basket design goal
|
|
133
|
+
|
|
134
|
+
The goal of a basket set is to surface non-obvious, actionable spending patterns —
|
|
135
|
+
not to confirm obvious ones (e.g. rent dominates). A good top-level set has 5–10
|
|
136
|
+
baskets where each basket is both meaningful in share and something the user can
|
|
137
|
+
actually track or influence.
|
|
138
|
+
|
|
139
|
+
The LLM starts from PWA category groups as a baseline and reorganises freely:
|
|
140
|
+
splitting a group, merging negligible ones, or extracting a cross-cutting basket
|
|
141
|
+
(e.g. "Travel" by tag, "Relocation" by event). Items that are each individually
|
|
142
|
+
negligible and not worth tracking separately are merged into the default basket.
|
|
143
|
+
|
|
144
|
+
Drill-down into any basket produces a second-level basket set (5–10 items) breaking
|
|
145
|
+
down that basket by sub-category, sub-tag, or sub-event, generated by the LLM on
|
|
146
|
+
demand using the same methodology.
|
|
147
|
+
|
|
132
148
|
### Basket structure
|
|
133
149
|
|
|
134
150
|
A view contains an ordered list of baskets plus a default basket name for unmatched
|
|
@@ -162,6 +178,12 @@ View config stored in `analytics.db` under key `view:<uuid>`:
|
|
|
162
178
|
|
|
163
179
|
The LLM is the analyst; the user is the client who reacts to proposals.
|
|
164
180
|
|
|
181
|
+
**Suggested questions.** The dashboard shows a row of clickable question chips above
|
|
182
|
+
the chat input. After every LLM turn the model emits an `update_suggestions` tool call
|
|
183
|
+
with a new list — keeping chips that remain contextually relevant and replacing ones
|
|
184
|
+
already answered or no longer useful. The initial set is generated immediately after
|
|
185
|
+
`query_spending_summary()` loads.
|
|
186
|
+
|
|
165
187
|
**Creating a new view.** The LLM calls `query_spending_summary()` to examine actual
|
|
166
188
|
spending patterns before saying anything. It then presents an initial basket proposal
|
|
167
189
|
with a rendered chart and justifies each basket with concrete data: "Релокация —
|
|
@@ -198,6 +198,7 @@ For implementation detail on each subsystem see `specs/reference/`:
|
|
|
198
198
|
| `timestamps.md` | Timezone storage policy |
|
|
199
199
|
| `currencies.md` | PWA/server responsibility split |
|
|
200
200
|
| `sql-tool.md` | `inv sql` design |
|
|
201
|
+
| `pwa-analytics.md` | Analytics view: page content, API contract, cache policy |
|
|
201
202
|
|
|
202
203
|
---
|
|
203
204
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# PWA analytics view
|
|
2
|
+
|
|
3
|
+
A dedicated `/analytics` route in the Vue PWA. Read-only summary of the ledger.
|
|
4
|
+
Complements the standalone `dinary-analytics` desktop app — the PWA view is
|
|
5
|
+
always-accessible and mobile-friendly; the desktop app is where deep exploration
|
|
6
|
+
and AI interaction happen.
|
|
7
|
+
|
|
8
|
+
## Page content
|
|
9
|
+
|
|
10
|
+
A single page, no tabs. Three sections:
|
|
11
|
+
|
|
12
|
+
1. **Period cards** — YTD savings as a hero card (with savings rate subtitle) + three
|
|
13
|
+
equal cards: current month total, last completed month total, year-to-date spent.
|
|
14
|
+
Savings rate = YTD savings / YTD income.
|
|
15
|
+
|
|
16
|
+
2. **Events** — all events from the last 12 months (open and closed), sorted by
|
|
17
|
+
date_from descending, each showing its total cost in accounting currency. Open
|
|
18
|
+
events are visually distinguished from closed ones.
|
|
19
|
+
|
|
20
|
+
3. **Basket trends** — top-5 category groups and tags ranked by absolute % change
|
|
21
|
+
between the last 3 months and the 3 months before that. Threshold filter:
|
|
22
|
+
items with `MAX(recent, prior) < per-kind AVG * 0.15` are excluded as noise.
|
|
23
|
+
Groups and tags are filtered against their own kind average so tag amounts are
|
|
24
|
+
not swamped by group amounts. Omitted entirely when data is insufficient.
|
|
25
|
+
|
|
26
|
+
## Data source
|
|
27
|
+
|
|
28
|
+
`GET /api/analytics/summary` — single endpoint, single page load:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
summary: { this_month_total, last_month_total, ytd_total, ytd_savings,
|
|
32
|
+
savings_rate, currency } # amounts preformatted "156 000"
|
|
33
|
+
events: [{ id, name, date_range, total, currency, open }] # date_range preformatted
|
|
34
|
+
trends: [{ basket_name, direction, pct }] | null
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
All queries are plain SQLite GROUP BY aggregations. DuckDB is not used on the
|
|
38
|
+
server (1 GB RAM constraint).
|
|
39
|
+
|
|
40
|
+
## Client cache
|
|
41
|
+
|
|
42
|
+
The PWA caches the summary response for 24 hours. Trends change on a
|
|
43
|
+
month-to-month basis, making sub-day freshness unnecessary.
|
|
@@ -34,16 +34,17 @@ Every shipped UI component, with its source file and one-line contract. The `.vu
|
|
|
34
34
|
|
|
35
35
|
| Component | File | Contract |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `HeaderSegmented` | `components/HeaderSegmented.vue` |
|
|
37
|
+
| `HeaderSegmented` | `components/HeaderSegmented.vue` | Five inline tabs, no overflow menu. Each tab is 40×36px, icon-only (20px lucide icon). Inactive: `color-mix(in srgb, <tabColor> 14%, transparent)` bg + tab color icon. Active: solid tab color fill + white icon + `0 4px 12px <tabColor>40` glow. Tabs in order: **Add** (Plus, `--expense`), **Review** (ListChecks, `--review`, doubtful-count badge bottom-right when `showBadge`), **Analytics** (BarChart3, `--stat`), **Income** (TrendingUp, `--income`), **LLM** (Cpu, `--llm`). `v-model:tab` with values `'add'`/`'review'`/`'analytics'`/`'income'`/`'llm'`. All five tabs are peers — no overflow menu. |
|
|
38
38
|
|
|
39
39
|
## App shell
|
|
40
40
|
|
|
41
41
|
| Component | File | Contract |
|
|
42
42
|
|---|---|---|
|
|
43
|
-
| `App` | `App.vue` | Top-level shell — dev banner, sticky header (brand +
|
|
43
|
+
| `App` | `App.vue` | Top-level shell — dev banner, sticky header (`Dinary` brand + `HeaderSegmented`), queue strip (amber full-width strip below header row, only when queue non-empty, tap → `QueueModal`), offline notice strip (below queue strip when offline), main view router by `tab`, `QueueModal`, global toast. Version string removed from header. |
|
|
44
44
|
| `AddView` | `views/AddView.vue` | Mounts `ExpenseForm`, `QrScanner`, the sticky bottom action bar (Scan 48×48 orange + Save flex-1 48px orange), and `KeyboardSaveBar` (orange variant). |
|
|
45
45
|
| `IncomeView` | `views/IncomeView.vue` | Mounts `IncomeForm` inline at top, `INCOMES` section grouped by year with year totals, `IncomeRow` list, scroll-sentinel pagination, edit sheet, sticky bottom Save bar (green), `KeyboardSaveBar` (green variant). |
|
|
46
46
|
| `ReviewView` | `views/ReviewView.vue` | Two-section list (`NEEDS REVIEW` warning header + `EXPENSES` neutral header). Owns the scroll container, two `IntersectionObserver` sentinels, refresh control, Confirm-all bulk action, and `ExpenseEditSheet` open/close. |
|
|
47
|
+
| `AnalyticsView` | `views/AnalyticsView.vue` | Read-only summary dashboard (see `specs/reference/pwa-analytics.md`). Hero savings card + 3 stat cards + auto trend chips (wrap, no scroll) + event list. Fetches from `GET /api/analytics/summary`; caches 24 h in localStorage. Skeleton on first load, graceful offline degradation. |
|
|
47
48
|
| `LLMView` | `views/LLMView.vue` | Provider pool management. Renders `HealthSummaryCard`, an optional `RECEIPT QUEUE` chip strip, then `PROVIDER POOL` and a list of `ProviderCard`s. Owns the 30s polling refresh timer. |
|
|
48
49
|
|
|
49
50
|
## Add view
|
|
@@ -47,6 +47,12 @@ These are alpha-on-white, so they tint with the underlying surface. Never use a
|
|
|
47
47
|
| `--warning` | `#f59e0b` | Doubtful left-border (c2), rate-limit pill, queue badge, NEEDS REVIEW label, dev banner | Background fills (except low-alpha tints inside cards) |
|
|
48
48
|
| `--error` / `--danger` | `#ef4444` / `#e94560` | Destructive confirm button, status-dot "error", inline error text, doubtful left-border (c1) | Anything that isn't an error or destructive action |
|
|
49
49
|
| `--expense` | `#f97316` | **Add view's primary color** (Add tab, currency pill, Save & Scan buttons, selected event chip, selected category quick-pick) | Anything outside the expense-entry context |
|
|
50
|
+
| `--review` | `#60a5fa` | **Review view's primary color** (Review tab fill, edit-sheet Save, sky-blue accent) | Anything outside the review context |
|
|
51
|
+
| `--income` | `#22c55e` | **Income view's primary color** — alias for `--success` in nav context (Income tab fill) | Generic success states — use `--success` there |
|
|
52
|
+
| `--stat` | `#818cf8` | **Analytics view's primary color** (Analytics tab fill, open-event accent, hero card gradient) | Anything outside the analytics context |
|
|
53
|
+
| `--llm` | `#22d3ee` | **LLM view's primary color** (LLM tab fill) | Anything outside the LLM context |
|
|
54
|
+
| `--up` | `#f87171` | Trend direction: spending increased (caution red) | General error states — use `--error` there |
|
|
55
|
+
| `--down` | `#34d399` | Trend direction: spending decreased (good green) | General success states — use `--success` there |
|
|
50
56
|
|
|
51
57
|
### Per-context primary color — the rule
|
|
52
58
|
|
|
@@ -55,13 +61,12 @@ Every top-level view picks **one** primary color and uses it for its main commit
|
|
|
55
61
|
| View | Primary | Where it shows |
|
|
56
62
|
|---|---|---|
|
|
57
63
|
| **Add** | `--expense` (orange) | Add tab fill, hero currency pill, bottom Save + Scan, KeyboardSaveBar, selected event chip, selected `CategoryQuickPicks` pill |
|
|
58
|
-
| **Income** | `--success` (green) | Income
|
|
59
|
-
| **Review** |
|
|
60
|
-
| **
|
|
64
|
+
| **Income** | `--income` / `--success` (green) | Income tab fill, IncomeForm currency pill, hero amount underline, bottom Save bar, KeyboardSaveBar, IncomeRow left-border, year-total label |
|
|
65
|
+
| **Review** | `--review` (sky-blue) + `--success` (approve chip) | Review tab fill, edit-sheet Save, Confirm-all button (green) |
|
|
66
|
+
| **Analytics** | `--stat` (indigo) | Analytics tab fill, open-event left-border + dot + OPEN pill, hero card gradient |
|
|
67
|
+
| **LLM** | `--llm` (cyan) | LLM tab fill |
|
|
61
68
|
|
|
62
|
-
**
|
|
63
|
-
|
|
64
|
-
**Never invent a hue** beyond what's listed. If a new view needs a primary color, pick one of the four above and own it.
|
|
69
|
+
**Never invent a hue** beyond what's listed. Each new view must pick one of the existing per-context tokens.
|
|
65
70
|
|
|
66
71
|
## Typography
|
|
67
72
|
|
|
@@ -1,40 +1,42 @@
|
|
|
1
1
|
# Screen Anatomy
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The five top-level views, their layout, and how the header segmented control binds them together.
|
|
4
4
|
|
|
5
5
|
## Navigation
|
|
6
6
|
|
|
7
|
-
A single **header segmented control** in `App.vue` switches between the
|
|
7
|
+
A single **header segmented control** in `App.vue` switches between the five views. There is no bottom tab bar and no overflow menu.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
|
|
11
|
-
│ Dinary
|
|
12
|
-
|
|
13
|
-
│
|
|
14
|
-
|
|
15
|
-
│
|
|
16
|
-
|
|
17
|
-
│
|
|
18
|
-
|
|
10
|
+
┌────────────────────────────────────────────┐
|
|
11
|
+
│ Dinary [+][≣][▩][↗][▦] │ sticky header row
|
|
12
|
+
├────────────────────────────────────────────┤
|
|
13
|
+
│ ⏱ 2 receipts queued tap to review → │ queue strip (amber), only when queued
|
|
14
|
+
├────────────────────────────────────────────┤
|
|
15
|
+
│ Offline — expenses will be queued │ offline strip, only when offline
|
|
16
|
+
├────────────────────────────────────────────┤
|
|
17
|
+
│ active view body │
|
|
18
|
+
└────────────────────────────────────────────┘
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
### Tab inventory (`HeaderSegmented.vue`)
|
|
22
22
|
|
|
23
|
-
| Tab |
|
|
24
|
-
|
|
25
|
-
| **Add** |
|
|
26
|
-
| **Review** |
|
|
27
|
-
|
|
|
28
|
-
| **Income** |
|
|
29
|
-
| **LLM providers** |
|
|
23
|
+
| Tab | key | Glyph (lucide) | Color token | Size |
|
|
24
|
+
|---|---|---|---|---|
|
|
25
|
+
| **Add** | `add` | `Plus` | `--expense` #f97316 | 40×36 |
|
|
26
|
+
| **Review** | `review` | `ListChecks` | `--review` #60a5fa | 40×36 |
|
|
27
|
+
| **Analytics** | `analytics` | `BarChart3` | `--stat` #818cf8 | 40×36 |
|
|
28
|
+
| **Income** | `income` | `TrendingUp` | `--income` #22c55e | 40×36 |
|
|
29
|
+
| **LLM providers** | `llm` | `Cpu` | `--muted` #94a3b8 | 40×36 |
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Each tab button: inactive = `color-mix(in srgb, <tabColor> 14%, transparent)` bg + tab color text. Active = solid tab color fill + `#fff` icon + `0 4px 12px <tabColor>66` glow.
|
|
32
|
+
|
|
33
|
+
**Rule for the future:** all tabs are peers — no overflow menu. Every new top-level view gets an inline tab.
|
|
32
34
|
|
|
33
35
|
### Header chrome
|
|
34
36
|
|
|
35
|
-
- **Brand
|
|
36
|
-
- **Queue
|
|
37
|
-
- **Offline notice strip**
|
|
37
|
+
- **Brand** (`Dinary`) on the left. Version string removed from header.
|
|
38
|
+
- **Queue strip** — full-width amber strip below the header row, renders only when `queue.items.length + receiptQueue.items.length > 0`. Shows count + "tap to review →". Tap → `QueueModal`. Stacks above the offline strip when both present.
|
|
39
|
+
- **Offline notice strip** — warning-color strip below the queue strip when `!isOnline`. Copy adapts by view: `Offline — expenses will be queued` on Add, `Offline — incomes can't be added or edited` on Income, generic `Offline — changes not available` elsewhere.
|
|
38
40
|
|
|
39
41
|
## Add view
|
|
40
42
|
|
|
@@ -111,9 +113,14 @@ Two ways to save:
|
|
|
111
113
|
|
|
112
114
|
After save: the form resets but keeps the default group/category and currency. A toast confirms the saved amount.
|
|
113
115
|
|
|
116
|
+
## Analytics view
|
|
117
|
+
|
|
118
|
+
Read-only financial summary. Reached via the inline `analytics` tab.
|
|
119
|
+
See `specs/reference/pwa-analytics.md` for content, API contract, and cache policy.
|
|
120
|
+
|
|
114
121
|
## Income view
|
|
115
122
|
|
|
116
|
-
The income-tracking view.
|
|
123
|
+
The income-tracking view. Accessed via the inline `income` tab.
|
|
117
124
|
|
|
118
125
|
```
|
|
119
126
|
┌──────────────────────────────────────┐
|
|
@@ -316,8 +323,7 @@ Each chip is a thin outlined pill. The strip is informational — no actions.
|
|
|
316
323
|
|
|
317
324
|
## When to add a new view
|
|
318
325
|
|
|
319
|
-
|
|
326
|
+
All five tabs are inline peers — icon-only at 40 px, fits at 340 px. A sixth tab would need design review. If the new view is:
|
|
320
327
|
|
|
321
|
-
- **
|
|
322
|
-
- **Secondary or rare** — append to `RARE_TABS` in `HeaderSegmented.vue`. Nothing else changes. Pick a glyph the dropdown menu can render at 22 px and an obvious label.
|
|
328
|
+
- **A new primary workflow** — add an inline tab with its own `--<context>` color token.
|
|
323
329
|
- **An admin / settings panel** — push it into the LLM view's pattern (a dedicated screen reachable from elsewhere) or into a sheet, not a top-level slot.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.0"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Analytics API: GET /api/analytics/summary"""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from datetime import date
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends
|
|
8
|
+
|
|
9
|
+
from dinary.config import settings
|
|
10
|
+
from dinary.db.storage import get_db
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
_SQL_DIR = Path(__file__).resolve().parent.parent / "db" / "sql"
|
|
15
|
+
_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _sql(name: str) -> str:
|
|
19
|
+
return (_SQL_DIR / name).read_text()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _fmt(amount: float) -> str:
|
|
23
|
+
return f"{round(amount):,}".replace(",", " ")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fmt_date_range(date_from: str | date, date_to: str | date) -> str:
|
|
27
|
+
df = date.fromisoformat(str(date_from)[:10]) if not isinstance(date_from, date) else date_from
|
|
28
|
+
dt = date.fromisoformat(str(date_to)[:10]) if not isinstance(date_to, date) else date_to
|
|
29
|
+
if df.year == dt.year and df.month == dt.month:
|
|
30
|
+
return f"{df.day}–{dt.day} {_MONTHS[df.month - 1]} {df.year}"
|
|
31
|
+
if df.year == dt.year:
|
|
32
|
+
return f"{df.day} {_MONTHS[df.month - 1]}–{dt.day} {_MONTHS[dt.month - 1]} {df.year}"
|
|
33
|
+
return f"{df.day} {_MONTHS[df.month - 1]} {df.year}–{dt.day} {_MONTHS[dt.month - 1]} {dt.year}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.get("/api/analytics/summary")
|
|
37
|
+
def get_analytics_summary(con: sqlite3.Connection = Depends(get_db)) -> dict: # noqa: B008
|
|
38
|
+
cur = con.cursor()
|
|
39
|
+
currency = settings.accounting_currency
|
|
40
|
+
|
|
41
|
+
this_month, last_month, ytd_expenses = cur.execute(_sql("analytics_summary.sql")).fetchone()
|
|
42
|
+
ytd_income = cur.execute(_sql("analytics_ytd_income.sql")).fetchone()[0]
|
|
43
|
+
|
|
44
|
+
ytd_savings = ytd_income - ytd_expenses
|
|
45
|
+
savings_rate = round(ytd_savings * 100 / ytd_income) if ytd_income > 0 else 0
|
|
46
|
+
|
|
47
|
+
events = [
|
|
48
|
+
{
|
|
49
|
+
"id": r[0],
|
|
50
|
+
"name": r[1],
|
|
51
|
+
"date_range": _fmt_date_range(r[2], r[3]),
|
|
52
|
+
"total": _fmt(r[4]),
|
|
53
|
+
"currency": currency,
|
|
54
|
+
"open": bool(r[5]),
|
|
55
|
+
}
|
|
56
|
+
for r in cur.execute(_sql("analytics_events.sql")).fetchall()
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
trend_rows = cur.execute(_sql("analytics_auto_trends.sql")).fetchall()
|
|
60
|
+
trends = [
|
|
61
|
+
{
|
|
62
|
+
"basket_name": r[1],
|
|
63
|
+
"direction": r[5],
|
|
64
|
+
"pct": f"{abs(int(r[4]))}%",
|
|
65
|
+
}
|
|
66
|
+
for r in trend_rows
|
|
67
|
+
] or None
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"summary": {
|
|
71
|
+
"this_month_total": _fmt(this_month),
|
|
72
|
+
"last_month_total": _fmt(last_month),
|
|
73
|
+
"ytd_total": _fmt(ytd_expenses),
|
|
74
|
+
"ytd_savings": _fmt(ytd_savings),
|
|
75
|
+
"savings_rate": f"{savings_rate}%",
|
|
76
|
+
"currency": currency,
|
|
77
|
+
},
|
|
78
|
+
"events": events,
|
|
79
|
+
"trends": trends,
|
|
80
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- Top-5 category groups and tags by % change between the two most recent 3-month windows.
|
|
2
|
+
-- Noise filter: items with MAX(recent, prior) below 15% of the average for their kind
|
|
3
|
+
-- (groups vs tags compared separately so tag amounts aren't swamped by group amounts).
|
|
4
|
+
WITH period_amounts AS (
|
|
5
|
+
SELECT
|
|
6
|
+
'group' AS kind,
|
|
7
|
+
cg.id,
|
|
8
|
+
cg.name,
|
|
9
|
+
COALESCE(SUM(CASE WHEN date(e.datetime) >= date('now', '-3 months')
|
|
10
|
+
THEN e.amount ELSE 0 END), 0) AS recent,
|
|
11
|
+
COALESCE(SUM(CASE WHEN date(e.datetime) < date('now', '-3 months')
|
|
12
|
+
AND date(e.datetime) >= date('now', '-6 months')
|
|
13
|
+
THEN e.amount ELSE 0 END), 0) AS prior
|
|
14
|
+
FROM category_groups cg
|
|
15
|
+
JOIN categories c ON c.group_id = cg.id AND c.is_active = 1
|
|
16
|
+
JOIN expenses e ON e.category_id = c.id
|
|
17
|
+
WHERE date(e.datetime) >= date('now', '-6 months')
|
|
18
|
+
AND cg.is_active = 1
|
|
19
|
+
GROUP BY cg.id, cg.name
|
|
20
|
+
|
|
21
|
+
UNION ALL
|
|
22
|
+
|
|
23
|
+
SELECT
|
|
24
|
+
'tag',
|
|
25
|
+
t.id,
|
|
26
|
+
t.name,
|
|
27
|
+
COALESCE(SUM(CASE WHEN date(e.datetime) >= date('now', '-3 months')
|
|
28
|
+
THEN e.amount ELSE 0 END), 0),
|
|
29
|
+
COALESCE(SUM(CASE WHEN date(e.datetime) < date('now', '-3 months')
|
|
30
|
+
AND date(e.datetime) >= date('now', '-6 months')
|
|
31
|
+
THEN e.amount ELSE 0 END), 0)
|
|
32
|
+
FROM tags t
|
|
33
|
+
JOIN expense_tags et ON et.tag_id = t.id
|
|
34
|
+
JOIN expenses e ON e.id = et.expense_id
|
|
35
|
+
WHERE date(e.datetime) >= date('now', '-6 months')
|
|
36
|
+
AND t.is_active = 1
|
|
37
|
+
GROUP BY t.id, t.name
|
|
38
|
+
),
|
|
39
|
+
thresholds AS (
|
|
40
|
+
SELECT kind,
|
|
41
|
+
AVG(CASE WHEN recent > prior THEN recent ELSE prior END) * 0.15 AS min_amt
|
|
42
|
+
FROM period_amounts
|
|
43
|
+
WHERE recent > 0 OR prior > 0
|
|
44
|
+
GROUP BY kind
|
|
45
|
+
)
|
|
46
|
+
SELECT
|
|
47
|
+
p.kind,
|
|
48
|
+
p.name,
|
|
49
|
+
p.recent,
|
|
50
|
+
p.prior,
|
|
51
|
+
ROUND((p.recent - p.prior) * 100.0 / p.prior) AS pct_change,
|
|
52
|
+
CASE WHEN p.recent >= p.prior THEN 'up' ELSE 'down' END AS direction
|
|
53
|
+
FROM period_amounts p
|
|
54
|
+
JOIN thresholds t ON t.kind = p.kind
|
|
55
|
+
WHERE p.prior > 0
|
|
56
|
+
AND (CASE WHEN p.recent > p.prior THEN p.recent ELSE p.prior END) >= t.min_amt
|
|
57
|
+
ORDER BY ABS(p.recent - p.prior) * 1.0 / p.prior DESC
|
|
58
|
+
LIMIT 5
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
SELECT
|
|
2
|
+
ev.id,
|
|
3
|
+
ev.name,
|
|
4
|
+
ev.date_from,
|
|
5
|
+
ev.date_to,
|
|
6
|
+
COALESCE(SUM(e.amount), 0) AS total,
|
|
7
|
+
CASE WHEN ev.date_to >= date('now') THEN 1 ELSE 0 END AS is_open
|
|
8
|
+
FROM events ev
|
|
9
|
+
LEFT JOIN expenses e ON e.event_id = ev.id
|
|
10
|
+
WHERE ev.date_from >= date('now', '-12 months')
|
|
11
|
+
GROUP BY ev.id, ev.name, ev.date_from, ev.date_to
|
|
12
|
+
ORDER BY ev.date_from DESC
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
SELECT
|
|
2
|
+
COALESCE(SUM(CASE WHEN strftime('%Y-%m', datetime) = strftime('%Y-%m', 'now')
|
|
3
|
+
THEN amount ELSE 0 END), 0) AS this_month,
|
|
4
|
+
COALESCE(SUM(CASE WHEN strftime('%Y-%m', datetime) = strftime('%Y-%m', 'now', '-1 month')
|
|
5
|
+
THEN amount ELSE 0 END), 0) AS last_month,
|
|
6
|
+
COALESCE(SUM(CASE WHEN strftime('%Y', datetime) = strftime('%Y', 'now')
|
|
7
|
+
THEN amount ELSE 0 END), 0) AS ytd_expenses
|
|
8
|
+
FROM expenses
|
|
@@ -16,6 +16,7 @@ from dinary import __version__
|
|
|
16
16
|
from dinary.adapters.llm_storage import SqliteLLMBrokerStorage
|
|
17
17
|
from dinary.adapters.llmbroker import LLMBroker
|
|
18
18
|
from dinary.api import (
|
|
19
|
+
analytics,
|
|
19
20
|
catalog,
|
|
20
21
|
currencies,
|
|
21
22
|
expense_corrections,
|
|
@@ -99,6 +100,7 @@ def create_app() -> FastAPI:
|
|
|
99
100
|
lifespan=_lifespan,
|
|
100
101
|
)
|
|
101
102
|
|
|
103
|
+
app.include_router(analytics.router)
|
|
102
104
|
app.include_router(expenses.router)
|
|
103
105
|
app.include_router(expense_corrections.router)
|
|
104
106
|
app.include_router(income.router)
|