@cachly-dev/mcp-server 0.10.48 → 0.10.49

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 (308) hide show
  1. package/.github/workflows/eas-build-android.yml +58 -0
  2. package/.github/workflows/eas-update.yml +55 -0
  3. package/README.md +5 -33
  4. package/STRATEGY.md +209 -0
  5. package/apps/travel-chaos-organizer/.env.example +49 -0
  6. package/apps/travel-chaos-organizer/DEPLOY.md +328 -0
  7. package/apps/travel-chaos-organizer/Makefile +57 -0
  8. package/apps/travel-chaos-organizer/README.md +159 -0
  9. package/apps/travel-chaos-organizer/backend/.env.example +10 -0
  10. package/apps/travel-chaos-organizer/backend/Dockerfile +25 -0
  11. package/apps/travel-chaos-organizer/backend/alembic/env.py +56 -0
  12. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +24 -0
  13. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  14. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +24 -0
  15. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +24 -0
  16. package/apps/travel-chaos-organizer/backend/alembic.ini +39 -0
  17. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  18. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  19. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +81 -0
  20. package/apps/travel-chaos-organizer/backend/app/config.py +66 -0
  21. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  22. package/apps/travel-chaos-organizer/backend/app/db/database.py +20 -0
  23. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +71 -0
  24. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +31 -0
  25. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +4 -0
  26. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +5 -0
  27. package/apps/travel-chaos-organizer/backend/app/limiter.py +4 -0
  28. package/apps/travel-chaos-organizer/backend/app/logging_config.py +25 -0
  29. package/apps/travel-chaos-organizer/backend/app/main.py +77 -0
  30. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  31. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +142 -0
  32. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  33. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +237 -0
  34. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +13 -0
  35. package/apps/travel-chaos-organizer/backend/app/routers/events.py +29 -0
  36. package/apps/travel-chaos-organizer/backend/app/routers/export.py +122 -0
  37. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +122 -0
  38. package/apps/travel-chaos-organizer/backend/app/routers/items.py +130 -0
  39. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +108 -0
  40. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +303 -0
  41. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +179 -0
  42. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +134 -0
  43. package/apps/travel-chaos-organizer/backend/app/routers/users.py +169 -0
  44. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +37 -0
  45. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  46. package/apps/travel-chaos-organizer/backend/app/services/cache.py +131 -0
  47. package/apps/travel-chaos-organizer/backend/app/services/drip.py +128 -0
  48. package/apps/travel-chaos-organizer/backend/app/services/email.py +61 -0
  49. package/apps/travel-chaos-organizer/backend/app/services/errors.py +44 -0
  50. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +10 -0
  51. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +101 -0
  52. package/apps/travel-chaos-organizer/backend/app/services/parser.py +22 -0
  53. package/apps/travel-chaos-organizer/backend/app/services/quota.py +85 -0
  54. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +11 -0
  55. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +35 -0
  56. package/apps/travel-chaos-organizer/backend/pytest.ini +4 -0
  57. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +6 -0
  58. package/apps/travel-chaos-organizer/backend/requirements.txt +25 -0
  59. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  60. package/apps/travel-chaos-organizer/backend/tests/conftest.py +223 -0
  61. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +99 -0
  62. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +142 -0
  63. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +32 -0
  64. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +121 -0
  65. package/apps/travel-chaos-organizer/backend/tests/test_export.py +48 -0
  66. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +107 -0
  67. package/apps/travel-chaos-organizer/backend/tests/test_items.py +138 -0
  68. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +113 -0
  69. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +145 -0
  70. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +98 -0
  71. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +72 -0
  72. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +163 -0
  73. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +46 -0
  74. package/apps/travel-chaos-organizer/backend/tests/test_search.py +67 -0
  75. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +123 -0
  76. package/apps/travel-chaos-organizer/backend/tests/test_users.py +52 -0
  77. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  78. package/apps/travel-chaos-organizer/caddy/Caddyfile +27 -0
  79. package/apps/travel-chaos-organizer/docker-compose.node1.yml +31 -0
  80. package/apps/travel-chaos-organizer/docker-compose.override.yml +14 -0
  81. package/apps/travel-chaos-organizer/docker-compose.yml +128 -0
  82. package/apps/travel-chaos-organizer/frontend/.env.example +13 -0
  83. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +80 -0
  84. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +166 -0
  85. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +130 -0
  86. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +381 -0
  87. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +346 -0
  88. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +363 -0
  89. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +34 -0
  90. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +35 -0
  91. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +91 -0
  92. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +71 -0
  93. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +29 -0
  94. package/apps/travel-chaos-organizer/frontend/app/index.tsx +16 -0
  95. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +119 -0
  96. package/apps/travel-chaos-organizer/frontend/app.config.ts +54 -0
  97. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +66 -0
  98. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +79 -0
  99. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +15 -0
  100. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +102 -0
  101. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +107 -0
  102. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +109 -0
  103. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +45 -0
  104. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +19 -0
  105. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +155 -0
  106. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +55 -0
  107. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +80 -0
  108. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +39 -0
  109. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +39 -0
  110. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +70 -0
  111. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +121 -0
  112. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +70 -0
  113. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +73 -0
  114. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +130 -0
  115. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +60 -0
  116. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +42 -0
  117. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +146 -0
  118. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +70 -0
  119. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +44 -0
  120. package/apps/travel-chaos-organizer/frontend/eas.json +22 -0
  121. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +24 -0
  122. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +60 -0
  123. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +112 -0
  124. package/apps/travel-chaos-organizer/frontend/lib/api.ts +223 -0
  125. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +93 -0
  126. package/apps/travel-chaos-organizer/frontend/lib/db.ts +86 -0
  127. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +54 -0
  128. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +13 -0
  129. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +112 -0
  130. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +145 -0
  131. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +43 -0
  132. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +28 -0
  133. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +44 -0
  134. package/apps/travel-chaos-organizer/frontend/package-lock.json +13037 -0
  135. package/apps/travel-chaos-organizer/frontend/package.json +44 -0
  136. package/apps/travel-chaos-organizer/frontend/tsconfig.json +23 -0
  137. package/apps/travel-chaos-organizer/keycloak/realm-export.json +59 -0
  138. package/apps/travel-chaos-organizer/landing/index.html +773 -0
  139. package/apps/travel-chaos-organizer/scripts/backup.sh +14 -0
  140. package/apps/travel-chaos-organizer/scripts/configure-env.sh +82 -0
  141. package/apps/travel-chaos-organizer/scripts/deploy.sh +54 -0
  142. package/apps/travel-chaos-organizer/scripts/health-check.sh +36 -0
  143. package/apps/travel-chaos-organizer/scripts/migrate.sh +7 -0
  144. package/apps/travel-chaos-organizer/scripts/server-setup.sh +85 -0
  145. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +29 -0
  146. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +71 -0
  147. package/apps/travel-chaos-organizer/telegram-bot/.env.example +3 -0
  148. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +6 -0
  149. package/apps/travel-chaos-organizer/telegram-bot/bot.py +504 -0
  150. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +3 -0
  151. package/dist/packages/telegram-notify/client.d.ts +19 -0
  152. package/dist/packages/telegram-notify/client.js +75 -0
  153. package/dist/packages/telegram-notify/client.js.map +1 -0
  154. package/dist/src/__tests__/auth.test.js.map +1 -0
  155. package/dist/{__tests__ → src/__tests__}/brain-flow.test.js +141 -0
  156. package/dist/src/__tests__/brain-flow.test.js.map +1 -0
  157. package/dist/src/__tests__/confidence.test.js.map +1 -0
  158. package/dist/src/__tests__/e2e/api-auth.test.js.map +1 -0
  159. package/dist/src/__tests__/e2e/api-health.test.js.map +1 -0
  160. package/dist/src/__tests__/e2e/cli.test.js.map +1 -0
  161. package/dist/src/__tests__/e2e/config.js.map +1 -0
  162. package/dist/src/__tests__/e2e/funnel.test.js.map +1 -0
  163. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +1 -0
  164. package/dist/src/__tests__/e2e/provisioning.test.js.map +1 -0
  165. package/dist/{__tests__ → src/__tests__}/handlers-unit.test.js +0 -2
  166. package/dist/src/__tests__/handlers-unit.test.js.map +1 -0
  167. package/dist/src/__tests__/namespace.test.js.map +1 -0
  168. package/dist/src/auth.js.map +1 -0
  169. package/dist/src/ckg.js.map +1 -0
  170. package/dist/src/confidence.js.map +1 -0
  171. package/dist/src/embeddings.js.map +1 -0
  172. package/dist/src/handlers/advanced.js.map +1 -0
  173. package/dist/{handlers → src/handlers}/brain.js +134 -30
  174. package/dist/src/handlers/brain.js.map +1 -0
  175. package/dist/src/handlers/cache.js.map +1 -0
  176. package/dist/{handlers → src/handlers}/context.js +9 -4
  177. package/dist/src/handlers/context.js.map +1 -0
  178. package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
  179. package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
  180. package/dist/src/handlers/fedbrain.js.map +1 -0
  181. package/dist/src/handlers/instances.js.map +1 -0
  182. package/dist/{handlers → src/handlers}/roadmap.js +7 -3
  183. package/dist/src/handlers/roadmap.js.map +1 -0
  184. package/dist/src/handlers/syndicate.js.map +1 -0
  185. package/dist/src/handlers/tco.d.ts +16 -0
  186. package/dist/src/handlers/tco.js +303 -0
  187. package/dist/src/handlers/tco.js.map +1 -0
  188. package/dist/src/handlers/team.js.map +1 -0
  189. package/dist/{index.js → src/index.js} +153 -218
  190. package/dist/src/index.js.map +1 -0
  191. package/dist/src/namespace.js.map +1 -0
  192. package/dist/src/notifier.d.ts +5 -0
  193. package/dist/src/notifier.js +6 -0
  194. package/dist/src/notifier.js.map +1 -0
  195. package/dist/src/search.js.map +1 -0
  196. package/dist/src/search.test.js.map +1 -0
  197. package/dist/{tools.d.ts → src/tools.d.ts} +196 -0
  198. package/dist/{tools.js → src/tools.js} +178 -0
  199. package/dist/src/tools.js.map +1 -0
  200. package/dist/src/utils.js.map +1 -0
  201. package/package.json +3 -3
  202. package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
  203. package/packages/telegram-notify/client.ts +88 -0
  204. package/packages/telegram-notify/emojis.json +22 -0
  205. package/packages/telegram-notify/pyproject.toml +17 -0
  206. package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
  207. package/packages/telegram-notify/telegram_notify/client.py +87 -0
  208. package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
  209. package/scripts/generate-icons.js +28 -0
  210. package/smithery.yaml +21 -8
  211. package/src/__tests__/brain-flow.test.ts +174 -0
  212. package/src/__tests__/handlers-unit.test.ts +1 -2
  213. package/src/handlers/brain.ts +138 -31
  214. package/src/handlers/context.ts +9 -4
  215. package/src/handlers/fedbrain.ts +21 -7
  216. package/src/handlers/roadmap.ts +7 -3
  217. package/src/handlers/tco.ts +347 -0
  218. package/src/index.ts +139 -220
  219. package/src/notifier.ts +5 -0
  220. package/src/tools.ts +191 -0
  221. package/tsconfig.json +2 -3
  222. package/dist/__tests__/auth.test.js.map +0 -1
  223. package/dist/__tests__/brain-flow.test.js.map +0 -1
  224. package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
  225. package/dist/__tests__/brain-predict-failures.test.js +0 -339
  226. package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
  227. package/dist/__tests__/confidence.test.js.map +0 -1
  228. package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
  229. package/dist/__tests__/e2e/api-health.test.js.map +0 -1
  230. package/dist/__tests__/e2e/cli.test.js.map +0 -1
  231. package/dist/__tests__/e2e/config.js.map +0 -1
  232. package/dist/__tests__/e2e/funnel.test.js.map +0 -1
  233. package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
  234. package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
  235. package/dist/__tests__/handlers-unit.test.js.map +0 -1
  236. package/dist/__tests__/namespace.test.js.map +0 -1
  237. package/dist/auth.js.map +0 -1
  238. package/dist/ckg.js.map +0 -1
  239. package/dist/confidence.js.map +0 -1
  240. package/dist/embeddings.js.map +0 -1
  241. package/dist/handlers/advanced.js.map +0 -1
  242. package/dist/handlers/brain.js.map +0 -1
  243. package/dist/handlers/cache.js.map +0 -1
  244. package/dist/handlers/context.js.map +0 -1
  245. package/dist/handlers/fedbrain.js.map +0 -1
  246. package/dist/handlers/instances.js.map +0 -1
  247. package/dist/handlers/roadmap.js.map +0 -1
  248. package/dist/handlers/syndicate.js.map +0 -1
  249. package/dist/handlers/team.js.map +0 -1
  250. package/dist/index.js.map +0 -1
  251. package/dist/namespace.js.map +0 -1
  252. package/dist/search.js.map +0 -1
  253. package/dist/search.test.js.map +0 -1
  254. package/dist/tools.js.map +0 -1
  255. package/dist/utils.js.map +0 -1
  256. package/src/__tests__/brain-predict-failures.test.ts +0 -430
  257. /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
  258. /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
  259. /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
  260. /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
  261. /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
  262. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
  263. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
  264. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
  265. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
  266. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
  267. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
  268. /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
  269. /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
  270. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
  271. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
  272. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
  273. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
  274. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
  275. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
  276. /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
  277. /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
  278. /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
  279. /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
  280. /package/dist/{auth.js → src/auth.js} +0 -0
  281. /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
  282. /package/dist/{ckg.js → src/ckg.js} +0 -0
  283. /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
  284. /package/dist/{confidence.js → src/confidence.js} +0 -0
  285. /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
  286. /package/dist/{embeddings.js → src/embeddings.js} +0 -0
  287. /package/dist/{handlers → src/handlers}/advanced.d.ts +0 -0
  288. /package/dist/{handlers → src/handlers}/advanced.js +0 -0
  289. /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
  290. /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
  291. /package/dist/{handlers → src/handlers}/cache.js +0 -0
  292. /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
  293. /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
  294. /package/dist/{handlers → src/handlers}/instances.js +0 -0
  295. /package/dist/{handlers → src/handlers}/roadmap.d.ts +0 -0
  296. /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
  297. /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
  298. /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
  299. /package/dist/{handlers → src/handlers}/team.js +0 -0
  300. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  301. /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
  302. /package/dist/{namespace.js → src/namespace.js} +0 -0
  303. /package/dist/{search.d.ts → src/search.d.ts} +0 -0
  304. /package/dist/{search.js → src/search.js} +0 -0
  305. /package/dist/{search.test.d.ts → src/search.test.d.ts} +0 -0
  306. /package/dist/{search.test.js → src/search.test.js} +0 -0
  307. /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
  308. /package/dist/{utils.js → src/utils.js} +0 -0
