dinary 1.2.2__tar.gz → 1.2.4__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 (442) hide show
  1. {dinary-1.2.2 → dinary-1.2.4}/.github/workflows/ci.yml +23 -3
  2. {dinary-1.2.2 → dinary-1.2.4}/.github/workflows/static.yml +1 -1
  3. {dinary-1.2.2 → dinary-1.2.4}/PKG-INFO +1 -1
  4. {dinary-1.2.2 → dinary-1.2.4}/activate.sh +1 -1
  5. {dinary-1.2.2 → dinary-1.2.4}/docs/mkdocs.yml +1 -0
  6. dinary-1.2.4/docs/src/en/analytics.md +33 -0
  7. {dinary-1.2.2 → dinary-1.2.4}/docs/src/en/index.md +1 -0
  8. {dinary-1.2.2 → dinary-1.2.4}/docs/src/en/operations.md +12 -12
  9. dinary-1.2.4/docs/src/ru/analytics.md +33 -0
  10. {dinary-1.2.2 → dinary-1.2.4}/docs/src/ru/index.md +1 -0
  11. {dinary-1.2.2 → dinary-1.2.4}/docs/src/ru/operations.md +12 -12
  12. {dinary-1.2.2 → dinary-1.2.4}/pyproject.toml +12 -0
  13. dinary-1.2.4/specs/plans/analytics-ai.md +70 -0
  14. dinary-1.2.4/specs/plans/analytics-pwa.md +78 -0
  15. dinary-1.2.4/specs/reference/analytics-ai.md +179 -0
  16. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/architecture.md +15 -0
  17. {dinary-1.2.2 → dinary-1.2.4}/specs/ui/components.md +1 -1
  18. {dinary-1.2.2 → dinary-1.2.4}/specs/ui/screens.md +1 -0
  19. dinary-1.2.4/src/dinary/__about__.py +1 -0
  20. dinary-1.2.4/src/dinary_analytics/__init__.py +0 -0
  21. dinary-1.2.4/src/dinary_analytics/backup.py +99 -0
  22. dinary-1.2.4/src/dinary_analytics/charts.py +225 -0
  23. dinary-1.2.4/src/dinary_analytics/connection.py +96 -0
  24. dinary-1.2.4/src/dinary_analytics/mcp_server.py +58 -0
  25. dinary-1.2.4/src/dinary_analytics/notebooks/dashboard.py +509 -0
  26. dinary-1.2.4/src/dinary_analytics/settings.py +41 -0
  27. {dinary-1.2.2 → dinary-1.2.4}/tasks/__init__.py +14 -2
  28. dinary-1.2.4/tasks/analytics.py +77 -0
  29. dinary-1.2.4/tasks/backups/analytics_backup.py +162 -0
  30. {dinary-1.2.2 → dinary-1.2.4}/tasks/backups/backups_replica.py +1 -1
  31. {dinary-1.2.2 → dinary-1.2.4}/tasks/backups/backups_restore.py +1 -1
  32. {dinary-1.2.2 → dinary-1.2.4}/tasks/backups/backups_yandex.py +2 -2
  33. {dinary-1.2.2 → dinary-1.2.4}/tasks/backups/restore_utils.py +1 -1
  34. {dinary-1.2.2 → dinary-1.2.4}/tasks/deploy.py +3 -3
  35. {dinary-1.2.2 → dinary-1.2.4}/tasks/devtools/constants.py +1 -1
  36. {dinary-1.2.2 → dinary-1.2.4}/tasks/healthcheck.py +42 -1
  37. {dinary-1.2.2 → dinary-1.2.4}/tasks/imports/import_tasks.py +8 -8
  38. dinary-1.2.2/tasks/imports/income_original_export.py → dinary-1.2.4/tasks/imports/income_extract.py +56 -42
  39. {dinary-1.2.2 → dinary-1.2.4}/tasks/imports/income_import.py +32 -0
  40. {dinary-1.2.2 → dinary-1.2.4}/tasks/setup.py +5 -1
  41. dinary-1.2.4/tests/analytics/__init__.py +0 -0
  42. dinary-1.2.4/tests/analytics/test_backup.py +78 -0
  43. dinary-1.2.4/tests/analytics/test_connection.py +106 -0
  44. dinary-1.2.4/tests/analytics/test_dashboard.py +344 -0
  45. dinary-1.2.4/tests/analytics/test_mcp_server.py +75 -0
  46. dinary-1.2.4/tests/analytics/test_settings.py +68 -0
  47. dinary-1.2.4/tests/imports/test_income_extract.py +339 -0
  48. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_backups_restore.py +3 -3
  49. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_server_receipt.py +46 -1
  50. {dinary-1.2.2 → dinary-1.2.4}/uv.lock +599 -0
  51. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/ExpenseEditSheet.vue +32 -14
  52. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/ExpenseForm.vue +5 -13
  53. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/ProviderCard.vue +16 -3
  54. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/ProviderSheet.vue +25 -0
  55. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/stores/catalog.js +24 -0
  56. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/ExpenseEditSheet.test.js +157 -0
  57. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-expense-form.test.js +55 -0
  58. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-provider-card.test.js +17 -1
  59. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-provider-sheet.test.js +20 -0
  60. dinary-1.2.2/specs/plans/analytics.md +0 -348
  61. dinary-1.2.2/src/dinary/__about__.py +0 -1
  62. {dinary-1.2.2 → dinary-1.2.4}/.claudeignore +0 -0
  63. {dinary-1.2.2 → dinary-1.2.4}/.coveragerc +0 -0
  64. {dinary-1.2.2 → dinary-1.2.4}/.deploy.example/.env +0 -0
  65. {dinary-1.2.2 → dinary-1.2.4}/.deploy.example/README.md +0 -0
  66. {dinary-1.2.2 → dinary-1.2.4}/.deploy.example/import_sources.json +0 -0
  67. {dinary-1.2.2 → dinary-1.2.4}/.deploy.example/llm_providers.toml +0 -0
  68. {dinary-1.2.2 → dinary-1.2.4}/.github/workflows/docs.yml +0 -0
  69. {dinary-1.2.2 → dinary-1.2.4}/.github/workflows/pip_publish.yml +0 -0
  70. {dinary-1.2.2 → dinary-1.2.4}/.gitignore +0 -0
  71. {dinary-1.2.2 → dinary-1.2.4}/.pre-commit-config.yaml +0 -0
  72. {dinary-1.2.2 → dinary-1.2.4}/AGENTS.md +0 -0
  73. {dinary-1.2.2 → dinary-1.2.4}/CLAUDE.md +0 -0
  74. {dinary-1.2.2 → dinary-1.2.4}/Dockerfile +0 -0
  75. {dinary-1.2.2 → dinary-1.2.4}/LICENSE +0 -0
  76. {dinary-1.2.2 → dinary-1.2.4}/README.md +0 -0
  77. {dinary-1.2.2 → dinary-1.2.4}/docker-compose.yml +0 -0
  78. {dinary-1.2.2 → dinary-1.2.4}/docs/src/common/images/about.jpg +0 -0
  79. {dinary-1.2.2 → dinary-1.2.4}/docs/src/common/reference.md +0 -0
  80. {dinary-1.2.2 → dinary-1.2.4}/docs/src/en/cloudflare-setup.md +0 -0
  81. {dinary-1.2.2 → dinary-1.2.4}/docs/src/en/deploy-oracle.md +0 -0
  82. {dinary-1.2.2 → dinary-1.2.4}/docs/src/en/deploy-selfhost.md +0 -0
  83. {dinary-1.2.2 → dinary-1.2.4}/docs/src/en/development.md +0 -0
  84. {dinary-1.2.2 → dinary-1.2.4}/docs/src/en/google-sheets-setup.md +0 -0
  85. {dinary-1.2.2 → dinary-1.2.4}/docs/src/en/installation.md +0 -0
  86. {dinary-1.2.2 → dinary-1.2.4}/docs/src/en/pwa-install.md +0 -0
  87. {dinary-1.2.2 → dinary-1.2.4}/docs/src/ru/cloudflare-setup.md +0 -0
  88. {dinary-1.2.2 → dinary-1.2.4}/docs/src/ru/deploy-oracle.md +0 -0
  89. {dinary-1.2.2 → dinary-1.2.4}/docs/src/ru/deploy-selfhost.md +0 -0
  90. {dinary-1.2.2 → dinary-1.2.4}/docs/src/ru/development.md +0 -0
  91. {dinary-1.2.2 → dinary-1.2.4}/docs/src/ru/google-sheets-setup.md +0 -0
  92. {dinary-1.2.2 → dinary-1.2.4}/docs/src/ru/installation.md +0 -0
  93. {dinary-1.2.2 → dinary-1.2.4}/docs/src/ru/pwa-install.md +0 -0
  94. {dinary-1.2.2 → dinary-1.2.4}/docs/src/ru/taxonomy.md +0 -0
  95. {dinary-1.2.2 → dinary-1.2.4}/invoke.yml +0 -0
  96. {dinary-1.2.2 → dinary-1.2.4}/pytest.ini +0 -0
  97. {dinary-1.2.2 → dinary-1.2.4}/scripts/verup.sh +0 -0
  98. {dinary-1.2.2 → dinary-1.2.4}/specs/README.md +0 -0
  99. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/catalog-api.md +0 -0
  100. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/classification-pipeline.md +0 -0
  101. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/currencies.md +0 -0
  102. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/frontend-cache.md +0 -0
  103. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/income-import.md +0 -0
  104. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/llm-providers.md +0 -0
  105. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/pwa-offline.md +0 -0
  106. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/receipt-fetching.md +0 -0
  107. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/sheets.md +0 -0
  108. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/sql-tool.md +0 -0
  109. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/stores.md +0 -0
  110. {dinary-1.2.2 → dinary-1.2.4}/specs/reference/timestamps.md +0 -0
  111. {dinary-1.2.2 → dinary-1.2.4}/specs/ui/README.md +0 -0
  112. {dinary-1.2.2 → dinary-1.2.4}/specs/ui/design-language.md +0 -0
  113. {dinary-1.2.2 → dinary-1.2.4}/specs/ui/future-screens-guide.md +0 -0
  114. {dinary-1.2.2 → dinary-1.2.4}/specs/ui/patterns.md +0 -0
  115. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/README.md +0 -0
  116. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/__init__.py +0 -0
  117. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/adapters/exchange_rates.py +0 -0
  118. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/adapters/llm_storage.py +0 -0
  119. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/adapters/llmbroker.py +0 -0
  120. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/adapters/nbp.py +0 -0
  121. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/adapters/nbs.py +0 -0
  122. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/adapters/rate_helpers.py +0 -0
  123. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/adapters/serbian_receipt_parser.py +0 -0
  124. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/adapters/sheets_client.py +0 -0
  125. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/catalog.py +0 -0
  126. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/catalog.py +0 -0
  127. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/catalog_writer.py +0 -0
  128. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/catalog_writer_categories.py +0 -0
  129. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/catalog_writer_errors.py +0 -0
  130. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/catalog_writer_events.py +0 -0
  131. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/catalog_writer_groups.py +0 -0
  132. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/expense_corrections.py +0 -0
  133. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/expenses.py +0 -0
  134. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/income.py +0 -0
  135. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/llm.py +0 -0
  136. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/qr_parser.py +0 -0
  137. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/controllers/rules.py +0 -0
  138. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/currencies.py +0 -0
  139. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/expense_corrections.py +0 -0
  140. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/expenses.py +0 -0
  141. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/income.py +0 -0
  142. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/llm.py +0 -0
  143. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/qr.py +0 -0
  144. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/receipts.py +0 -0
  145. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/api/rules.py +0 -0
  146. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/classification/item_normalizer.py +0 -0
  147. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/classification/persist.py +0 -0
  148. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/classification/receipt_classifier.py +0 -0
  149. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/classification/store_resolver.py +0 -0
  150. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/classification/task.py +0 -0
  151. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/rate_prefetch/task.py +0 -0
  152. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/sheet_logging/income_sheet_logging.py +0 -0
  153. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/sheet_logging/logging_jobs.py +0 -0
  154. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/sheet_logging/sheet_logging.py +0 -0
  155. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/sheet_logging/sheets_write.py +0 -0
  156. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/background/sheet_logging/task.py +0 -0
  157. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/config.py +0 -0
  158. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/catalog.py +0 -0
  159. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/classification_rules.py +0 -0
  160. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/currencies.py +0 -0
  161. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/db_migrations.py +0 -0
  162. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/expenses.py +0 -0
  163. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/income.py +0 -0
  164. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/0001_initial_schema.rollback.sql +0 -0
  165. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/0001_initial_schema.sql +0 -0
  166. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/0002_exchange_rates_source_target.rollback.sql +0 -0
  167. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/0002_exchange_rates_source_target.sql +0 -0
  168. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/0003_app_currencies.rollback.sql +0 -0
  169. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/0003_app_currencies.sql +0 -0
  170. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/0004_receipt_pipeline.rollback.sql +0 -0
  171. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/0004_receipt_pipeline.sql +0 -0
  172. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/0005_income_logging.rollback.sql +0 -0
  173. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/0005_income_logging.sql +0 -0
  174. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/migrations/README.md +0 -0
  175. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/receipts.py +0 -0
  176. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/__init__.py +0 -0
  177. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/get_category_by_name.sql +0 -0
  178. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/get_existing_expense.sql +0 -0
  179. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/get_month_expenses.sql +0 -0
  180. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/insert_expense.sql +0 -0
  181. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/insert_income.sql +0 -0
  182. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/list_categories.sql +0 -0
  183. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/list_incomes.sql +0 -0
  184. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/logging_projection.sql +0 -0
  185. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/resolve_mapping_for_year.sql +0 -0
  186. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql/seed_load_categories.sql +0 -0
  187. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/sql_loader.py +0 -0
  188. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/db/storage.py +0 -0
  189. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/main.py +0 -0
  190. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/sheets/sheet_mapping.py +0 -0
  191. {dinary-1.2.2 → dinary-1.2.4}/src/dinary/sheets/sheets.py +0 -0
  192. {dinary-1.2.2 → dinary-1.2.4}/tasks/backups/backup_retention.py +0 -0
  193. {dinary-1.2.2 → dinary-1.2.4}/tasks/backups/backup_snapshots.py +0 -0
  194. {dinary-1.2.2 → dinary-1.2.4}/tasks/backups/backups_status.py +0 -0
  195. {dinary-1.2.2 → dinary-1.2.4}/tasks/db.py +0 -0
  196. {dinary-1.2.2 → dinary-1.2.4}/tasks/devtools/build_docs.py +0 -0
  197. {dinary-1.2.2 → dinary-1.2.4}/tasks/devtools/dev.py +0 -0
  198. {dinary-1.2.2 → dinary-1.2.4}/tasks/devtools/env.py +0 -0
  199. {dinary-1.2.2 → dinary-1.2.4}/tasks/imports/README.md +0 -0
  200. {dinary-1.2.2 → dinary-1.2.4}/tasks/imports/expense_import.py +0 -0
  201. {dinary-1.2.2 → dinary-1.2.4}/tasks/imports/report_2d_3d.py +0 -0
  202. {dinary-1.2.2 → dinary-1.2.4}/tasks/imports/seed.py +0 -0
  203. {dinary-1.2.2 → dinary-1.2.4}/tasks/imports/seed_config.py +0 -0
  204. {dinary-1.2.2 → dinary-1.2.4}/tasks/imports/seed_derivation.py +0 -0
  205. {dinary-1.2.2 → dinary-1.2.4}/tasks/imports/verify_equivalence.py +0 -0
  206. {dinary-1.2.2 → dinary-1.2.4}/tasks/imports/verify_income.py +0 -0
  207. {dinary-1.2.2 → dinary-1.2.4}/tasks/receipt.py +0 -0
  208. {dinary-1.2.2 → dinary-1.2.4}/tasks/reports/expenses.py +0 -0
  209. {dinary-1.2.2 → dinary-1.2.4}/tasks/reports/income.py +0 -0
  210. {dinary-1.2.2 → dinary-1.2.4}/tasks/reports/report_helpers.py +0 -0
  211. {dinary-1.2.2 → dinary-1.2.4}/tasks/reports/report_tasks.py +0 -0
  212. {dinary-1.2.2 → dinary-1.2.4}/tasks/reports/verify_budget.py +0 -0
  213. {dinary-1.2.2 → dinary-1.2.4}/tasks/reports/verify_income.py +0 -0
  214. {dinary-1.2.2 → dinary-1.2.4}/tasks/server.py +0 -0
  215. {dinary-1.2.2 → dinary-1.2.4}/tasks/sql.py +0 -0
  216. {dinary-1.2.2 → dinary-1.2.4}/tasks/ssh_utils.py +0 -0
  217. {dinary-1.2.2 → dinary-1.2.4}/tests/api/_admin_catalog_helpers.py +0 -0
  218. {dinary-1.2.2 → dinary-1.2.4}/tests/api/_api_helpers.py +0 -0
  219. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_admin_catalog_add.py +0 -0
  220. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_admin_catalog_delete.py +0 -0
  221. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_admin_catalog_meta.py +0 -0
  222. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_admin_catalog_patch.py +0 -0
  223. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_admin_llm.py +0 -0
  224. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api.py +0 -0
  225. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_catalog.py +0 -0
  226. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_concurrency.py +0 -0
  227. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_conflict.py +0 -0
  228. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_currencies.py +0 -0
  229. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_delete_expense.py +0 -0
  230. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_delete_receipt.py +0 -0
  231. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_expenses_recent_patch.py +0 -0
  232. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_get_expenses.py +0 -0
  233. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_income.py +0 -0
  234. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_post_expense.py +0 -0
  235. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_receipts.py +0 -0
  236. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_rules_approve.py +0 -0
  237. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_api_validation.py +0 -0
  238. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_receipt_pipeline_e2e.py +0 -0
  239. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_receipt_review.py +0 -0
  240. {dinary-1.2.2 → dinary-1.2.4}/tests/api/test_review_page_ux.py +0 -0
  241. {dinary-1.2.2 → dinary-1.2.4}/tests/conftest.py +0 -0
  242. {dinary-1.2.2 → dinary-1.2.4}/tests/currency/_currency_rates_helpers.py +0 -0
  243. {dinary-1.2.2 → dinary-1.2.4}/tests/currency/test_currency_rates_misc.py +0 -0
  244. {dinary-1.2.2 → dinary-1.2.4}/tests/currency/test_currency_rates_nbp.py +0 -0
  245. {dinary-1.2.2 → dinary-1.2.4}/tests/currency/test_currency_rates_resolve.py +0 -0
  246. {dinary-1.2.2 → dinary-1.2.4}/tests/currency/test_rate_helpers.py +0 -0
  247. {dinary-1.2.2 → dinary-1.2.4}/tests/currency/test_rate_prefetch_task.py +0 -0
  248. {dinary-1.2.2 → dinary-1.2.4}/tests/imports/test_expense_import.py +0 -0
  249. {dinary-1.2.2 → dinary-1.2.4}/tests/imports/test_seed_config.py +0 -0
  250. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/_catalog_writer_helpers.py +0 -0
  251. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/_ledger_repo_helpers.py +0 -0
  252. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/test_catalog_writer_invariants.py +0 -0
  253. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/test_catalog_writer_patch.py +0 -0
  254. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/test_income_db.py +0 -0
  255. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/test_ledger_repo_catalog.py +0 -0
  256. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/test_ledger_repo_expenses_insert.py +0 -0
  257. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/test_ledger_repo_expenses_lookup.py +0 -0
  258. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/test_ledger_repo_expenses_race.py +0 -0
  259. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/test_ledger_repo_jobs.py +0 -0
  260. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/test_ledger_repo_logging_projection.py +0 -0
  261. {dinary-1.2.2 → dinary-1.2.4}/tests/ledger/test_migrations.py +0 -0
  262. {dinary-1.2.2 → dinary-1.2.4}/tests/reports/_report_2d_3d_helpers.py +0 -0
  263. {dinary-1.2.2 → dinary-1.2.4}/tests/reports/test_report_2d_3d.py +0 -0
  264. {dinary-1.2.2 → dinary-1.2.4}/tests/reports/test_report_2d_3d_aggregate.py +0 -0
  265. {dinary-1.2.2 → dinary-1.2.4}/tests/reports/test_report_2d_3d_render.py +0 -0
  266. {dinary-1.2.2 → dinary-1.2.4}/tests/reports/test_report_2d_3d_resolve.py +0 -0
  267. {dinary-1.2.2 → dinary-1.2.4}/tests/reports/test_reports_expenses.py +0 -0
  268. {dinary-1.2.2 → dinary-1.2.4}/tests/reports/test_reports_income.py +0 -0
  269. {dinary-1.2.2 → dinary-1.2.4}/tests/reports/test_reports_verify_budget.py +0 -0
  270. {dinary-1.2.2 → dinary-1.2.4}/tests/reports/test_reports_verify_income.py +0 -0
  271. {dinary-1.2.2 → dinary-1.2.4}/tests/services/test_classification_rules.py +0 -0
  272. {dinary-1.2.2 → dinary-1.2.4}/tests/services/test_item_normalizer.py +0 -0
  273. {dinary-1.2.2 → dinary-1.2.4}/tests/services/test_llm_storage.py +0 -0
  274. {dinary-1.2.2 → dinary-1.2.4}/tests/services/test_llmbroker.py +0 -0
  275. {dinary-1.2.2 → dinary-1.2.4}/tests/services/test_qr_parser.py +0 -0
  276. {dinary-1.2.2 → dinary-1.2.4}/tests/services/test_receipt_classification.py +0 -0
  277. {dinary-1.2.2 → dinary-1.2.4}/tests/services/test_receipt_classifier.py +0 -0
  278. {dinary-1.2.2 → dinary-1.2.4}/tests/services/test_receipt_parser.py +0 -0
  279. {dinary-1.2.2 → dinary-1.2.4}/tests/services/test_sql_loader.py +0 -0
  280. {dinary-1.2.2 → dinary-1.2.4}/tests/services/test_store_resolver.py +0 -0
  281. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/_sheet_logging_helpers.py +0 -0
  282. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/_sheet_mapping_helpers.py +0 -0
  283. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/_sheets_helpers.py +0 -0
  284. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/test_income_drain.py +0 -0
  285. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/test_sheet_logging.py +0 -0
  286. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/test_sheet_logging_derive.py +0 -0
  287. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/test_sheet_logging_drain.py +0 -0
  288. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/test_sheet_logging_drain_one.py +0 -0
  289. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/test_sheet_mapping_parse.py +0 -0
  290. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/test_sheet_mapping_reload.py +0 -0
  291. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/test_sheet_mapping_resolve.py +0 -0
  292. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/test_sheets_read.py +0 -0
  293. {dinary-1.2.2 → dinary-1.2.4}/tests/sheets/test_sheets_rows.py +0 -0
  294. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_receipt_drain.py +0 -0
  295. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_receipt_pipeline.py +0 -0
  296. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_reclassify_receipts.py +0 -0
  297. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_backups_retention.py +0 -0
  298. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_backups_status.py +0 -0
  299. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_backups_yandex_setup.py +0 -0
  300. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_db.py +0 -0
  301. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_deploy.py +0 -0
  302. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_dev.py +0 -0
  303. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_imports.py +0 -0
  304. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_reports.py +0 -0
  305. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_restore_utils.py +0 -0
  306. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_server.py +0 -0
  307. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_setup_replica.py +0 -0
  308. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_ssh_utils.py +0 -0
  309. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tasks_ssh_utils_scripts.py +0 -0
  310. {dinary-1.2.2 → dinary-1.2.4}/tests/tasks/test_tools_sql.py +0 -0
  311. {dinary-1.2.2 → dinary-1.2.4}/tests/test_config.py +0 -0
  312. {dinary-1.2.2 → dinary-1.2.4}/tests/test_main.py +0 -0
  313. {dinary-1.2.2 → dinary-1.2.4}/tests/test_webapp_api_contract.py +0 -0
  314. {dinary-1.2.2 → dinary-1.2.4}/webapp/index.html +0 -0
  315. {dinary-1.2.2 → dinary-1.2.4}/webapp/package-lock.json +0 -0
  316. {dinary-1.2.2 → dinary-1.2.4}/webapp/package.json +0 -0
  317. {dinary-1.2.2 → dinary-1.2.4}/webapp/public/apple-touch-icon-precomposed.png +0 -0
  318. {dinary-1.2.2 → dinary-1.2.4}/webapp/public/apple-touch-icon.png +0 -0
  319. {dinary-1.2.2 → dinary-1.2.4}/webapp/public/favicon.ico +0 -0
  320. {dinary-1.2.2 → dinary-1.2.4}/webapp/public/icons/icon-180.png +0 -0
  321. {dinary-1.2.2 → dinary-1.2.4}/webapp/public/icons/icon-192.png +0 -0
  322. {dinary-1.2.2 → dinary-1.2.4}/webapp/public/icons/icon-512.png +0 -0
  323. {dinary-1.2.2 → dinary-1.2.4}/webapp/public/manifest.json +0 -0
  324. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/App.vue +0 -0
  325. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/api/_request.js +0 -0
  326. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/api/adminLlm.js +0 -0
  327. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/api/catalog.js +0 -0
  328. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/api/currencies.js +0 -0
  329. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/api/expenseCorrections.js +0 -0
  330. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/api/expenses.js +0 -0
  331. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/api/income.js +0 -0
  332. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/api/receipts.js +0 -0
  333. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/api/review.js +0 -0
  334. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/assets/base.css +0 -0
  335. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/BaseModal.vue +0 -0
  336. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/BaseSheet.vue +0 -0
  337. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/CatalogSelectField.vue +0 -0
  338. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/CategoryQuickPicks.vue +0 -0
  339. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/CategorySheet.vue +0 -0
  340. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/ConfirmDeleteSheet.vue +0 -0
  341. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/CorrectionSheet.vue +0 -0
  342. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/CurrencyAmountRow.vue +0 -0
  343. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/CurrencyPicker.vue +0 -0
  344. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/ExpenseRow.vue +0 -0
  345. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/HeaderSegmented.vue +0 -0
  346. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/HealthSummaryCard.vue +0 -0
  347. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/IconBtn.vue +0 -0
  348. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/IncomeEditSheet.vue +0 -0
  349. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/IncomeForm.vue +0 -0
  350. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/IncomeRow.vue +0 -0
  351. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/InlineCreateEvent.vue +0 -0
  352. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/InlineCreateRow.vue +0 -0
  353. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/KeyboardSaveBar.vue +0 -0
  354. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/ManageList.vue +0 -0
  355. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/QrScanner.vue +0 -0
  356. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/QueueModal.vue +0 -0
  357. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/ReceiptCascadeCard.vue +0 -0
  358. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/RuleRow.vue +0 -0
  359. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/ScopeSelector.vue +0 -0
  360. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/StatusDot.vue +0 -0
  361. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/components/TagPicker.vue +0 -0
  362. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/addResult.js +0 -0
  363. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/catalogManage.js +0 -0
  364. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/flushQueue.js +0 -0
  365. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/flushReceiptQueue.js +0 -0
  366. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/receipt.js +0 -0
  367. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/useExpenseDeleteFlow.js +0 -0
  368. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/useKeyboardVisible.js +0 -0
  369. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/useOnline.js +0 -0
  370. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/useStaleCache.js +0 -0
  371. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/useSwipeRow.js +0 -0
  372. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/composables/zbar.js +0 -0
  373. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/legacy-pwa-cleanup.js +0 -0
  374. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/main.js +0 -0
  375. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/modals/EditModal.vue +0 -0
  376. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/stores/currency.js +0 -0
  377. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/stores/frequentCategories.js +0 -0
  378. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/stores/income.js +0 -0
  379. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/stores/llm.js +0 -0
  380. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/stores/queue.js +0 -0
  381. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/stores/receiptQueue.js +0 -0
  382. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/stores/review.js +0 -0
  383. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/stores/toast.js +0 -0
  384. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/views/AddView.vue +0 -0
  385. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/views/IncomeView.vue +0 -0
  386. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/views/LLMView.vue +0 -0
  387. {dinary-1.2.2 → dinary-1.2.4}/webapp/src/views/ReviewView.vue +0 -0
  388. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/BaseSheet.test.js +0 -0
  389. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/CategoryQuickPicks.test.js +0 -0
  390. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/CategorySheet.test.js +0 -0
  391. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/CurrencyAmountRow.test.js +0 -0
  392. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/ExpenseRow.test.js +0 -0
  393. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/ReceiptCascadeCard.test.js +0 -0
  394. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/ScopeSelector.test.js +0 -0
  395. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/api-adminLlm.test.js +0 -0
  396. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/api-catalog.test.js +0 -0
  397. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/api-currencies.test.js +0 -0
  398. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/api-expenses.test.js +0 -0
  399. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/api-income.test.js +0 -0
  400. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/api-receipts.test.js +0 -0
  401. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/api-request.test.js +0 -0
  402. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/api-review.test.js +0 -0
  403. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-app.test.js +0 -0
  404. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-catalog-select-field.test.js +0 -0
  405. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-correction-sheet.test.js +0 -0
  406. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-currency-picker.test.js +0 -0
  407. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-edit-modal.test.js +0 -0
  408. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-expense-edit-sheet.test.js +0 -0
  409. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-header-segmented.test.js +0 -0
  410. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-health-summary-card.test.js +0 -0
  411. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-inline-create-event.test.js +0 -0
  412. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-inline-create-row.test.js +0 -0
  413. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-manage-list.test.js +0 -0
  414. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-queue-modal.test.js +0 -0
  415. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-review-view.test.js +0 -0
  416. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-rule-row.test.js +0 -0
  417. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-status-dot.test.js +0 -0
  418. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/component-tag-picker.test.js +0 -0
  419. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/composable-add-result.test.js +0 -0
  420. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/composable-catalog-manage.test.js +0 -0
  421. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/composable-flush-queue.test.js +0 -0
  422. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/composable-flush-receipt-queue.test.js +0 -0
  423. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/composable-keyboard-visible.test.js +0 -0
  424. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/composable-receipt.test.js +0 -0
  425. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/composable-stale-cache.test.js +0 -0
  426. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/composable-use-online.test.js +0 -0
  427. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/frequentCategories.test.js +0 -0
  428. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/legacy-pwa-cleanup.test.js +0 -0
  429. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/setup.js +0 -0
  430. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/store-catalog.test.js +0 -0
  431. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/store-currency.test.js +0 -0
  432. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/store-income.test.js +0 -0
  433. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/store-llm.test.js +0 -0
  434. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/store-queue.test.js +0 -0
  435. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/store-receipt-queue-durability.test.js +0 -0
  436. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/store-receipt-queue.test.js +0 -0
  437. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/store-review.test.js +0 -0
  438. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/store-toast.test.js +0 -0
  439. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/useExpenseDeleteFlow.test.js +0 -0
  440. {dinary-1.2.2 → dinary-1.2.4}/webapp/tests/useSwipeRow.test.js +0 -0
  441. {dinary-1.2.2 → dinary-1.2.4}/webapp/vite.config.js +0 -0
  442. {dinary-1.2.2 → dinary-1.2.4}/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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dinary
