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.
Files changed (453) hide show
  1. {dinary-1.2.3 → dinary-1.3.0}/.github/workflows/ci.yml +23 -3
  2. {dinary-1.2.3 → dinary-1.3.0}/.github/workflows/static.yml +1 -1
  3. {dinary-1.2.3 → dinary-1.3.0}/CLAUDE.md +1 -0
  4. {dinary-1.2.3 → dinary-1.3.0}/PKG-INFO +1 -1
  5. {dinary-1.2.3 → dinary-1.3.0}/activate.sh +1 -1
  6. {dinary-1.2.3 → dinary-1.3.0}/docs/mkdocs.yml +1 -0
  7. dinary-1.3.0/docs/src/en/analytics.md +33 -0
  8. {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/index.md +1 -0
  9. {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/operations.md +12 -12
  10. dinary-1.3.0/docs/src/ru/analytics.md +33 -0
  11. {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/index.md +1 -0
  12. {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/operations.md +12 -12
  13. {dinary-1.2.3 → dinary-1.3.0}/pyproject.toml +12 -0
  14. dinary-1.3.0/specs/plans/analytics-ai.md +77 -0
  15. dinary-1.3.0/specs/reference/analytics-ai.md +201 -0
  16. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/architecture.md +16 -0
  17. dinary-1.3.0/specs/reference/pwa-analytics.md +43 -0
  18. {dinary-1.2.3 → dinary-1.3.0}/specs/ui/components.md +4 -3
  19. {dinary-1.2.3 → dinary-1.3.0}/specs/ui/design-language.md +11 -6
  20. {dinary-1.2.3 → dinary-1.3.0}/specs/ui/screens.md +33 -26
  21. dinary-1.3.0/src/dinary/__about__.py +1 -0
  22. dinary-1.3.0/src/dinary/api/analytics.py +80 -0
  23. dinary-1.3.0/src/dinary/db/sql/analytics_auto_trends.sql +58 -0
  24. dinary-1.3.0/src/dinary/db/sql/analytics_events.sql +12 -0
  25. dinary-1.3.0/src/dinary/db/sql/analytics_summary.sql +8 -0
  26. dinary-1.3.0/src/dinary/db/sql/analytics_ytd_income.sql +3 -0
  27. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/main.py +2 -0
  28. dinary-1.3.0/src/dinary_analytics/__init__.py +0 -0
  29. dinary-1.3.0/src/dinary_analytics/backup.py +99 -0
  30. dinary-1.3.0/src/dinary_analytics/charts.py +225 -0
  31. dinary-1.3.0/src/dinary_analytics/connection.py +96 -0
  32. dinary-1.3.0/src/dinary_analytics/mcp_server.py +58 -0
  33. dinary-1.3.0/src/dinary_analytics/notebooks/dashboard.py +509 -0
  34. dinary-1.3.0/src/dinary_analytics/settings.py +41 -0
  35. {dinary-1.2.3 → dinary-1.3.0}/tasks/__init__.py +12 -0
  36. dinary-1.3.0/tasks/analytics.py +77 -0
  37. dinary-1.3.0/tasks/backups/analytics_backup.py +162 -0
  38. {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backups_replica.py +1 -1
  39. {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backups_restore.py +1 -1
  40. {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backups_yandex.py +2 -2
  41. {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/restore_utils.py +1 -1
  42. {dinary-1.2.3 → dinary-1.3.0}/tasks/deploy.py +3 -3
  43. {dinary-1.2.3 → dinary-1.3.0}/tasks/devtools/constants.py +1 -1
  44. {dinary-1.2.3 → dinary-1.3.0}/tasks/healthcheck.py +42 -1
  45. {dinary-1.2.3 → dinary-1.3.0}/tasks/setup.py +5 -1
  46. dinary-1.3.0/tests/analytics/__init__.py +0 -0
  47. dinary-1.3.0/tests/analytics/test_backup.py +78 -0
  48. dinary-1.3.0/tests/analytics/test_connection.py +106 -0
  49. dinary-1.3.0/tests/analytics/test_dashboard.py +344 -0
  50. dinary-1.3.0/tests/analytics/test_mcp_server.py +75 -0
  51. dinary-1.3.0/tests/analytics/test_settings.py +68 -0
  52. dinary-1.3.0/tests/api/test_api_analytics.py +192 -0
  53. {dinary-1.2.3 → dinary-1.3.0}/tests/imports/test_income_extract.py +9 -0
  54. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_backups_restore.py +3 -3
  55. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_server_receipt.py +46 -1
  56. {dinary-1.2.3 → dinary-1.3.0}/uv.lock +599 -0
  57. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/App.vue +40 -43
  58. dinary-1.3.0/webapp/src/api/analytics.js +5 -0
  59. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/assets/base.css +10 -0
  60. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ExpenseEditSheet.vue +32 -14
  61. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ExpenseForm.vue +5 -13
  62. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ExpenseRow.vue +2 -0
  63. dinary-1.3.0/webapp/src/components/HeaderSegmented.vue +96 -0
  64. dinary-1.3.0/webapp/src/stores/analytics.js +49 -0
  65. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/catalog.js +24 -0
  66. dinary-1.3.0/webapp/src/views/AnalyticsView.vue +383 -0
  67. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/views/ReviewView.vue +108 -3
  68. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/ExpenseEditSheet.test.js +157 -0
  69. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-app.test.js +5 -5
  70. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-expense-form.test.js +55 -0
  71. dinary-1.3.0/webapp/tests/component-header-segmented.test.js +74 -0
  72. dinary-1.2.3/specs/plans/analytics.md +0 -348
  73. dinary-1.2.3/src/dinary/__about__.py +0 -1
  74. dinary-1.2.3/webapp/src/components/HeaderSegmented.vue +0 -255
  75. dinary-1.2.3/webapp/tests/component-header-segmented.test.js +0 -146
  76. {dinary-1.2.3 → dinary-1.3.0}/.claudeignore +0 -0
  77. {dinary-1.2.3 → dinary-1.3.0}/.coveragerc +0 -0
  78. {dinary-1.2.3 → dinary-1.3.0}/.deploy.example/.env +0 -0
  79. {dinary-1.2.3 → dinary-1.3.0}/.deploy.example/README.md +0 -0
  80. {dinary-1.2.3 → dinary-1.3.0}/.deploy.example/import_sources.json +0 -0
  81. {dinary-1.2.3 → dinary-1.3.0}/.deploy.example/llm_providers.toml +0 -0
  82. {dinary-1.2.3 → dinary-1.3.0}/.github/workflows/docs.yml +0 -0
  83. {dinary-1.2.3 → dinary-1.3.0}/.github/workflows/pip_publish.yml +0 -0
  84. {dinary-1.2.3 → dinary-1.3.0}/.gitignore +0 -0
  85. {dinary-1.2.3 → dinary-1.3.0}/.pre-commit-config.yaml +0 -0
  86. {dinary-1.2.3 → dinary-1.3.0}/AGENTS.md +0 -0
  87. {dinary-1.2.3 → dinary-1.3.0}/Dockerfile +0 -0
  88. {dinary-1.2.3 → dinary-1.3.0}/LICENSE +0 -0
  89. {dinary-1.2.3 → dinary-1.3.0}/README.md +0 -0
  90. {dinary-1.2.3 → dinary-1.3.0}/docker-compose.yml +0 -0
  91. {dinary-1.2.3 → dinary-1.3.0}/docs/src/common/images/about.jpg +0 -0
  92. {dinary-1.2.3 → dinary-1.3.0}/docs/src/common/reference.md +0 -0
  93. {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/cloudflare-setup.md +0 -0
  94. {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/deploy-oracle.md +0 -0
  95. {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/deploy-selfhost.md +0 -0
  96. {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/development.md +0 -0
  97. {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/google-sheets-setup.md +0 -0
  98. {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/installation.md +0 -0
  99. {dinary-1.2.3 → dinary-1.3.0}/docs/src/en/pwa-install.md +0 -0
  100. {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/cloudflare-setup.md +0 -0
  101. {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/deploy-oracle.md +0 -0
  102. {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/deploy-selfhost.md +0 -0
  103. {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/development.md +0 -0
  104. {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/google-sheets-setup.md +0 -0
  105. {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/installation.md +0 -0
  106. {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/pwa-install.md +0 -0
  107. {dinary-1.2.3 → dinary-1.3.0}/docs/src/ru/taxonomy.md +0 -0
  108. {dinary-1.2.3 → dinary-1.3.0}/invoke.yml +0 -0
  109. {dinary-1.2.3 → dinary-1.3.0}/pytest.ini +0 -0
  110. {dinary-1.2.3 → dinary-1.3.0}/scripts/verup.sh +0 -0
  111. {dinary-1.2.3 → dinary-1.3.0}/specs/README.md +0 -0
  112. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/catalog-api.md +0 -0
  113. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/classification-pipeline.md +0 -0
  114. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/currencies.md +0 -0
  115. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/frontend-cache.md +0 -0
  116. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/income-import.md +0 -0
  117. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/llm-providers.md +0 -0
  118. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/pwa-offline.md +0 -0
  119. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/receipt-fetching.md +0 -0
  120. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/sheets.md +0 -0
  121. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/sql-tool.md +0 -0
  122. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/stores.md +0 -0
  123. {dinary-1.2.3 → dinary-1.3.0}/specs/reference/timestamps.md +0 -0
  124. {dinary-1.2.3 → dinary-1.3.0}/specs/ui/README.md +0 -0
  125. {dinary-1.2.3 → dinary-1.3.0}/specs/ui/future-screens-guide.md +0 -0
  126. {dinary-1.2.3 → dinary-1.3.0}/specs/ui/patterns.md +0 -0
  127. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/README.md +0 -0
  128. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/__init__.py +0 -0
  129. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/exchange_rates.py +0 -0
  130. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/llm_storage.py +0 -0
  131. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/llmbroker.py +0 -0
  132. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/nbp.py +0 -0
  133. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/nbs.py +0 -0
  134. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/rate_helpers.py +0 -0
  135. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/serbian_receipt_parser.py +0 -0
  136. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/adapters/sheets_client.py +0 -0
  137. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/catalog.py +0 -0
  138. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog.py +0 -0
  139. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer.py +0 -0
  140. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_categories.py +0 -0
  141. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_errors.py +0 -0
  142. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_events.py +0 -0
  143. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/catalog_writer_groups.py +0 -0
  144. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/expense_corrections.py +0 -0
  145. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/expenses.py +0 -0
  146. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/income.py +0 -0
  147. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/llm.py +0 -0
  148. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/qr_parser.py +0 -0
  149. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/controllers/rules.py +0 -0
  150. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/currencies.py +0 -0
  151. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/expense_corrections.py +0 -0
  152. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/expenses.py +0 -0
  153. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/income.py +0 -0
  154. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/llm.py +0 -0
  155. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/qr.py +0 -0
  156. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/receipts.py +0 -0
  157. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/api/rules.py +0 -0
  158. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/classification/item_normalizer.py +0 -0
  159. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/classification/persist.py +0 -0
  160. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/classification/receipt_classifier.py +0 -0
  161. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/classification/store_resolver.py +0 -0
  162. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/classification/task.py +0 -0
  163. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/rate_prefetch/task.py +0 -0
  164. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/sheet_logging/income_sheet_logging.py +0 -0
  165. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/sheet_logging/logging_jobs.py +0 -0
  166. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/sheet_logging/sheet_logging.py +0 -0
  167. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/sheet_logging/sheets_write.py +0 -0
  168. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/background/sheet_logging/task.py +0 -0
  169. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/config.py +0 -0
  170. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/catalog.py +0 -0
  171. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/classification_rules.py +0 -0
  172. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/currencies.py +0 -0
  173. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/db_migrations.py +0 -0
  174. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/expenses.py +0 -0
  175. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/income.py +0 -0
  176. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0001_initial_schema.rollback.sql +0 -0
  177. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0001_initial_schema.sql +0 -0
  178. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0002_exchange_rates_source_target.rollback.sql +0 -0
  179. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0002_exchange_rates_source_target.sql +0 -0
  180. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0003_app_currencies.rollback.sql +0 -0
  181. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0003_app_currencies.sql +0 -0
  182. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0004_receipt_pipeline.rollback.sql +0 -0
  183. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0004_receipt_pipeline.sql +0 -0
  184. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0005_income_logging.rollback.sql +0 -0
  185. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/0005_income_logging.sql +0 -0
  186. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/migrations/README.md +0 -0
  187. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/receipts.py +0 -0
  188. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/__init__.py +0 -0
  189. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/get_category_by_name.sql +0 -0
  190. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/get_existing_expense.sql +0 -0
  191. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/get_month_expenses.sql +0 -0
  192. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/insert_expense.sql +0 -0
  193. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/insert_income.sql +0 -0
  194. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/list_categories.sql +0 -0
  195. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/list_incomes.sql +0 -0
  196. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/logging_projection.sql +0 -0
  197. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/resolve_mapping_for_year.sql +0 -0
  198. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql/seed_load_categories.sql +0 -0
  199. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/sql_loader.py +0 -0
  200. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/db/storage.py +0 -0
  201. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/sheets/sheet_mapping.py +0 -0
  202. {dinary-1.2.3 → dinary-1.3.0}/src/dinary/sheets/sheets.py +0 -0
  203. {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backup_retention.py +0 -0
  204. {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backup_snapshots.py +0 -0
  205. {dinary-1.2.3 → dinary-1.3.0}/tasks/backups/backups_status.py +0 -0
  206. {dinary-1.2.3 → dinary-1.3.0}/tasks/db.py +0 -0
  207. {dinary-1.2.3 → dinary-1.3.0}/tasks/devtools/build_docs.py +0 -0
  208. {dinary-1.2.3 → dinary-1.3.0}/tasks/devtools/dev.py +0 -0
  209. {dinary-1.2.3 → dinary-1.3.0}/tasks/devtools/env.py +0 -0
  210. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/README.md +0 -0
  211. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/expense_import.py +0 -0
  212. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/import_tasks.py +0 -0
  213. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/income_extract.py +0 -0
  214. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/income_import.py +0 -0
  215. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/report_2d_3d.py +0 -0
  216. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/seed.py +0 -0
  217. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/seed_config.py +0 -0
  218. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/seed_derivation.py +0 -0
  219. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/verify_equivalence.py +0 -0
  220. {dinary-1.2.3 → dinary-1.3.0}/tasks/imports/verify_income.py +0 -0
  221. {dinary-1.2.3 → dinary-1.3.0}/tasks/receipt.py +0 -0
  222. {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/expenses.py +0 -0
  223. {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/income.py +0 -0
  224. {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/report_helpers.py +0 -0
  225. {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/report_tasks.py +0 -0
  226. {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/verify_budget.py +0 -0
  227. {dinary-1.2.3 → dinary-1.3.0}/tasks/reports/verify_income.py +0 -0
  228. {dinary-1.2.3 → dinary-1.3.0}/tasks/server.py +0 -0
  229. {dinary-1.2.3 → dinary-1.3.0}/tasks/sql.py +0 -0
  230. {dinary-1.2.3 → dinary-1.3.0}/tasks/ssh_utils.py +0 -0
  231. {dinary-1.2.3 → dinary-1.3.0}/tests/api/_admin_catalog_helpers.py +0 -0
  232. {dinary-1.2.3 → dinary-1.3.0}/tests/api/_api_helpers.py +0 -0
  233. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_admin_catalog_add.py +0 -0
  234. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_admin_catalog_delete.py +0 -0
  235. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_admin_catalog_meta.py +0 -0
  236. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_admin_catalog_patch.py +0 -0
  237. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_admin_llm.py +0 -0
  238. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api.py +0 -0
  239. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_catalog.py +0 -0
  240. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_concurrency.py +0 -0
  241. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_conflict.py +0 -0
  242. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_currencies.py +0 -0
  243. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_delete_expense.py +0 -0
  244. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_delete_receipt.py +0 -0
  245. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_expenses_recent_patch.py +0 -0
  246. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_get_expenses.py +0 -0
  247. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_income.py +0 -0
  248. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_post_expense.py +0 -0
  249. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_receipts.py +0 -0
  250. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_rules_approve.py +0 -0
  251. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_api_validation.py +0 -0
  252. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_receipt_pipeline_e2e.py +0 -0
  253. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_receipt_review.py +0 -0
  254. {dinary-1.2.3 → dinary-1.3.0}/tests/api/test_review_page_ux.py +0 -0
  255. {dinary-1.2.3 → dinary-1.3.0}/tests/conftest.py +0 -0
  256. {dinary-1.2.3 → dinary-1.3.0}/tests/currency/_currency_rates_helpers.py +0 -0
  257. {dinary-1.2.3 → dinary-1.3.0}/tests/currency/test_currency_rates_misc.py +0 -0
  258. {dinary-1.2.3 → dinary-1.3.0}/tests/currency/test_currency_rates_nbp.py +0 -0
  259. {dinary-1.2.3 → dinary-1.3.0}/tests/currency/test_currency_rates_resolve.py +0 -0
  260. {dinary-1.2.3 → dinary-1.3.0}/tests/currency/test_rate_helpers.py +0 -0
  261. {dinary-1.2.3 → dinary-1.3.0}/tests/currency/test_rate_prefetch_task.py +0 -0
  262. {dinary-1.2.3 → dinary-1.3.0}/tests/imports/test_expense_import.py +0 -0
  263. {dinary-1.2.3 → dinary-1.3.0}/tests/imports/test_seed_config.py +0 -0
  264. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/_catalog_writer_helpers.py +0 -0
  265. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/_ledger_repo_helpers.py +0 -0
  266. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_catalog_writer_invariants.py +0 -0
  267. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_catalog_writer_patch.py +0 -0
  268. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_income_db.py +0 -0
  269. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_catalog.py +0 -0
  270. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_expenses_insert.py +0 -0
  271. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_expenses_lookup.py +0 -0
  272. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_expenses_race.py +0 -0
  273. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_jobs.py +0 -0
  274. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_ledger_repo_logging_projection.py +0 -0
  275. {dinary-1.2.3 → dinary-1.3.0}/tests/ledger/test_migrations.py +0 -0
  276. {dinary-1.2.3 → dinary-1.3.0}/tests/reports/_report_2d_3d_helpers.py +0 -0
  277. {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_report_2d_3d.py +0 -0
  278. {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_report_2d_3d_aggregate.py +0 -0
  279. {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_report_2d_3d_render.py +0 -0
  280. {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_report_2d_3d_resolve.py +0 -0
  281. {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_reports_expenses.py +0 -0
  282. {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_reports_income.py +0 -0
  283. {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_reports_verify_budget.py +0 -0
  284. {dinary-1.2.3 → dinary-1.3.0}/tests/reports/test_reports_verify_income.py +0 -0
  285. {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_classification_rules.py +0 -0
  286. {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_item_normalizer.py +0 -0
  287. {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_llm_storage.py +0 -0
  288. {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_llmbroker.py +0 -0
  289. {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_qr_parser.py +0 -0
  290. {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_receipt_classification.py +0 -0
  291. {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_receipt_classifier.py +0 -0
  292. {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_receipt_parser.py +0 -0
  293. {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_sql_loader.py +0 -0
  294. {dinary-1.2.3 → dinary-1.3.0}/tests/services/test_store_resolver.py +0 -0
  295. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/_sheet_logging_helpers.py +0 -0
  296. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/_sheet_mapping_helpers.py +0 -0
  297. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/_sheets_helpers.py +0 -0
  298. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_income_drain.py +0 -0
  299. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_logging.py +0 -0
  300. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_logging_derive.py +0 -0
  301. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_logging_drain.py +0 -0
  302. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_logging_drain_one.py +0 -0
  303. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_mapping_parse.py +0 -0
  304. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_mapping_reload.py +0 -0
  305. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheet_mapping_resolve.py +0 -0
  306. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheets_read.py +0 -0
  307. {dinary-1.2.3 → dinary-1.3.0}/tests/sheets/test_sheets_rows.py +0 -0
  308. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_receipt_drain.py +0 -0
  309. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_receipt_pipeline.py +0 -0
  310. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_reclassify_receipts.py +0 -0
  311. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_backups_retention.py +0 -0
  312. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_backups_status.py +0 -0
  313. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_backups_yandex_setup.py +0 -0
  314. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_db.py +0 -0
  315. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_deploy.py +0 -0
  316. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_dev.py +0 -0
  317. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_imports.py +0 -0
  318. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_reports.py +0 -0
  319. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_restore_utils.py +0 -0
  320. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_server.py +0 -0
  321. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_setup_replica.py +0 -0
  322. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_ssh_utils.py +0 -0
  323. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tasks_ssh_utils_scripts.py +0 -0
  324. {dinary-1.2.3 → dinary-1.3.0}/tests/tasks/test_tools_sql.py +0 -0
  325. {dinary-1.2.3 → dinary-1.3.0}/tests/test_config.py +0 -0
  326. {dinary-1.2.3 → dinary-1.3.0}/tests/test_main.py +0 -0
  327. {dinary-1.2.3 → dinary-1.3.0}/tests/test_webapp_api_contract.py +0 -0
  328. {dinary-1.2.3 → dinary-1.3.0}/webapp/index.html +0 -0
  329. {dinary-1.2.3 → dinary-1.3.0}/webapp/package-lock.json +0 -0
  330. {dinary-1.2.3 → dinary-1.3.0}/webapp/package.json +0 -0
  331. {dinary-1.2.3 → dinary-1.3.0}/webapp/public/apple-touch-icon-precomposed.png +0 -0
  332. {dinary-1.2.3 → dinary-1.3.0}/webapp/public/apple-touch-icon.png +0 -0
  333. {dinary-1.2.3 → dinary-1.3.0}/webapp/public/favicon.ico +0 -0
  334. {dinary-1.2.3 → dinary-1.3.0}/webapp/public/icons/icon-180.png +0 -0
  335. {dinary-1.2.3 → dinary-1.3.0}/webapp/public/icons/icon-192.png +0 -0
  336. {dinary-1.2.3 → dinary-1.3.0}/webapp/public/icons/icon-512.png +0 -0
  337. {dinary-1.2.3 → dinary-1.3.0}/webapp/public/manifest.json +0 -0
  338. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/_request.js +0 -0
  339. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/adminLlm.js +0 -0
  340. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/catalog.js +0 -0
  341. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/currencies.js +0 -0
  342. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/expenseCorrections.js +0 -0
  343. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/expenses.js +0 -0
  344. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/income.js +0 -0
  345. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/receipts.js +0 -0
  346. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/api/review.js +0 -0
  347. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/BaseModal.vue +0 -0
  348. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/BaseSheet.vue +0 -0
  349. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CatalogSelectField.vue +0 -0
  350. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CategoryQuickPicks.vue +0 -0
  351. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CategorySheet.vue +0 -0
  352. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ConfirmDeleteSheet.vue +0 -0
  353. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CorrectionSheet.vue +0 -0
  354. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CurrencyAmountRow.vue +0 -0
  355. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/CurrencyPicker.vue +0 -0
  356. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/HealthSummaryCard.vue +0 -0
  357. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/IconBtn.vue +0 -0
  358. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/IncomeEditSheet.vue +0 -0
  359. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/IncomeForm.vue +0 -0
  360. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/IncomeRow.vue +0 -0
  361. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/InlineCreateEvent.vue +0 -0
  362. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/InlineCreateRow.vue +0 -0
  363. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/KeyboardSaveBar.vue +0 -0
  364. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ManageList.vue +0 -0
  365. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ProviderCard.vue +0 -0
  366. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ProviderSheet.vue +0 -0
  367. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/QrScanner.vue +0 -0
  368. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/QueueModal.vue +0 -0
  369. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ReceiptCascadeCard.vue +0 -0
  370. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/RuleRow.vue +0 -0
  371. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/ScopeSelector.vue +0 -0
  372. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/StatusDot.vue +0 -0
  373. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/components/TagPicker.vue +0 -0
  374. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/addResult.js +0 -0
  375. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/catalogManage.js +0 -0
  376. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/flushQueue.js +0 -0
  377. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/flushReceiptQueue.js +0 -0
  378. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/receipt.js +0 -0
  379. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/useExpenseDeleteFlow.js +0 -0
  380. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/useKeyboardVisible.js +0 -0
  381. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/useOnline.js +0 -0
  382. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/useStaleCache.js +0 -0
  383. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/useSwipeRow.js +0 -0
  384. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/composables/zbar.js +0 -0
  385. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/legacy-pwa-cleanup.js +0 -0
  386. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/main.js +0 -0
  387. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/modals/EditModal.vue +0 -0
  388. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/currency.js +0 -0
  389. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/frequentCategories.js +0 -0
  390. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/income.js +0 -0
  391. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/llm.js +0 -0
  392. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/queue.js +0 -0
  393. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/receiptQueue.js +0 -0
  394. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/review.js +0 -0
  395. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/stores/toast.js +0 -0
  396. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/views/AddView.vue +0 -0
  397. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/views/IncomeView.vue +0 -0
  398. {dinary-1.2.3 → dinary-1.3.0}/webapp/src/views/LLMView.vue +0 -0
  399. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/BaseSheet.test.js +0 -0
  400. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/CategoryQuickPicks.test.js +0 -0
  401. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/CategorySheet.test.js +0 -0
  402. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/CurrencyAmountRow.test.js +0 -0
  403. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/ExpenseRow.test.js +0 -0
  404. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/ReceiptCascadeCard.test.js +0 -0
  405. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/ScopeSelector.test.js +0 -0
  406. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-adminLlm.test.js +0 -0
  407. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-catalog.test.js +0 -0
  408. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-currencies.test.js +0 -0
  409. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-expenses.test.js +0 -0
  410. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-income.test.js +0 -0
  411. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-receipts.test.js +0 -0
  412. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-request.test.js +0 -0
  413. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/api-review.test.js +0 -0
  414. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-catalog-select-field.test.js +0 -0
  415. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-correction-sheet.test.js +0 -0
  416. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-currency-picker.test.js +0 -0
  417. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-edit-modal.test.js +0 -0
  418. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-expense-edit-sheet.test.js +0 -0
  419. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-health-summary-card.test.js +0 -0
  420. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-inline-create-event.test.js +0 -0
  421. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-inline-create-row.test.js +0 -0
  422. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-manage-list.test.js +0 -0
  423. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-provider-card.test.js +0 -0
  424. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-provider-sheet.test.js +0 -0
  425. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-queue-modal.test.js +0 -0
  426. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-review-view.test.js +0 -0
  427. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-rule-row.test.js +0 -0
  428. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-status-dot.test.js +0 -0
  429. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/component-tag-picker.test.js +0 -0
  430. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-add-result.test.js +0 -0
  431. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-catalog-manage.test.js +0 -0
  432. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-flush-queue.test.js +0 -0
  433. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-flush-receipt-queue.test.js +0 -0
  434. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-keyboard-visible.test.js +0 -0
  435. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-receipt.test.js +0 -0
  436. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-stale-cache.test.js +0 -0
  437. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/composable-use-online.test.js +0 -0
  438. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/frequentCategories.test.js +0 -0
  439. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/legacy-pwa-cleanup.test.js +0 -0
  440. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/setup.js +0 -0
  441. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-catalog.test.js +0 -0
  442. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-currency.test.js +0 -0
  443. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-income.test.js +0 -0
  444. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-llm.test.js +0 -0
  445. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-queue.test.js +0 -0
  446. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-receipt-queue-durability.test.js +0 -0
  447. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-receipt-queue.test.js +0 -0
  448. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-review.test.js +0 -0
  449. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/store-toast.test.js +0 -0
  450. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/useExpenseDeleteFlow.test.js +0 -0
  451. {dinary-1.2.3 → dinary-1.3.0}/webapp/tests/useSwipeRow.test.js +0 -0
  452. {dinary-1.2.3 → dinary-1.3.0}/webapp/vite.config.js +0 -0
  453. {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 report
112
- run: "${{ env.PYTEST_CMD }} --alluredir=./allure-results"
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
@@ -30,5 +30,5 @@ jobs:
30
30
  uses: andgineer/uv-venv@v3
31
31
 
32
32
  - name: Install dependencies
33
- run: uv sync --frozen
33
+ run: uv sync --frozen --group analytics
34
34
  - run: pre-commit run --verbose --all-files
@@ -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.2.3
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
@@ -49,6 +49,7 @@ extra:
49
49
 
50
50
  nav:
51
51
  - index.md
52
+ - analytics.md
52
53
  - installation.md
53
54
  - google-sheets-setup.md
54
55
  - pwa-install.md
@@ -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-cloud-backup` reads from `yandex:` on whichever machine it
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-cloud-backup` calls this automatically when `yandex:` is
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-cloud-backup --list-only # show inventory
356
- inv restore-cloud-backup # restore latest
357
- inv restore-cloud-backup --snapshot 2026-03-15 # specific date
358
- inv restore-cloud-backup --yes # skip confirm
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-cloud-backup` on the wrong terminal cannot silently
372
+ `inv restore-yadisk` on the wrong terminal cannot silently
373
373
  overwrite prod.
374
374
 
375
- `restore-cloud-backup` is **local-only** — it writes to
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-cloud-backup` prompts automatically** (same WebDAV +
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-cloud-backup --snapshot 2026-03-15
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-cloud-backup --snapshot 2026-03-15 --yes
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-cloud-backup # restore DB from Yandex.Disk
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-cloud-backup` читает из `yandex:` на той машине, где
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-cloud-backup` вызывает её автоматически при отсутствии
362
+ `restore-yadisk` вызывает её автоматически при отсутствии
363
363
  `yandex:`, так что ручной запуск заранее необязателен.
364
364
 
365
365
  ## Восстановление на конкретную дату из Яндекс.Диска
366
366
 
367
367
  ```bash
368
- inv restore-cloud-backup --list-only # список снапшотов
369
- inv restore-cloud-backup # восстановить самый свежий
370
- inv restore-cloud-backup --snapshot 2026-03-15 # конкретную дату
371
- inv restore-cloud-backup --yes # без подтверждения
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-cloud-backup` в случайном терминале нельзя было
385
+ `inv restore-yadisk` в случайном терминале нельзя было
386
386
  молча затереть прод.
387
387
 
388
- `restore-cloud-backup` — **local-only**: пишет в `./data/dinary.db`
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-cloud-backup` сам запросит авторизацию** (та же схема
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-cloud-backup --snapshot 2026-03-15
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-cloud-backup --snapshot 2026-03-15 --yes
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-cloud-backup # восстановить БД с Яндекс.Диска
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.