dinary 1.2.3__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.3 → dinary-1.3.0}/.github/workflows/ci.yml +23 -3
- {dinary-1.2.3 → dinary-1.3.0}/.github/workflows/static.yml +1 -1
- {dinary-1.2.3 → dinary-1.3.0}/CLAUDE.md +1 -0
- {dinary-1.2.3 → dinary-1.3.0}/PKG-INFO +1 -1
- {dinary-1.2.3 → dinary-1.3.0}/activate.sh +1 -1
- {dinary-1.2.3 → dinary-1.3.0}/docs/mkdocs.yml +1 -0
- dinary-1.3.0/docs/src/en/analytics.md +33 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/index.md +1 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/operations.md +12 -12
- dinary-1.3.0/docs/src/ru/analytics.md +33 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/index.md +1 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/operations.md +12 -12
- {dinary-1.2.3 → dinary-1.3.0}/pyproject.toml +12 -0
- dinary-1.3.0/specs/plans/analytics-ai.md +77 -0
- dinary-1.3.0/specs/reference/analytics-ai.md +201 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/architecture.md +16 -0
- dinary-1.3.0/specs/reference/pwa-analytics.md +43 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/ui/components.md +4 -3
- {dinary-1.2.3 → dinary-1.3.0}/specs/ui/design-language.md +11 -6
- {dinary-1.2.3 → dinary-1.3.0}/specs/ui/screens.md +33 -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.3 → dinary-1.3.0}/src/dinary/main.py +2 -0
- dinary-1.3.0/src/dinary_analytics/__init__.py +0 -0
- dinary-1.3.0/src/dinary_analytics/backup.py +99 -0
- dinary-1.3.0/src/dinary_analytics/charts.py +225 -0
- dinary-1.3.0/src/dinary_analytics/connection.py +96 -0
- dinary-1.3.0/src/dinary_analytics/mcp_server.py +58 -0
- dinary-1.3.0/src/dinary_analytics/notebooks/dashboard.py +509 -0
- dinary-1.3.0/src/dinary_analytics/settings.py +41 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/__init__.py +12 -0
- dinary-1.3.0/tasks/analytics.py +77 -0
- dinary-1.3.0/tasks/backups/analytics_backup.py +162 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backups_replica.py +1 -1
- {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backups_restore.py +1 -1
- {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backups_yandex.py +2 -2
- {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/restore_utils.py +1 -1
- {dinary-1.2.3 → dinary-1.3.0}/tasks/deploy.py +3 -3
- {dinary-1.2.3 → dinary-1.3.0}/tasks/devtools/constants.py +1 -1
- {dinary-1.2.3 → dinary-1.3.0}/tasks/healthcheck.py +42 -1
- {dinary-1.2.3 → dinary-1.3.0}/tasks/setup.py +5 -1
- dinary-1.3.0/tests/analytics/__init__.py +0 -0
- dinary-1.3.0/tests/analytics/test_backup.py +78 -0
- dinary-1.3.0/tests/analytics/test_connection.py +106 -0
- dinary-1.3.0/tests/analytics/test_dashboard.py +344 -0
- dinary-1.3.0/tests/analytics/test_mcp_server.py +75 -0
- dinary-1.3.0/tests/analytics/test_settings.py +68 -0
- dinary-1.3.0/tests/api/test_api_analytics.py +192 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/imports/test_income_extract.py +9 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_backups_restore.py +3 -3
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_server_receipt.py +46 -1
- {dinary-1.2.3 → dinary-1.3.0}/uv.lock +599 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/App.vue +40 -43
- dinary-1.3.0/webapp/src/api/analytics.js +5 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/assets/base.css +10 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ExpenseEditSheet.vue +32 -14
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ExpenseForm.vue +5 -13
- {dinary-1.2.3 → 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.2.3 → dinary-1.3.0}/webapp/src/stores/catalog.js +24 -0
- dinary-1.3.0/webapp/src/views/AnalyticsView.vue +383 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/views/ReviewView.vue +108 -3
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/ExpenseEditSheet.test.js +157 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-app.test.js +5 -5
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-expense-form.test.js +55 -0
- dinary-1.3.0/webapp/tests/component-header-segmented.test.js +74 -0
- dinary-1.2.3/specs/plans/analytics.md +0 -348
- dinary-1.2.3/src/dinary/__about__.py +0 -1
- dinary-1.2.3/webapp/src/components/HeaderSegmented.vue +0 -255
- dinary-1.2.3/webapp/tests/component-header-segmented.test.js +0 -146
- {dinary-1.2.3 → dinary-1.3.0}/.claudeignore +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/.coveragerc +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/.deploy.example/.env +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/.deploy.example/README.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/.deploy.example/import_sources.json +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/.deploy.example/llm_providers.toml +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/.github/workflows/docs.yml +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/.github/workflows/pip_publish.yml +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/.gitignore +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/.pre-commit-config.yaml +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/AGENTS.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/Dockerfile +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/LICENSE +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/README.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docker-compose.yml +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/common/images/about.jpg +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/common/reference.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/cloudflare-setup.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/deploy-oracle.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/deploy-selfhost.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/development.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/google-sheets-setup.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/installation.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/pwa-install.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/cloudflare-setup.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/deploy-oracle.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/deploy-selfhost.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/development.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/google-sheets-setup.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/installation.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/pwa-install.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/taxonomy.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/invoke.yml +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/pytest.ini +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/scripts/verup.sh +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/README.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/catalog-api.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/classification-pipeline.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/currencies.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/frontend-cache.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/income-import.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/llm-providers.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/pwa-offline.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/receipt-fetching.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/sheets.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/sql-tool.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/stores.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/reference/timestamps.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/ui/README.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/ui/future-screens-guide.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/specs/ui/patterns.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/README.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/__init__.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/exchange_rates.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/llm_storage.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/llmbroker.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/nbp.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/nbs.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/rate_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/serbian_receipt_parser.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/sheets_client.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/catalog.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_categories.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_errors.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_events.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_groups.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/expense_corrections.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/expenses.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/income.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/llm.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/qr_parser.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/rules.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/currencies.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/expense_corrections.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/expenses.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/income.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/llm.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/qr.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/receipts.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/rules.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/classification/item_normalizer.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/classification/persist.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/classification/receipt_classifier.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/classification/store_resolver.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/classification/task.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/rate_prefetch/task.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/sheet_logging/income_sheet_logging.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/sheet_logging/logging_jobs.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/sheet_logging/sheet_logging.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/sheet_logging/sheets_write.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/sheet_logging/task.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/config.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/catalog.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/classification_rules.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/currencies.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/db_migrations.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/expenses.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/income.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0001_initial_schema.rollback.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0001_initial_schema.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0002_exchange_rates_source_target.rollback.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0002_exchange_rates_source_target.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0003_app_currencies.rollback.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0003_app_currencies.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0004_receipt_pipeline.rollback.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0004_receipt_pipeline.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0005_income_logging.rollback.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0005_income_logging.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/README.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/receipts.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/__init__.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/get_category_by_name.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/get_existing_expense.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/get_month_expenses.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/insert_expense.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/insert_income.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/list_categories.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/list_incomes.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/logging_projection.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/resolve_mapping_for_year.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/seed_load_categories.sql +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql_loader.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/storage.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/sheets/sheet_mapping.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/src/dinary/sheets/sheets.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backup_retention.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backup_snapshots.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backups_status.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/db.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/devtools/build_docs.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/devtools/dev.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/devtools/env.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/README.md +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/expense_import.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/import_tasks.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/income_extract.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/income_import.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/report_2d_3d.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/seed.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/seed_config.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/seed_derivation.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/verify_equivalence.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/verify_income.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/receipt.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/expenses.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/income.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/report_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/report_tasks.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/verify_budget.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/verify_income.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/server.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/sql.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tasks/ssh_utils.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/_admin_catalog_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/_api_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_admin_catalog_add.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_admin_catalog_delete.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_admin_catalog_meta.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_admin_catalog_patch.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_admin_llm.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_catalog.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_concurrency.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_conflict.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_currencies.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_delete_expense.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_delete_receipt.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_expenses_recent_patch.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_get_expenses.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_income.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_post_expense.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_receipts.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_rules_approve.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_validation.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_receipt_pipeline_e2e.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_receipt_review.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_review_page_ux.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/conftest.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/currency/_currency_rates_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/currency/test_currency_rates_misc.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/currency/test_currency_rates_nbp.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/currency/test_currency_rates_resolve.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/currency/test_rate_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/currency/test_rate_prefetch_task.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/imports/test_expense_import.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/imports/test_seed_config.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/_catalog_writer_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/_ledger_repo_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_catalog_writer_invariants.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_catalog_writer_patch.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_income_db.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_catalog.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_expenses_insert.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_expenses_lookup.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_expenses_race.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_jobs.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_logging_projection.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_migrations.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/reports/_report_2d_3d_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_report_2d_3d.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_report_2d_3d_aggregate.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_report_2d_3d_render.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_report_2d_3d_resolve.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_reports_expenses.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_reports_income.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_reports_verify_budget.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_reports_verify_income.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_classification_rules.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_item_normalizer.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_llm_storage.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_llmbroker.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_qr_parser.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_receipt_classification.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_receipt_classifier.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_receipt_parser.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_sql_loader.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_store_resolver.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/_sheet_logging_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/_sheet_mapping_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/_sheets_helpers.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_income_drain.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_logging.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_logging_derive.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_logging_drain.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_logging_drain_one.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_mapping_parse.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_mapping_reload.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_mapping_resolve.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheets_read.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheets_rows.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_receipt_drain.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_receipt_pipeline.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_reclassify_receipts.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_backups_retention.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_backups_status.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_backups_yandex_setup.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_db.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_deploy.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_dev.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_imports.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_reports.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_restore_utils.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_server.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_setup_replica.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_ssh_utils.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_ssh_utils_scripts.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tools_sql.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/test_config.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/test_main.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/tests/test_webapp_api_contract.py +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/index.html +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/package-lock.json +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/package.json +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/public/apple-touch-icon-precomposed.png +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/public/apple-touch-icon.png +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/public/favicon.ico +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/public/icons/icon-180.png +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/public/icons/icon-192.png +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/public/icons/icon-512.png +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/public/manifest.json +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/_request.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/adminLlm.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/catalog.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/currencies.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/expenseCorrections.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/expenses.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/income.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/receipts.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/review.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/BaseModal.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/BaseSheet.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CatalogSelectField.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CategoryQuickPicks.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CategorySheet.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ConfirmDeleteSheet.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CorrectionSheet.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CurrencyAmountRow.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CurrencyPicker.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/HealthSummaryCard.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/IconBtn.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/IncomeEditSheet.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/IncomeForm.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/IncomeRow.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/InlineCreateEvent.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/InlineCreateRow.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/KeyboardSaveBar.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ManageList.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ProviderCard.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ProviderSheet.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/QrScanner.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/QueueModal.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ReceiptCascadeCard.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/RuleRow.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ScopeSelector.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/StatusDot.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/TagPicker.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/addResult.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/catalogManage.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/flushQueue.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/flushReceiptQueue.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/receipt.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/useExpenseDeleteFlow.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/useKeyboardVisible.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/useOnline.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/useStaleCache.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/useSwipeRow.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/zbar.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/legacy-pwa-cleanup.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/main.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/modals/EditModal.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/currency.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/frequentCategories.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/income.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/llm.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/queue.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/receiptQueue.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/review.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/toast.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/views/AddView.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/views/IncomeView.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/src/views/LLMView.vue +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/BaseSheet.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/CategoryQuickPicks.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/CategorySheet.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/CurrencyAmountRow.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/ExpenseRow.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/ReceiptCascadeCard.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/ScopeSelector.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-adminLlm.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-catalog.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-currencies.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-expenses.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-income.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-receipts.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-request.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-review.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-catalog-select-field.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-correction-sheet.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-currency-picker.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-edit-modal.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-expense-edit-sheet.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-health-summary-card.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-inline-create-event.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-inline-create-row.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-manage-list.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-provider-card.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-provider-sheet.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-queue-modal.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-review-view.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-rule-row.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-status-dot.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-tag-picker.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-add-result.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-catalog-manage.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-flush-queue.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-flush-receipt-queue.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-keyboard-visible.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-receipt.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-stale-cache.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-use-online.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/frequentCategories.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/legacy-pwa-cleanup.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/setup.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-catalog.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-currency.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-income.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-llm.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-queue.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-receipt-queue-durability.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-receipt-queue.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-review.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-toast.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/useExpenseDeleteFlow.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/useSwipeRow.test.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/vite.config.js +0 -0
- {dinary-1.2.3 → dinary-1.3.0}/webapp/vitest.config.js +0 -0
|
@@ -25,6 +25,7 @@ env:
|
|
|
25
25
|
--junitxml=pytest.xml
|
|
26
26
|
--cov-report=term-missing:skip-covered
|
|
27
27
|
--cov=src
|
|
28
|
+
--ignore=tests/analytics
|
|
28
29
|
tests/
|
|
29
30
|
|
|
30
31
|
on:
|
|
@@ -90,7 +91,6 @@ jobs:
|
|
|
90
91
|
with:
|
|
91
92
|
python-version: ${{ env.PRIMARY_PYTHON_VERSION }}
|
|
92
93
|
|
|
93
|
-
|
|
94
94
|
- name: Install uv environment
|
|
95
95
|
uses: andgineer/uv-venv@v3
|
|
96
96
|
|
|
@@ -108,8 +108,28 @@ jobs:
|
|
|
108
108
|
- name: Run JS tests with Allure
|
|
109
109
|
run: npm --prefix webapp test
|
|
110
110
|
|
|
111
|
-
- name: Test with pytest and Allure
|
|
112
|
-
run:
|
|
111
|
+
- name: Test server with pytest and Allure
|
|
112
|
+
run: >-
|
|
113
|
+
python -m pytest
|
|
114
|
+
--junitxml=pytest.xml
|
|
115
|
+
--cov=src
|
|
116
|
+
--cov-report=term-missing:skip-covered
|
|
117
|
+
--ignore=tests/analytics
|
|
118
|
+
--alluredir=./allure-results
|
|
119
|
+
tests/
|
|
120
|
+
|
|
121
|
+
- name: Install analytics dependencies
|
|
122
|
+
run: uv sync --frozen --group analytics
|
|
123
|
+
|
|
124
|
+
- name: Test analytics with pytest and Allure
|
|
125
|
+
run: >-
|
|
126
|
+
python -m pytest
|
|
127
|
+
--junitxml=pytest-analytics.xml
|
|
128
|
+
--cov=src
|
|
129
|
+
--cov-append
|
|
130
|
+
--cov-report=term-missing:skip-covered
|
|
131
|
+
--alluredir=./allure-results
|
|
132
|
+
tests/analytics/
|
|
113
133
|
|
|
114
134
|
- name: Load Allure test report history
|
|
115
135
|
uses: actions/checkout@v4
|
|
@@ -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/
|
|
@@ -41,7 +41,7 @@ if [[ ! -d ${VENV_FOLDER} ]] ; then
|
|
|
41
41
|
if uv venv ${VENV_FOLDER} --python=python${PRIMARY_PYTHON_VERSION}; then
|
|
42
42
|
|
|
43
43
|
. ${VENV_FOLDER}/bin/activate
|
|
44
|
-
uv sync --frozen
|
|
44
|
+
uv sync --frozen --group analytics
|
|
45
45
|
END_TIME=$(date +%s)
|
|
46
46
|
echo "Environment created in $((END_TIME - $START_TIME)) seconds"
|
|
47
47
|
else
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Your Personal Financial Analyst
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
inv analytics
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Opens a browser page at `http://localhost:2718` where you can **chat with your own spending data** in plain language — and browse interactive charts while you do it.
|
|
8
|
+
|
|
9
|
+
Ask things like:
|
|
10
|
+
|
|
11
|
+
- *"What did I spend most on last month?"*
|
|
12
|
+
- *"How does my food spending compare to last year?"*
|
|
13
|
+
- *"How much did the Italy trip cost, broken down by category?"*
|
|
14
|
+
- *"What's my savings rate for 2025?"*
|
|
15
|
+
|
|
16
|
+
The analyst knows your full expense history, categories, events, and tags. It queries your local database live — nothing leaves your machine.
|
|
17
|
+
|
|
18
|
+
## Charts
|
|
19
|
+
|
|
20
|
+
Alongside the chat, four visual summaries give you the big picture at a glance:
|
|
21
|
+
|
|
22
|
+
| | What it shows |
|
|
23
|
+
|---|---|
|
|
24
|
+
| **12-month rolling** | Top-10 categories stacked by month, with income and monthly savings |
|
|
25
|
+
| **Year comparison** | Any previous year overlaid on the rolling view |
|
|
26
|
+
| **Event** | Where the money went during a trip or project |
|
|
27
|
+
| **Tag** | Spending pattern for a label (e.g. "work", "dog") across a chosen year |
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
The chat requires a Gemini API key configured as a provider in `.deploy/llm_providers.toml`. Without it, the charts still work — only the chat shows a warning.
|
|
32
|
+
|
|
33
|
+
The **year comparison**, **tag**, and **tag year** selectors remember your last choice across restarts. The **event** selector always opens on the most recent completed event.
|
|
@@ -18,3 +18,4 @@ Dinary server is a FastAPI backend that:
|
|
|
18
18
|
- [Your own computer](deploy-selfhost.md) — $0 (Tailscale Funnel or Cloudflare Tunnel)
|
|
19
19
|
3. Set up HTTPS access — see deployment guides above.
|
|
20
20
|
4. [Install the PWA](pwa-install.md) on your phone.
|
|
21
|
+
5. Run `inv analytics` to talk to [your personal financial analyst](analytics.md).
|
|
@@ -333,7 +333,7 @@ within hours rather than the next morning.
|
|
|
333
333
|
|
|
334
334
|
### Local Yandex.Disk access: `inv setup-yadisk`
|
|
335
335
|
|
|
336
|
-
`restore-
|
|
336
|
+
`restore-yadisk` reads from `yandex:` on whichever machine it
|
|
337
337
|
runs. `inv setup-yadisk` configures that remote locally — on VM 1
|
|
338
338
|
during disaster recovery, or on the operator laptop for debug
|
|
339
339
|
bootstraps:
|
|
@@ -346,16 +346,16 @@ Uses the same WebDAV + app-password flow as `inv setup-replica` (no
|
|
|
346
346
|
browser OAuth needed). Idempotent: skips the prompt if `yandex:` is
|
|
347
347
|
already configured and healthy.
|
|
348
348
|
|
|
349
|
-
`restore-
|
|
349
|
+
`restore-yadisk` calls this automatically when `yandex:` is
|
|
350
350
|
absent, so running it beforehand is optional.
|
|
351
351
|
|
|
352
352
|
## Point-in-time restore from Yandex.Disk
|
|
353
353
|
|
|
354
354
|
```bash
|
|
355
|
-
inv restore-
|
|
356
|
-
inv restore-
|
|
357
|
-
inv restore-
|
|
358
|
-
inv restore-
|
|
355
|
+
inv restore-yadisk --list-only # show inventory
|
|
356
|
+
inv restore-yadisk # restore latest
|
|
357
|
+
inv restore-yadisk --snapshot 2026-03-15 # specific date
|
|
358
|
+
inv restore-yadisk --yes # skip confirm
|
|
359
359
|
```
|
|
360
360
|
|
|
361
361
|
### Two intended use cases
|
|
@@ -369,10 +369,10 @@ inv restore-cloud-backup --yes # skip confirm
|
|
|
369
369
|
(via SSH) when both the local DB and the Litestream replica on
|
|
370
370
|
VM 2 are unusable. The SSH + `cd ~/dinary` + interactive
|
|
371
371
|
confirmation hops are intentional friction so a one-word
|
|
372
|
-
`inv restore-
|
|
372
|
+
`inv restore-yadisk` on the wrong terminal cannot silently
|
|
373
373
|
overwrite prod.
|
|
374
374
|
|
|
375
|
-
`restore-
|
|
375
|
+
`restore-yadisk` is **local-only** — it writes to
|
|
376
376
|
`./data/dinary.db` relative to the cwd and has no `--remote` mode.
|
|
377
377
|
There is no way to invoke it against a remote host from the
|
|
378
378
|
operator machine.
|
|
@@ -391,7 +391,7 @@ operator machine.
|
|
|
391
391
|
present on VM 1 via `inv setup-server`. On macOS: `brew install
|
|
392
392
|
rclone sqlite zstd`.
|
|
393
393
|
- A `yandex:` rclone remote configured locally. If it is absent,
|
|
394
|
-
**`restore-
|
|
394
|
+
**`restore-yadisk` prompts automatically** (same WebDAV +
|
|
395
395
|
app-password flow as `inv setup-replica`). To pre-configure before
|
|
396
396
|
the first restore run: `inv setup-yadisk`.
|
|
397
397
|
|
|
@@ -415,7 +415,7 @@ unusable:
|
|
|
415
415
|
ssh ubuntu@dinary # or the public IP / Tailscale IP
|
|
416
416
|
sudo systemctl stop dinary litestream # avoid a half-written DB
|
|
417
417
|
cd ~/dinary
|
|
418
|
-
inv restore-
|
|
418
|
+
inv restore-yadisk --snapshot 2026-03-15
|
|
419
419
|
# confirmation prompt: shows row count / size / mtime of the
|
|
420
420
|
# current DB plus compressed size of the incoming snapshot, then
|
|
421
421
|
# asks for literal 'yes'.
|
|
@@ -430,7 +430,7 @@ directory (so `./data/dinary.db` is the snapshot, not prod):
|
|
|
430
430
|
```bash
|
|
431
431
|
cd /tmp/restore-preview
|
|
432
432
|
mkdir -p data
|
|
433
|
-
inv restore-
|
|
433
|
+
inv restore-yadisk --snapshot 2026-03-15 --yes
|
|
434
434
|
sqlite3 data/dinary.db 'SELECT COUNT(*) FROM expense'
|
|
435
435
|
```
|
|
436
436
|
|
|
@@ -448,7 +448,7 @@ To deploy a specific version with a DB restore (e.g. rollback after a bad deploy
|
|
|
448
448
|
|
|
449
449
|
```bash
|
|
450
450
|
inv deploy --ref=v0.4.0 --no-start # deploy code but skip service start
|
|
451
|
-
inv restore-
|
|
451
|
+
inv restore-yadisk # restore DB from Yandex.Disk
|
|
452
452
|
inv restart-server # start; yoyo applies forward migrations
|
|
453
453
|
```
|
|
454
454
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Персональный финансовый аналитик
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
inv analytics
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Открывает страницу в браузере на `http://localhost:2718`, где можно **разговаривать со своими данными о расходах** на обычном языке — и параллельно смотреть интерактивные графики.
|
|
8
|
+
|
|
9
|
+
Спрашивайте что угодно:
|
|
10
|
+
|
|
11
|
+
- *«На что я потратил больше всего в прошлом месяце?»*
|
|
12
|
+
- *«Как изменились траты на еду по сравнению с прошлым годом?»*
|
|
13
|
+
- *«Сколько стоила поездка в Белград по категориям?»*
|
|
14
|
+
- *«Какая у меня норма сбережений за 2025 год?»*
|
|
15
|
+
|
|
16
|
+
Аналитик знает полную историю расходов, категории, события и теги. Запросы выполняются к локальной базе данных — данные никуда не уходят.
|
|
17
|
+
|
|
18
|
+
## Визуальные сводки
|
|
19
|
+
|
|
20
|
+
Рядом с чатом — четыре диаграммы для быстрого взгляда на картину в целом:
|
|
21
|
+
|
|
22
|
+
| | Что показывает |
|
|
23
|
+
|---|---|
|
|
24
|
+
| **Скользящие 12 месяцев** | Топ-10 категорий по месяцам, доходы и ежемесячная экономия |
|
|
25
|
+
| **Сравнение по годам** | Любой прошлый год рядом с текущим |
|
|
26
|
+
| **По событию** | Куда ушли деньги во время поездки или проекта |
|
|
27
|
+
| **По тегу** | Структура трат по метке («работа», «собака» и т.д.) за выбранный год |
|
|
28
|
+
|
|
29
|
+
## Настройка
|
|
30
|
+
|
|
31
|
+
Чат требует API-ключ Gemini, настроенный как провайдер в `.deploy/llm_providers.toml`. Без него диаграммы работают в штатном режиме — только в области чата появляется предупреждение.
|
|
32
|
+
|
|
33
|
+
Селекторы **сравнения лет**, **тега** и **года тега** запоминают выбор между запусками. Селектор **события** всегда открывается на последнем завершённом событии.
|
|
@@ -19,3 +19,4 @@ Dinary server — бэкенд на FastAPI, который:
|
|
|
19
19
|
3. Первоначально загружается [Классификатор](taxonomy.md) который вы далее можете корректировать
|
|
20
20
|
4. Настройте HTTPS-доступ — см. инструкции по деплою выше.
|
|
21
21
|
4. [Установите PWA](pwa-install.md) на телефон.
|
|
22
|
+
5. Запустите `inv analytics` — своего [персонального финансового аналитика](analytics.md).
|
|
@@ -346,7 +346,7 @@ UTC уже через несколько часов, а не следующим
|
|
|
346
346
|
|
|
347
347
|
### Локальный доступ к Яндекс.Диску: `inv setup-yadisk`
|
|
348
348
|
|
|
349
|
-
`restore-
|
|
349
|
+
`restore-yadisk` читает из `yandex:` на той машине, где
|
|
350
350
|
запущена. `inv setup-yadisk` настраивает этот remote локально — на
|
|
351
351
|
VM 1 при disaster recovery или на ноутбуке для отладочного
|
|
352
352
|
бутстрапа:
|
|
@@ -359,16 +359,16 @@ inv setup-yadisk
|
|
|
359
359
|
setup-replica`, — браузерный OAuth не нужен. Идемпотентна:
|
|
360
360
|
пропускает промпт, если `yandex:` уже настроен и работает.
|
|
361
361
|
|
|
362
|
-
`restore-
|
|
362
|
+
`restore-yadisk` вызывает её автоматически при отсутствии
|
|
363
363
|
`yandex:`, так что ручной запуск заранее необязателен.
|
|
364
364
|
|
|
365
365
|
## Восстановление на конкретную дату из Яндекс.Диска
|
|
366
366
|
|
|
367
367
|
```bash
|
|
368
|
-
inv restore-
|
|
369
|
-
inv restore-
|
|
370
|
-
inv restore-
|
|
371
|
-
inv restore-
|
|
368
|
+
inv restore-yadisk --list-only # список снапшотов
|
|
369
|
+
inv restore-yadisk # восстановить самый свежий
|
|
370
|
+
inv restore-yadisk --snapshot 2026-03-15 # конкретную дату
|
|
371
|
+
inv restore-yadisk --yes # без подтверждения
|
|
372
372
|
```
|
|
373
373
|
|
|
374
374
|
### Два предполагаемых сценария
|
|
@@ -382,10 +382,10 @@ inv restore-cloud-backup --yes # без подтвер
|
|
|
382
382
|
(через SSH), когда и локальная БД, и Litestream-реплика на VM 2
|
|
383
383
|
непригодны. Тройная защита «SSH + `cd ~/dinary` + интерактивное
|
|
384
384
|
подтверждение» — это намеренное трение, чтобы одним словом
|
|
385
|
-
`inv restore-
|
|
385
|
+
`inv restore-yadisk` в случайном терминале нельзя было
|
|
386
386
|
молча затереть прод.
|
|
387
387
|
|
|
388
|
-
`restore-
|
|
388
|
+
`restore-yadisk` — **local-only**: пишет в `./data/dinary.db`
|
|
389
389
|
относительно cwd, режима `--remote` нет. Запустить задачу на
|
|
390
390
|
удалённом хосте с операторской машины невозможно.
|
|
391
391
|
|
|
@@ -403,7 +403,7 @@ inv restore-cloud-backup --yes # без подтвер
|
|
|
403
403
|
есть через `inv setup-server`. На macOS: `brew install rclone
|
|
404
404
|
sqlite zstd`.
|
|
405
405
|
- Настроен удалённый `yandex:` в rclone. Если его нет,
|
|
406
|
-
**`restore-
|
|
406
|
+
**`restore-yadisk` сам запросит авторизацию** (та же схема
|
|
407
407
|
WebDAV + app-пароль, что у `inv setup-replica`). Для
|
|
408
408
|
предварительной настройки до первого restore: `inv setup-yadisk`.
|
|
409
409
|
|
|
@@ -427,7 +427,7 @@ inv restore-cloud-backup --yes # без подтвер
|
|
|
427
427
|
ssh ubuntu@dinary # или публичный IP / Tailscale IP
|
|
428
428
|
sudo systemctl stop dinary litestream # чтобы не получить наполовину переписанную БД
|
|
429
429
|
cd ~/dinary
|
|
430
|
-
inv restore-
|
|
430
|
+
inv restore-yadisk --snapshot 2026-03-15
|
|
431
431
|
# промпт подтверждения: печатает row count / size / mtime текущей
|
|
432
432
|
# БД плюс сжатый размер входящего снапшота и требует напечатать
|
|
433
433
|
# буквально 'yes'.
|
|
@@ -442,7 +442,7 @@ inv verify-db # integrity + FK check
|
|
|
442
442
|
```bash
|
|
443
443
|
cd /tmp/restore-preview
|
|
444
444
|
mkdir -p data
|
|
445
|
-
inv restore-
|
|
445
|
+
inv restore-yadisk --snapshot 2026-03-15 --yes
|
|
446
446
|
sqlite3 data/dinary.db 'SELECT COUNT(*) FROM expense'
|
|
447
447
|
```
|
|
448
448
|
|
|
@@ -461,7 +461,7 @@ sqlite3 data/dinary.db 'SELECT COUNT(*) FROM expense'
|
|
|
461
461
|
|
|
462
462
|
```bash
|
|
463
463
|
inv deploy --ref=v0.4.0 --no-start # задеплоить код, сервис не запускать
|
|
464
|
-
inv restore-
|
|
464
|
+
inv restore-yadisk # восстановить БД с Яндекс.Диска
|
|
465
465
|
inv restart-server # запустить; yoyo применит прямые миграции
|
|
466
466
|
```
|
|
467
467
|
|
|
@@ -57,6 +57,9 @@ search_path = ["src", "."]
|
|
|
57
57
|
[tool.ruff]
|
|
58
58
|
line-length = 99
|
|
59
59
|
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
per-file-ignores = { "src/dinary_analytics/notebooks/*.py" = ["PLC0415", "N803", "N806", "B018"] }
|
|
62
|
+
|
|
60
63
|
[tool.ruff.lint.pylint]
|
|
61
64
|
max-args = 8
|
|
62
65
|
|
|
@@ -82,6 +85,15 @@ dev = [
|
|
|
82
85
|
"pytest-timeout>=2.3.1",
|
|
83
86
|
"zensical>=0.0.41",
|
|
84
87
|
]
|
|
88
|
+
analytics = [
|
|
89
|
+
"duckdb>=1.2.0",
|
|
90
|
+
"lmdb>=1.6.0",
|
|
91
|
+
"marimo>=0.10.0",
|
|
92
|
+
"google-genai>=1.0.0",
|
|
93
|
+
"mcp>=1.9.0",
|
|
94
|
+
"altair>=5.0.0",
|
|
95
|
+
"polars>=1.0.0",
|
|
96
|
+
]
|
|
85
97
|
|
|
86
98
|
[project.scripts]
|
|
87
99
|
dinary = "dinary.main:main"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Analytics AI — implementation plan
|
|
2
|
+
|
|
3
|
+
See `specs/reference/analytics-ai.md` for architecture, storage design, LLM
|
|
4
|
+
strategy, invariants, and Analytics Views design.
|
|
5
|
+
|
|
6
|
+
## Remaining deliverables
|
|
7
|
+
|
|
8
|
+
### Template notebooks and extended dashboard
|
|
9
|
+
|
|
10
|
+
1. `notebooks/events.py` — event/trip cost breakdown notebook.
|
|
11
|
+
2. `notebooks/tags.py` — tag-bucket comparison notebook.
|
|
12
|
+
3. `dashboard.py` extended to full configurable widget set.
|
|
13
|
+
|
|
14
|
+
### MCP server extensions
|
|
15
|
+
|
|
16
|
+
4. `get_config(key)` and `set_config(key, value)` tools in `mcp_server.py`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### AI Views feature
|
|
21
|
+
|
|
22
|
+
5. **`queries/spending_summary.sql`** — aggregates last-12-months expenses into
|
|
23
|
+
three result sets: events (id, name, total_amount, date_from, date_to),
|
|
24
|
+
tags (id, name, expense_count, total_amount), category groups (id, name,
|
|
25
|
+
total_amount). Used by the LLM before proposing a new view.
|
|
26
|
+
|
|
27
|
+
6. **`queries/view_data.sql`** — given a basket config passed as a JSON parameter,
|
|
28
|
+
assigns each expense to its first-matching basket (event match checked before
|
|
29
|
+
tag match, unmatched → default basket name), then aggregates by
|
|
30
|
+
(basket_name, year_month, group_name). Returns one row per
|
|
31
|
+
(basket, month, group) triple.
|
|
32
|
+
|
|
33
|
+
7. **`settings.py` extensions** — `list_view_ids() → list[str]`, `get_view(id)`,
|
|
34
|
+
`save_view(config: dict)`, `delete_view(id)`. Keys in LMDB: `view:<uuid>`.
|
|
35
|
+
|
|
36
|
+
8. **MCP server** — expose `list_views`, `get_view(id)`, `save_view(config)`,
|
|
37
|
+
`delete_view(id)` tools so Claude Desktop / Claude Code can manage views
|
|
38
|
+
externally.
|
|
39
|
+
|
|
40
|
+
9. **In-session LLM tools in `dashboard.py`** (Gemini chat tool definitions):
|
|
41
|
+
- `query_spending_summary()` → runs `spending_summary.sql`, returns JSON
|
|
42
|
+
- `propose_view(baskets, default_basket, chart_type)` → sets the in-memory
|
|
43
|
+
draft view config and triggers chart re-render; does not save
|
|
44
|
+
- `update_basket(name, event_ids, tag_ids)` → modifies a basket in the draft
|
|
45
|
+
- `remove_basket(name)` → removes a basket from the draft
|
|
46
|
+
- `set_chart_type(type)` → updates draft chart type
|
|
47
|
+
- `save_current_view(name)` → persists draft via `settings.save_view()`
|
|
48
|
+
- `delete_view(id)` → removes a saved view via `settings.delete_view()`
|
|
49
|
+
|
|
50
|
+
10. **Altair chart for basket views** — stacked bar: X = year_month, Y = amount,
|
|
51
|
+
color = basket_name. On click of a bar segment: filter to that basket + period
|
|
52
|
+
and show a secondary stacked bar by group_name as a drill-down panel below.
|
|
53
|
+
Use `alt.selection_point` on basket + month for the drill-down interaction.
|
|
54
|
+
|
|
55
|
+
11. **View selector UI in `dashboard.py`** — `mo.ui.dropdown` populated from
|
|
56
|
+
`settings.list_view_ids()` + labels from stored configs; "New view" button
|
|
57
|
+
clears the draft and triggers the LLM with the `query_spending_summary()`
|
|
58
|
+
result plus instructions to propose baskets with chart. Period selector
|
|
59
|
+
(year / custom range) shown alongside the chart.
|
|
60
|
+
|
|
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.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Analytics standalone app
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
A standalone application installed locally by each user. Opens a browser dashboard
|
|
6
|
+
(Marimo in `run` mode — no code visible), provides a natural-language AI chat, and
|
|
7
|
+
connects to the dinary ledger via a local read-only replica. Non-technical users
|
|
8
|
+
see a clean web app; power users additionally connect Claude Code or Claude Desktop
|
|
9
|
+
via the MCP server.
|
|
10
|
+
|
|
11
|
+
Out of scope: OLTP writes, sheet logging, imports, migrations, API surface.
|
|
12
|
+
|
|
13
|
+
## Repository placement
|
|
14
|
+
|
|
15
|
+
`src/dinary_analytics/` alongside `src/dinary/` in the monorepo root. One-way
|
|
16
|
+
dependency: `dinary_analytics` imports from `dinary`; `dinary` never imports
|
|
17
|
+
from `dinary_analytics`. Heavy deps (DuckDB, Polars, Marimo, LLM SDKs) live in
|
|
18
|
+
the `analytics` dependency group in the root `pyproject.toml`.
|
|
19
|
+
|
|
20
|
+
`uv sync` on the laptop installs everything. The deploy task runs
|
|
21
|
+
`uv sync --no-dev --no-group analytics` on VM 1, keeping the server image lean.
|
|
22
|
+
|
|
23
|
+
## Package structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/dinary_analytics/
|
|
27
|
+
connection.py # read-only DuckDB ATTACH to ledger-replica.db
|
|
28
|
+
mcp_server.py # MCP server: DuckDB queries + analytics.db writes
|
|
29
|
+
settings.py # analytics.db read/write (LMDB)
|
|
30
|
+
backup.py # analytics.db backup/restore CLI
|
|
31
|
+
queries/ # named .sql files for reusable analytical queries
|
|
32
|
+
notebooks/
|
|
33
|
+
dashboard.py # main app: charts + Gemini chat + AI views
|
|
34
|
+
events.py # event/trip cost breakdown
|
|
35
|
+
tags.py # tag-bucket comparison
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Runtime directory
|
|
39
|
+
|
|
40
|
+
`.analytics/` at the repo root, gitignored. Created on first `inv analytics`.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
.analytics/
|
|
44
|
+
ledger-replica.db # read-only SQLite replica of dinary ledger
|
|
45
|
+
analytics.db # app database: view configs, history (LMDB)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Storage
|
|
49
|
+
|
|
50
|
+
### ledger-replica.db
|
|
51
|
+
|
|
52
|
+
Read-only SQLite replica of the dinary server DB. Synced on every `inv analytics`
|
|
53
|
+
run before Marimo starts. DuckDB opens it with `ATTACH ... (READ_ONLY)` — never
|
|
54
|
+
writable.
|
|
55
|
+
|
|
56
|
+
### analytics.db — LMDB
|
|
57
|
+
|
|
58
|
+
Holds: analytics view configs, dashboard configurations, LLM conversation history.
|
|
59
|
+
DuckDB never stores application state; any materialized caches there are disposable
|
|
60
|
+
and regenerated from the replica.
|
|
61
|
+
|
|
62
|
+
`analytics.db` must be backed up. `ledger-replica.db` is reproducible and excluded
|
|
63
|
+
from backup.
|
|
64
|
+
|
|
65
|
+
## SQL-first design
|
|
66
|
+
|
|
67
|
+
Analytical business logic lives in SQL executed by DuckDB, not in Python DataFrames.
|
|
68
|
+
Python owns orchestration, plotting, and LLM glue. Keeps queries portable to
|
|
69
|
+
DuckDB-WASM without rewrite.
|
|
70
|
+
|
|
71
|
+
## LLM strategy
|
|
72
|
+
|
|
73
|
+
**Default — Gemini Free API.** Built into the Marimo dashboard chat. User provides
|
|
74
|
+
a Google AI Studio API key. The model receives the ledger schema and executes DuckDB
|
|
75
|
+
queries via tool calls.
|
|
76
|
+
|
|
77
|
+
**Power users — Claude Code / Claude Desktop via MCP.** User connects their Claude
|
|
78
|
+
subscription to the `dinary-analytics` MCP server. Claude can answer arbitrary
|
|
79
|
+
questions and reconfigure dashboards (view configs, widget order) by writing to
|
|
80
|
+
`analytics.db`.
|
|
81
|
+
|
|
82
|
+
## inv analytics
|
|
83
|
+
|
|
84
|
+
Single entry point. On every run:
|
|
85
|
+
|
|
86
|
+
1. Syncs `ledger-replica.db` from the dinary server.
|
|
87
|
+
2. Starts the MCP server.
|
|
88
|
+
3. Opens `notebooks/dashboard.py` via `marimo run`.
|
|
89
|
+
|
|
90
|
+
## MCP server
|
|
91
|
+
|
|
92
|
+
- `query(sql)` — read-only DuckDB query against the ledger replica, returns JSON.
|
|
93
|
+
- `schema()` — ledger schema for LLM context.
|
|
94
|
+
- `get_config(key)` — reads a config entry from `analytics.db`.
|
|
95
|
+
- `set_config(key, value)` — writes a config entry to `analytics.db`.
|
|
96
|
+
- `list_views()`, `get_view(id)`, `save_view(config)`, `delete_view(id)` — manage analytics view configs.
|
|
97
|
+
|
|
98
|
+
`set_config` and the view management tools are the only write paths to `analytics.db`.
|
|
99
|
+
|
|
100
|
+
## PWA integration
|
|
101
|
+
|
|
102
|
+
View configs and tag bucket definitions sync to the PWA via `PUT /api/analytics/config`
|
|
103
|
+
on the dinary server (see `analytics-pwa.md`).
|
|
104
|
+
|
|
105
|
+
## Runtime tiers
|
|
106
|
+
|
|
107
|
+
**Tier 1 — laptop/desktop (primary).** `inv analytics` opens Marimo in the browser.
|
|
108
|
+
Full AI integration. Zero extra services.
|
|
109
|
+
|
|
110
|
+
**Tier 2 — DuckDB-WASM in browser (deferred).** SQL-first design keeps this
|
|
111
|
+
achievable without rewrite.
|
|
112
|
+
|
|
113
|
+
## Invariants
|
|
114
|
+
|
|
115
|
+
- Analytics is strictly read-only against `ledger.*`. `ATTACH (READ_ONLY)` enforced at connection level.
|
|
116
|
+
- `dinary` server package never depends on `dinary_analytics`.
|
|
117
|
+
- DuckDB is a query engine only — no application state stored there.
|
|
118
|
+
- `analytics.db` is the sole write target for configs and history.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Analytics Views
|
|
123
|
+
|
|
124
|
+
### Concept
|
|
125
|
+
|
|
126
|
+
An analytics view is a named, reusable grouping configuration that organises expenses
|
|
127
|
+
into user-defined baskets for charting. Each view is live: when opened it re-executes
|
|
128
|
+
against current ledger data. The user selects the time period at open time.
|
|
129
|
+
|
|
130
|
+
Multiple views can be saved, renamed, copied, and deleted.
|
|
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
|
+
|
|
148
|
+
### Basket structure
|
|
149
|
+
|
|
150
|
+
A view contains an ordered list of baskets plus a default basket name for unmatched
|
|
151
|
+
expenses. A basket matches an expense if any of its trigger conditions are met (OR
|
|
152
|
+
logic within triggers). Priority is first-match: an expense is assigned to the first
|
|
153
|
+
basket whose triggers match it.
|
|
154
|
+
|
|
155
|
+
Basket triggers:
|
|
156
|
+
- `events` — list of event IDs: matches any expense belonging to those events.
|
|
157
|
+
- `tags` — list of tag IDs: matches any expense carrying any of those tags.
|
|
158
|
+
|
|
159
|
+
Within each basket the breakdown by category group is always available as a
|
|
160
|
+
drill-down.
|
|
161
|
+
|
|
162
|
+
View config stored in `analytics.db` under key `view:<uuid>`:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"id": "<uuid>",
|
|
167
|
+
"name": "По смыслу жизни",
|
|
168
|
+
"baskets": [
|
|
169
|
+
{ "name": "Релокация", "triggers": { "events": [3], "tags": [] } },
|
|
170
|
+
{ "name": "Путешествия", "triggers": { "events": [], "tags": [7] } }
|
|
171
|
+
],
|
|
172
|
+
"default_basket": "Основное",
|
|
173
|
+
"chart_type": "stacked_bar_monthly"
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### LLM interaction model
|
|
178
|
+
|
|
179
|
+
The LLM is the analyst; the user is the client who reacts to proposals.
|
|
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
|
+
|
|
187
|
+
**Creating a new view.** The LLM calls `query_spending_summary()` to examine actual
|
|
188
|
+
spending patterns before saying anything. It then presents an initial basket proposal
|
|
189
|
+
with a rendered chart and justifies each basket with concrete data: "Релокация —
|
|
190
|
+
45k за 3 мес (40% квартала), выделил в отдельный блок." The user never has to name
|
|
191
|
+
categories, events, or tags.
|
|
192
|
+
|
|
193
|
+
**Refining a view.** The user expresses dissatisfaction about what they see in the
|
|
194
|
+
chart ("поездки выглядят странно"). The LLM consults the data and proposes specific
|
|
195
|
+
alternatives: "Могу объединить все поездки в один блок Путешествия — тогда видна
|
|
196
|
+
общая сумма за год. Или оставить детализацию с итоговой строкой. Что ближе?" The
|
|
197
|
+
user only approves or redirects; the LLM modifies the config via in-session tools
|
|
198
|
+
and the chart re-renders immediately.
|
|
199
|
+
|
|
200
|
+
Users never compose basket definitions by naming categories or writing rules. The
|
|
201
|
+
LLM does this from data.
|