@@ -0,0 +1,44 @@
1
+ """
2
+ Actionable error helpers — mirrors cachly's handleApiError pattern.
3
+ All user-facing messages include a concrete next step.
4
+ """
5
+
6
+ from fastapi import HTTPException, status
7
+
8
+
9
+ def ollama_unavailable(url: str) -> HTTPException:
10
+ return HTTPException(
11
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
12
+ detail=(
13
+ f"Ollama nicht erreichbar unter {url}. "
14
+ "Stelle sicher, dass Ollama läuft und OLLAMA_URL korrekt gesetzt ist."
15
+ ),
16
+ )
17
+
18
+
19
+ def ollama_model_not_found(model: str) -> HTTPException:
20
+ return HTTPException(
21
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
22
+ detail=(
23
+ f"Ollama-Modell '{model}' nicht gefunden. "
24
+ f"Führe 'ollama pull {model}' auf deinem Server aus."
25
+ ),
26
+ )
27
+
28
+
29
+ def unsupported_file_type(mime: str) -> HTTPException:
30
+ return HTTPException(
31
+ status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
32
+ detail=(
33
+ f"Dateityp '{mime}' wird nicht unterstützt. "
34
+ "Unterstützt: application/pdf, image/*, text/*"
35
+ ),
36
+ )
37
+
38
+
39
+ def low_confidence_warning(confidence: float) -> dict:
40
+ return {
41
+ "warning": f"Niedrige Erkennungssicherheit ({confidence:.0%}). "
42
+ "Bitte parsed_data manuell prüfen.",
43
+ "confidence": confidence,
44
+ }
@@ -0,0 +1,10 @@
1
+ """
2
+ Re-exports from the shared telegram-notify package.
3
+ Canonical implementation: packages/telegram-notify/telegram_notify/client.py
4
+
5
+ Install locally: pip install -e ../../../../packages/telegram-notify
6
+ Docker build: COPY packages/telegram-notify /tmp/telegram-notify && pip install /tmp/telegram-notify
7
+ """
8
+ from telegram_notify import notify, notify_user, format_message
9
+
10
+ __all__ = ["notify", "notify_user", "format_message"]
@@ -0,0 +1,101 @@
1
+ import base64
2
+ import json
3
+ import re
4
+ import httpx
5
+ from app.config import get_settings
6
+ from app.services.errors import ollama_unavailable, ollama_model_not_found
7
+ from app.services import cache as cache_svc
8
+
9
+ settings = get_settings()
10
+
11
+ PARSE_PROMPT = """You are a travel document parser. Extract structured travel information from the following content.
12
+
13
+ Return ONLY valid JSON matching this exact schema:
14
+ {
15
+ "type": "flight|train|bus|hotel|rental_car|activity|transfer|document|other",
16
+ "title": "short human-readable title",
17
+ "booking_ref": "booking/confirmation number or null",
18
+ "provider": "airline/hotel/company name or null",
19
+ "event_at": "ISO 8601 datetime or null",
20
+ "event_end_at": "ISO 8601 datetime or null",
21
+ "origin": "departure city/airport or null",
22
+ "destination": "arrival city/hotel address or null",
23
+ "passengers": ["name1", "name2"] or null,
24
+ "confirmation_number": "separate confirmation if different from booking_ref or null",
25
+ "price": "total price with currency or null",
26
+ "raw_summary": "one sentence summary of this travel item",
27
+ "confidence": 0.0 to 1.0
28
+ }
29
+
30
+ Content to parse:
31
+ """
32
+
33
+
34
+ async def parse_text(raw_text: str) -> tuple[dict, bool]:
35
+ """Returns (parsed_dict, was_cached). Cachly cache hit skips Ollama entirely."""
36
+ cached = await cache_svc.cache_get(raw_text)
37
+ if cached is not None:
38
+ return cached, True
39
+
40
+ payload = {
41
+ "model": settings.ollama_model,
42
+ "prompt": PARSE_PROMPT + raw_text,
43
+ "stream": False,
44
+ "format": "json",
45
+ }
46
+ try:
47
+ async with httpx.AsyncClient(timeout=settings.ollama_timeout) as client:
48
+ resp = await client.post(f"{settings.ollama_url}/api/generate", json=payload)
49
+ if resp.status_code == 404:
50
+ raise ollama_model_not_found(settings.ollama_model)
51
+ resp.raise_for_status()
52
+ response_text = resp.json().get("response", "{}")
53
+ result = _safe_parse(response_text)
54
+ await cache_svc.cache_set(raw_text, result)
55
+ return result, False
56
+ except httpx.ConnectError:
57
+ raise ollama_unavailable(settings.ollama_url)
58
+
59
+
60
+ async def parse_image(image_bytes: bytes, mime_type: str) -> tuple[dict, str, bool]:
61
+ """Returns (parsed_dict, raw_text, was_cached)."""
62
+ cached = await cache_svc.cache_get(image_bytes)
63
+ if cached is not None:
64
+ return cached, "", True
65
+
66
+ b64 = base64.b64encode(image_bytes).decode()
67
+ payload = {
68
+ "model": settings.ollama_model,
69
+ "prompt": PARSE_PROMPT + "Extract all visible travel information from this image.",
70
+ "images": [b64],
71
+ "stream": False,
72
+ "format": "json",
73
+ }
74
+ try:
75
+ async with httpx.AsyncClient(timeout=settings.ollama_timeout) as client:
76
+ resp = await client.post(f"{settings.ollama_url}/api/generate", json=payload)
77
+ if resp.status_code == 404:
78
+ raise ollama_model_not_found(settings.ollama_model)
79
+ resp.raise_for_status()
80
+ response_text = resp.json().get("response", "{}")
81
+ result = _safe_parse(response_text)
82
+ await cache_svc.cache_set(image_bytes, result)
83
+ # For images, Ollama returns structured JSON directly — there is no
84
+ # separate OCR text output. Store the human-readable raw_summary
85
+ # (from the parsed result) as raw_text instead of the JSON blob.
86
+ return result, result.get("raw_summary") or None, False
87
+ except httpx.ConnectError:
88
+ raise ollama_unavailable(settings.ollama_url)
89
+
90
+
91
+ def _safe_parse(text: str) -> dict:
92
+ try:
93
+ cleaned = re.sub(r"```(?:json)?\s*|\s*```", "", text).strip()
94
+ return json.loads(cleaned)
95
+ except json.JSONDecodeError:
96
+ return {
97
+ "type": "other",
98
+ "title": "Unrecognized document",
99
+ "raw_summary": text[:300],
100
+ "confidence": 0.0,
101
+ }
@@ -0,0 +1,22 @@
1
+ """Extracts raw text from various file types before sending to Ollama."""
2
+ import io
3
+ from pathlib import Path
4
+ import fitz # PyMuPDF
5
+
6
+
7
+ def extract_text_from_pdf(content: bytes) -> str:
8
+ doc = fitz.open(stream=content, filetype="pdf")
9
+ pages = [page.get_text() for page in doc]
10
+ return "\n".join(pages).strip()
11
+
12
+
13
+ def is_image(mime_type: str) -> bool:
14
+ return mime_type.startswith("image/")
15
+
16
+
17
+ def is_pdf(mime_type: str) -> bool:
18
+ return mime_type == "application/pdf"
19
+
20
+
21
+ def is_text(mime_type: str) -> bool:
22
+ return mime_type.startswith("text/")
@@ -0,0 +1,85 @@
1
+ """
2
+ Freemium quota enforcement.
3
+
4
+ Plans:
5
+ free — 50 AI parses / day, 3 active trips
6
+ pro — unlimited parses, unlimited trips
7
+
8
+ Usage:
9
+ await quota.check_parse(db, uid) # raises HTTP 429 if over limit
10
+ await quota.check_trips(db, uid) # raises HTTP 402 if over limit
11
+ await quota.get_plan(db, uid) # returns "free" | "pro"
12
+ """
13
+ from datetime import datetime, timedelta, timezone
14
+ from sqlalchemy.ext.asyncio import AsyncSession
15
+ from sqlalchemy import text
16
+ from fastapi import HTTPException
17
+
18
+ FREE_DAILY_PARSES = 50
19
+ FREE_MAX_TRIPS = 3
20
+
21
+
22
+ def _now() -> datetime:
23
+ return datetime.now(timezone.utc)
24
+
25
+
26
+ async def get_plan(db: AsyncSession, uid: str) -> str:
27
+ row = await db.execute(
28
+ text("""
29
+ SELECT plan FROM users
30
+ WHERE id = :uid
31
+ AND (plan_expires_at IS NULL OR plan_expires_at > :now)
32
+ """),
33
+ {"uid": uid, "now": _now()},
34
+ )
35
+ r = row.fetchone()
36
+ return r[0] if r else "free"
37
+
38
+
39
+ async def _ensure_user(db: AsyncSession, uid: str) -> None:
40
+ await db.execute(
41
+ text("INSERT INTO users (id) VALUES (:uid) ON CONFLICT (id) DO NOTHING"),
42
+ {"uid": uid},
43
+ )
44
+
45
+
46
+ async def check_parse(db: AsyncSession, uid: str) -> None:
47
+ await _ensure_user(db, uid)
48
+ plan = await get_plan(db, uid)
49
+ if plan != "free":
50
+ return
51
+
52
+ cutoff = _now() - timedelta(days=1)
53
+ row = await db.execute(
54
+ text("""
55
+ SELECT COUNT(*) FROM events
56
+ WHERE user_id = :uid
57
+ AND event_name IN ('parse_file', 'parse_text', 'parse_url')
58
+ AND created_at >= :cutoff
59
+ """),
60
+ {"uid": uid, "cutoff": cutoff},
61
+ )
62
+ count = row.scalar() or 0
63
+ if count >= FREE_DAILY_PARSES:
64
+ raise HTTPException(
65
+ status_code=429,
66
+ detail=f"Free plan: {FREE_DAILY_PARSES} AI parses per day. Upgrade to Pro for unlimited access.",
67
+ )
68
+
69
+
70
+ async def check_trips(db: AsyncSession, uid: str) -> None:
71
+ await _ensure_user(db, uid)
72
+ plan = await get_plan(db, uid)
73
+ if plan != "free":
74
+ return
75
+
76
+ row = await db.execute(
77
+ text("SELECT COUNT(*) FROM trips WHERE user_id = :uid"),
78
+ {"uid": uid},
79
+ )
80
+ count = row.scalar() or 0
81
+ if count >= FREE_MAX_TRIPS:
82
+ raise HTTPException(
83
+ status_code=402,
84
+ detail=f"Free plan: max {FREE_MAX_TRIPS} trips. Upgrade to Pro for unlimited trips.",
85
+ )
@@ -0,0 +1,11 @@
1
+ def init(dsn: str = "", environment: str = "production", release: str = "0.1.0") -> None:
2
+ if not dsn:
3
+ return
4
+ import sentry_sdk
5
+ sentry_sdk.init(
6
+ dsn=dsn,
7
+ environment=environment,
8
+ release=release,
9
+ traces_sample_rate=0.05,
10
+ profiles_sample_rate=0.01,
11
+ )
@@ -0,0 +1,35 @@
1
+ """
2
+ Fire-and-forget event tracking.
3
+ Never raises — telemetry must never break the request.
4
+ """
5
+ import json
6
+ from typing import Any
7
+ from sqlalchemy.ext.asyncio import AsyncSession
8
+ from sqlalchemy import text
9
+
10
+
11
+ async def track(
12
+ db: AsyncSession,
13
+ user_id: str | None,
14
+ event_name: str,
15
+ properties: dict[str, Any] | None = None,
16
+ platform: str | None = None,
17
+ app_version: str | None = None,
18
+ ) -> None:
19
+ try:
20
+ await db.execute(
21
+ text("""
22
+ INSERT INTO events (user_id, event_name, properties, platform, app_version)
23
+ VALUES (:uid, :name, :props, :platform, :version)
24
+ """),
25
+ {
26
+ "uid": user_id,
27
+ "name": event_name,
28
+ "props": json.dumps(properties or {}),
29
+ "platform": platform,
30
+ "version": app_version,
31
+ },
32
+ )
33
+ await db.commit()
34
+ except Exception:
35
+ pass
@@ -0,0 +1,4 @@
1
+ [pytest]
2
+ asyncio_mode = auto
3
+ testpaths = tests
4
+ addopts = -v --tb=short
@@ -0,0 +1,6 @@
1
+ -r requirements.txt
2
+ pytest==8.3.4
3
+ pytest-asyncio==0.24.0
4
+ pytest-cov==6.0.0
5
+ httpx==0.28.0
6
+ aiosqlite==0.20.0
@@ -0,0 +1,25 @@
1
+ # shared monorepo package — installed from /packages/telegram-notify in Docker
2
+ # local dev: pip install -e ../../../../packages/telegram-notify
3
+ telegram-notify @ file:///packages/telegram-notify
4
+ fastapi==0.115.5
5
+ uvicorn[standard]==0.32.1
6
+ python-jose[cryptography]==3.3.0
7
+ python-multipart==0.0.12
8
+ httpx==0.28.0
9
+ asyncpg==0.30.0
10
+ sqlalchemy[asyncio]==2.0.36
11
+ alembic==1.14.0
12
+ python-dotenv==1.0.1
13
+ pydantic[email]==2.10.3
14
+ email-validator>=2.0
15
+ pydantic-settings==2.6.1
16
+ Pillow==11.0.0
17
+ PyMuPDF==1.24.14
18
+ aiofiles==24.1.0
19
+ python-magic==0.4.27
20
+ redis[asyncio]==5.2.1
21
+ slowapi>=0.1.9
22
+ limits>=3.6.0
23
+ sentry-sdk[fastapi]>=2.0
24
+ stripe>=8.0
25
+ reportlab>=4.0
@@ -0,0 +1,223 @@
1
+ """
2
+ Shared fixtures for Travel Chaos Organizer backend tests.
3
+
4
+ Patterns from cachly-mcp:
5
+ - In-memory mocks instead of real services (no Docker required for unit tests)
6
+ - Deterministic fake data via factory helpers
7
+ - Auth always injectable / bypassable in unit scope
8
+ """
9
+
10
+ import json
11
+ import uuid
12
+ from collections.abc import AsyncGenerator
13
+ from datetime import date
14
+ from typing import Any
15
+ from unittest.mock import AsyncMock, patch
16
+
17
+ import pytest
18
+ import pytest_asyncio
19
+ from fastapi import FastAPI
20
+ from httpx import ASGITransport, AsyncClient
21
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
22
+
23
+ from app.db.database import Base, get_db
24
+ from app.main import app
25
+ from app.models.schemas import ParsedTravelData
26
+
27
+ # ── In-memory SQLite DB (mirrors cachly MockRedis pattern) ────────────────────
28
+
29
+ TEST_DB_URL = "sqlite+aiosqlite:///:memory:"
30
+
31
+
32
+ @pytest_asyncio.fixture(scope="function")
33
+ async def db_engine():
34
+ import sqlalchemy
35
+ engine = create_async_engine(TEST_DB_URL, echo=False)
36
+ async with engine.begin() as conn:
37
+ # Execute each DDL statement separately — SQLite doesn't support
38
+ # multi-statement execute() calls.
39
+ for stmt in SQLITE_SCHEMA.strip().split(";"):
40
+ stmt = stmt.strip()
41
+ if stmt:
42
+ await conn.execute(sqlalchemy.text(stmt))
43
+ yield engine
44
+ await engine.dispose()
45
+
46
+
47
+ @pytest_asyncio.fixture
48
+ async def db_session(db_engine) -> AsyncGenerator[AsyncSession, None]:
49
+ SessionLocal = async_sessionmaker(db_engine, expire_on_commit=False)
50
+ async with SessionLocal() as session:
51
+ yield session
52
+ await session.rollback()
53
+
54
+
55
+ # ── Auth bypass (like cachly's checkJwt mock) ─────────────────────────────────
56
+
57
+ FAKE_USER_ID = "user-test-123"
58
+ FAKE_USER_PAYLOAD = {"sub": FAKE_USER_ID, "email": "test@example.com"}
59
+
60
+
61
+ @pytest_asyncio.fixture
62
+ async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
63
+ """HTTP client with auth bypassed and real in-memory DB."""
64
+ from app.auth.keycloak import get_current_user
65
+ from app.db.database import get_db
66
+
67
+ from app.auth.keycloak import user_id_or_bot
68
+
69
+ async def override_auth():
70
+ return FAKE_USER_PAYLOAD
71
+
72
+ async def override_user_id_or_bot():
73
+ return FAKE_USER_ID
74
+
75
+ async def override_db():
76
+ yield db_session
77
+
78
+ app.dependency_overrides[get_current_user] = override_auth
79
+ app.dependency_overrides[user_id_or_bot] = override_user_id_or_bot
80
+ app.dependency_overrides[get_db] = override_db
81
+
82
+ async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
83
+ yield c
84
+
85
+ app.dependency_overrides.clear()
86
+
87
+
88
+ # ── Ollama mock (no real LLM needed) ──────────────────────────────────────────
89
+
90
+ PARSED_FLIGHT = ParsedTravelData(
91
+ type="flight",
92
+ title="LH 400 Frankfurt → New York",
93
+ booking_ref="ABC123",
94
+ provider="Lufthansa",
95
+ event_at="2024-08-15T10:30:00",
96
+ origin="FRA",
97
+ destination="JFK",
98
+ passengers=["Max Mustermann"],
99
+ price="€ 850",
100
+ confidence=0.92,
101
+ )
102
+
103
+
104
+ @pytest.fixture
105
+ def mock_ollama_flight():
106
+ with patch("app.services.ollama.parse_text", new_callable=AsyncMock) as m:
107
+ m.return_value = (PARSED_FLIGHT.model_dump(), False)
108
+ yield m
109
+
110
+
111
+ @pytest.fixture
112
+ def mock_ollama_low_confidence():
113
+ with patch("app.services.ollama.parse_text", new_callable=AsyncMock) as m:
114
+ m.return_value = (
115
+ {"type": "other", "title": "Unrecognized document", "confidence": 0.1},
116
+ False,
117
+ )
118
+ yield m
119
+
120
+
121
+ # ── Factory helpers ────────────────────────────────────────────────────────────
122
+
123
+ def make_trip(name: str = "Sommerurlaub 2024", **kwargs) -> dict[str, Any]:
124
+ return {"name": name, "description": "Test trip", "start_date": "2024-08-01",
125
+ "end_date": "2024-08-15", **kwargs}
126
+
127
+
128
+ def make_item(**kwargs) -> dict[str, Any]:
129
+ return {"type": "flight", "title": "LH 400 FRA→JFK", "event_at": "2024-08-15T10:30:00",
130
+ "booking_ref": "ABC123", "provider": "Lufthansa", **kwargs}
131
+
132
+
133
+ # ── SQLite-compatible schema (no PostgreSQL ENUMs / pgcrypto) ─────────────────
134
+
135
+ def _uuid_default() -> str:
136
+ """SQLite UUID expression: generates xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format."""
137
+ return (
138
+ "lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' || "
139
+ "lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' || "
140
+ "lower(hex(randomblob(6)))"
141
+ )
142
+
143
+ _UD = _uuid_default()
144
+
145
+ SQLITE_SCHEMA = f"""
146
+ CREATE TABLE IF NOT EXISTS trips (
147
+ id TEXT PRIMARY KEY DEFAULT ({_UD}),
148
+ user_id TEXT NOT NULL,
149
+ name TEXT NOT NULL,
150
+ description TEXT,
151
+ start_date TEXT,
152
+ end_date TEXT,
153
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
154
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
155
+ );
156
+
157
+ CREATE TABLE IF NOT EXISTS trip_items (
158
+ id TEXT PRIMARY KEY DEFAULT ({_UD}),
159
+ trip_id TEXT NOT NULL,
160
+ user_id TEXT NOT NULL,
161
+ type TEXT NOT NULL DEFAULT 'other',
162
+ title TEXT NOT NULL,
163
+ raw_text TEXT,
164
+ parsed_data TEXT,
165
+ event_at TEXT,
166
+ event_end_at TEXT,
167
+ booking_ref TEXT,
168
+ provider TEXT,
169
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
170
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
171
+ );
172
+
173
+ CREATE TABLE IF NOT EXISTS attachments (
174
+ id TEXT PRIMARY KEY DEFAULT ({_UD}),
175
+ trip_item_id TEXT,
176
+ inbox_id TEXT,
177
+ user_id TEXT NOT NULL,
178
+ file_path TEXT NOT NULL,
179
+ file_name TEXT NOT NULL,
180
+ mime_type TEXT NOT NULL,
181
+ size_bytes INTEGER,
182
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
183
+ );
184
+
185
+ CREATE TABLE IF NOT EXISTS chaos_inbox (
186
+ id TEXT PRIMARY KEY DEFAULT ({_UD}),
187
+ user_id TEXT NOT NULL,
188
+ raw_content TEXT,
189
+ source TEXT,
190
+ status TEXT NOT NULL DEFAULT 'pending',
191
+ parsed_data TEXT,
192
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
193
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
194
+ );
195
+
196
+ CREATE TABLE IF NOT EXISTS users (
197
+ id TEXT PRIMARY KEY,
198
+ email TEXT,
199
+ plan TEXT NOT NULL DEFAULT 'free',
200
+ plan_expires_at TEXT,
201
+ stripe_customer_id TEXT,
202
+ telegram_chat_id TEXT UNIQUE,
203
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
204
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
205
+ );
206
+
207
+ CREATE TABLE IF NOT EXISTS events (
208
+ id TEXT PRIMARY KEY DEFAULT ({_UD}),
209
+ user_id TEXT,
210
+ event_name TEXT NOT NULL,
211
+ properties TEXT,
212
+ platform TEXT,
213
+ app_version TEXT,
214
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
215
+ );
216
+
217
+ CREATE TABLE IF NOT EXISTS waitlist (
218
+ id TEXT PRIMARY KEY DEFAULT ({_UD}),
219
+ email TEXT NOT NULL UNIQUE,
220
+ source TEXT DEFAULT 'landing',
221
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
222
+ );
223
+ """
@@ -0,0 +1,99 @@
1
+ """Tests for admin endpoints."""
2
+ import base64
3
+
4
+ import pytest
5
+ from httpx import AsyncClient
6
+
7
+ pytestmark = pytest.mark.asyncio
8
+
9
+
10
+ def admin_headers(user: str = "admin", password: str = "changeme") -> dict:
11
+ token = base64.b64encode(f"{user}:{password}".encode()).decode()
12
+ return {"Authorization": f"Basic {token}"}
13
+
14
+
15
+ async def test_admin_health_requires_auth(client: AsyncClient):
16
+ r = await client.get("/admin/health")
17
+ assert r.status_code == 401
18
+
19
+
20
+ async def test_admin_health_ok(client: AsyncClient):
21
+ r = await client.get("/admin/health", headers=admin_headers())
22
+ assert r.status_code == 200
23
+ data = r.json()
24
+ assert "events_total" in data
25
+ assert "waitlist_total" in data
26
+
27
+
28
+ async def test_admin_dashboard_requires_auth(client: AsyncClient):
29
+ r = await client.get("/admin")
30
+ assert r.status_code == 401
31
+
32
+
33
+ async def test_admin_dashboard_returns_html(client: AsyncClient):
34
+ r = await client.get("/admin", headers=admin_headers())
35
+ assert r.status_code == 200
36
+ assert "text/html" in r.headers["content-type"]
37
+ assert "TCO Admin" in r.text
38
+
39
+
40
+ async def test_admin_wrong_password(client: AsyncClient):
41
+ r = await client.get("/admin/health", headers=admin_headers(password="wrongpassword"))
42
+ assert r.status_code == 401
43
+
44
+
45
+ async def test_admin_events_summary(client: AsyncClient):
46
+ r = await client.get("/admin/events/summary", headers=admin_headers())
47
+ assert r.status_code == 200
48
+ assert "events" in r.json()
49
+
50
+
51
+ async def test_admin_waitlist(client: AsyncClient):
52
+ r = await client.get("/admin/waitlist", headers=admin_headers())
53
+ assert r.status_code == 200
54
+ assert "signups" in r.json()
55
+
56
+
57
+ async def test_admin_set_user_plan(client: AsyncClient, db_session):
58
+ from sqlalchemy import text
59
+
60
+ # First create the user by calling /me
61
+ await client.get("/api/v1/users/me")
62
+
63
+ r = await client.patch(
64
+ "/admin/users/user-test-123/plan?plan=pro", headers=admin_headers()
65
+ )
66
+ assert r.status_code == 200
67
+ assert r.json()["plan"] == "pro"
68
+
69
+ # Verify in DB
70
+ row = await db_session.execute(
71
+ text("SELECT plan FROM users WHERE id = 'user-test-123'")
72
+ )
73
+ assert row.fetchone()[0] == "pro"
74
+
75
+
76
+ async def test_admin_set_invalid_plan(client: AsyncClient):
77
+ """Setting an invalid plan returns 400."""
78
+ # Create user first
79
+ await client.get("/api/v1/users/me")
80
+
81
+ r = await client.patch(
82
+ "/admin/users/user-test-123/plan?plan=enterprise", headers=admin_headers()
83
+ )
84
+ assert r.status_code == 400
85
+
86
+
87
+ async def test_admin_users_list(client: AsyncClient):
88
+ r = await client.get("/admin/users", headers=admin_headers())
89
+ assert r.status_code == 200
90
+ assert "users" in r.json()
91
+
92
+
93
+ async def test_admin_health_counts(client: AsyncClient):
94
+ """Health endpoint returns all expected count fields."""
95
+ r = await client.get("/admin/health", headers=admin_headers())
96
+ assert r.status_code == 200
97
+ data = r.json()
98
+ assert "users_total" in data
99
+ assert "pro_users" in data