3
- Version: 1.2.2
3
+ Version: 1.2.4
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,70 @@
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
+ ### PWA sync
19
+
20
+ 5. `PUT /api/analytics/config` endpoint in dinary server + `analytics_pwa_config`
21
+ migration (coordinated with `analytics-pwa.md` work).
22
+
23
+ ---
24
+
25
+ ### AI Views feature
26
+
27
+ 6. **`queries/spending_summary.sql`** — aggregates last-12-months expenses into
28
+ three result sets: events (id, name, total_amount, date_from, date_to),
29
+ tags (id, name, expense_count, total_amount), category groups (id, name,
30
+ total_amount). Used by the LLM before proposing a new view.
31
+
32
+ 7. **`queries/view_data.sql`** — given a basket config passed as a JSON parameter,
33
+ assigns each expense to its first-matching basket (event match checked before
34
+ tag match, unmatched → default basket name), then aggregates by
35
+ (basket_name, year_month, group_name). Returns one row per
36
+ (basket, month, group) triple.
37
+
38
+ 8. **`settings.py` extensions** — `list_view_ids() → list[str]`, `get_view(id)`,
39
+ `save_view(config: dict)`, `delete_view(id)`. Keys in LMDB: `view:<uuid>`.
40
+
41
+ 9. **MCP server** — expose `list_views`, `get_view(id)`, `save_view(config)`,
42
+ `delete_view(id)` tools so Claude Desktop / Claude Code can manage views
43
+ externally.
44
+
45
+ 10. **In-session LLM tools in `dashboard.py`** (Gemini chat tool definitions):
46
+ - `query_spending_summary()` → runs `spending_summary.sql`, returns JSON
47
+ - `propose_view(baskets, default_basket, chart_type)` → sets the in-memory
48
+ draft view config and triggers chart re-render; does not save
49
+ - `update_basket(name, event_ids, tag_ids)` → modifies a basket in the draft
50
+ - `remove_basket(name)` → removes a basket from the draft
51
+ - `set_chart_type(type)` → updates draft chart type
52
+ - `save_current_view(name)` → persists draft via `settings.save_view()`
53
+ - `delete_view(id)` → removes a saved view via `settings.delete_view()`
54
+
55
+ 11. **Altair chart for basket views** — stacked bar: X = year_month, Y = amount,
56
+ color = basket_name. On click of a bar segment: filter to that basket + period
57
+ and show a secondary stacked bar by group_name as a drill-down panel below.
58
+ Use `alt.selection_point` on basket + month for the drill-down interaction.
59
+
60
+ 12. **View selector UI in `dashboard.py`** — `mo.ui.dropdown` populated from
61
+ `settings.list_view_ids()` + labels from stored configs; "New view" button
62
+ clears the draft and triggers the LLM with the `query_spending_summary()`
63
+ result plus instructions to propose baskets with chart. Period selector
64
+ (year / custom range) shown alongside the chart.
65
+
66
+ 13. **"New view" LLM prompt** — system prompt instructs the LLM: (a) call
67
+ `query_spending_summary()` first, (b) produce a `propose_view()` call
68
+ immediately with a concrete basket set, (c) explain each basket choice with
69
+ the numbers from the summary, (d) invite the user to react — never to describe
70
+ what they want in terms of categories or tags.
@@ -0,0 +1,78 @@
1
+ # Analytics — PWA embedded view
2
+
3
+ ## Scope
4
+
5
+ A dedicated `/analytics` route in the existing Vue PWA. Shows a summary of the
6
+ ledger drawn from the dinary server. Complements the standalone `dinary-analytics`
7
+ app (`analytics-ai.md`) — the PWA view is always-accessible and mobile-friendly;
8
+ the standalone app is where deep exploration and AI interaction happen.
9
+
10
+ Out of scope: OLTP writes, sheet logging, imports, migrations.
11
+
12
+ ## Data source
13
+
14
+ New FastAPI endpoints reading dinary SQLite directly. DuckDB is not used on the
15
+ server (1 GB RAM constraint — see `architecture.md`). All queries are plain SQLite
16
+ GROUP BY aggregations.
17
+
18
+ ## Default views (zero configuration)
19
+
20
+ These views are always present regardless of whether analytics-ai has ever been run.
21
+
22
+ 1. **Monthly trend** — total expenses per month for the selected year, broken down
23
+ by category group. Query: expenses JOIN categories JOIN category_groups,
24
+ GROUP BY year_month, group_name.
25
+
26
+ 2. **Events** — all events with total cost in accounting currency, sorted by date
27
+ descending. Query: expenses JOIN events, GROUP BY event_id.
28
+
29
+ ## Config-driven basket views
30
+
31
+ When the user has run analytics-ai and saved one or more Analytics Views, those
32
+ views appear as additional tabs in `/analytics`. The PWA has no view editor — the
33
+ standalone app is the only configuration tool.
34
+
35
+ Config is written by analytics-ai via `PUT /api/analytics/config` and stored in
36
+ `analytics_pwa_config` as a JSON blob under key `views`. The server executes basket
37
+ assignment on request using a parameterised SQLite query; no basket logic lives in
38
+ Python. Basket assignment: for each expense, check event triggers first, then tag
39
+ triggers, first match wins, unmatched go to the default basket.
40
+
41
+ ## Config table
42
+
43
+ `analytics_pwa_config` in dinary SQLite:
44
+
45
+ ```
46
+ key TEXT PRIMARY KEY
47
+ value JSON
48
+ updated_at TIMESTAMP
49
+ ```
50
+
51
+ Only key in use: `views` — JSON array of view config objects (same schema as
52
+ stored in analytics.db, see `analytics-ai.md`). The server never writes this
53
+ table; only `PUT /api/analytics/config` does.
54
+
55
+ ## Implementation outline
56
+
57
+ **Backend**
58
+
59
+ - New FastAPI router `api/analytics.py`.
60
+ - New migration: `analytics_pwa_config` table.
61
+ - `GET /api/analytics/config` — returns current `analytics_pwa_config` rows as JSON.
62
+ - `PUT /api/analytics/config` — replaces one or more keys; called by analytics-ai only.
63
+ - `GET /api/analytics/monthly?year=<year>` — monthly trend data.
64
+ - `GET /api/analytics/events` — events with totals.
65
+ - `GET /api/analytics/view/<view_id>?year=<year>` — executes basket assignment for
66
+ the named view config stored in `analytics_pwa_config.views`, returns
67
+ (basket_name, year_month, group_name, amount) rows.
68
+ - New SQL files: `db/sql/analytics_monthly.sql`, `db/sql/analytics_events.sql`,
69
+ `db/sql/analytics_view.sql` (parameterised basket assignment).
70
+
71
+ **Frontend**
72
+
73
+ - New Vue route `/analytics`, new `AnalyticsView.vue`.
74
+ - On load: fetches config + default view data in parallel.
75
+ - Tab bar: "Monthly" tab + "Events" tab always present; one tab per basket view
76
+ from config (if any).
77
+ - Period selector (year) applies to all tabs.
78
+ - Chart library: to be decided at implementation time — keep it lightweight.