dinary 1.2.4__tar.gz → 1.3.0__tar.gz

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