@cloudgrid-io/mcp 0.13.0 → 0.15.0

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 (364) hide show
  1. package/package.json +1 -1
  2. package/src/corpus/capability-map.md +81 -7
  3. package/src/corpus/templates/_cloudgrid.yaml.reference +228 -0
  4. package/src/corpus/templates/admin-dashboard/cloudgrid.yaml +226 -8
  5. package/src/corpus/templates/admin-dashboard/index.md +3 -0
  6. package/src/corpus/templates/ai-app/cloudgrid.yaml +225 -11
  7. package/src/corpus/templates/ai-app/index.md +3 -11
  8. package/src/corpus/templates/ai-knowledge-base/AGENTS.md +254 -0
  9. package/src/corpus/templates/ai-knowledge-base/README.md +28 -0
  10. package/src/corpus/templates/ai-knowledge-base/cloudgrid.yaml +238 -0
  11. package/src/corpus/templates/ai-knowledge-base/index.md +81 -0
  12. package/src/corpus/templates/analytics-dashboard/README.md +85 -0
  13. package/src/corpus/templates/analytics-dashboard/cloudgrid.yaml +232 -0
  14. package/src/corpus/templates/analytics-dashboard/index.md +316 -0
  15. package/src/corpus/templates/analytics-dashboard/services/web/app/api/events/route.js +60 -0
  16. package/src/corpus/templates/analytics-dashboard/services/web/app/event-form.js +100 -0
  17. package/src/corpus/templates/analytics-dashboard/services/web/app/layout.js +41 -0
  18. package/src/corpus/templates/analytics-dashboard/services/web/app/page.js +54 -0
  19. package/src/corpus/templates/analytics-dashboard/services/web/lib/db.js +32 -0
  20. package/src/corpus/templates/analytics-dashboard/services/web/package.json +16 -0
  21. package/src/corpus/templates/api-dashboard/README.md +84 -0
  22. package/src/corpus/templates/api-dashboard/cloudgrid.yaml +233 -0
  23. package/src/corpus/templates/api-dashboard/index.md +310 -0
  24. package/src/corpus/templates/api-dashboard/services/web/app/api/requests/route.js +62 -0
  25. package/src/corpus/templates/api-dashboard/services/web/app/dashboard.js +156 -0
  26. package/src/corpus/templates/api-dashboard/services/web/app/layout.js +42 -0
  27. package/src/corpus/templates/api-dashboard/services/web/app/page.js +39 -0
  28. package/src/corpus/templates/api-dashboard/services/web/lib/db.js +32 -0
  29. package/src/corpus/templates/api-dashboard/services/web/package.json +16 -0
  30. package/src/corpus/templates/api-docs/cloudgrid.yaml +222 -4
  31. package/src/corpus/templates/api-docs/index.md +3 -5
  32. package/src/corpus/templates/api-service/cloudgrid.yaml +224 -9
  33. package/src/corpus/templates/api-service/index.md +3 -11
  34. package/src/corpus/templates/app-with-data/cloudgrid.yaml +226 -8
  35. package/src/corpus/templates/app-with-data/index.md +3 -8
  36. package/src/corpus/templates/appointment-booking/AGENTS.md +223 -0
  37. package/src/corpus/templates/appointment-booking/README.md +16 -0
  38. package/src/corpus/templates/appointment-booking/cloudgrid.yaml +250 -0
  39. package/src/corpus/templates/appointment-booking/index.md +72 -0
  40. package/src/corpus/templates/approval-workflow/AGENTS.md +170 -0
  41. package/src/corpus/templates/approval-workflow/README.md +12 -0
  42. package/src/corpus/templates/approval-workflow/cloudgrid.yaml +237 -0
  43. package/src/corpus/templates/approval-workflow/index.md +58 -0
  44. package/src/corpus/templates/billing-dashboard/AGENTS.md +199 -0
  45. package/src/corpus/templates/billing-dashboard/README.md +18 -0
  46. package/src/corpus/templates/billing-dashboard/cloudgrid.yaml +235 -0
  47. package/src/corpus/templates/billing-dashboard/index.md +87 -0
  48. package/src/corpus/templates/blog-cms/README.md +94 -0
  49. package/src/corpus/templates/blog-cms/cloudgrid.yaml +233 -0
  50. package/src/corpus/templates/blog-cms/index.md +358 -0
  51. package/src/corpus/templates/blog-cms/services/web/app/admin/admin-editor.js +133 -0
  52. package/src/corpus/templates/blog-cms/services/web/app/admin/page.js +33 -0
  53. package/src/corpus/templates/blog-cms/services/web/app/api/posts/route.js +84 -0
  54. package/src/corpus/templates/blog-cms/services/web/app/layout.js +47 -0
  55. package/src/corpus/templates/blog-cms/services/web/app/page.js +56 -0
  56. package/src/corpus/templates/blog-cms/services/web/app/posts/[slug]/page.js +45 -0
  57. package/src/corpus/templates/blog-cms/services/web/lib/db.js +32 -0
  58. package/src/corpus/templates/blog-cms/services/web/package.json +16 -0
  59. package/src/corpus/templates/booking-system/AGENTS.md +189 -0
  60. package/src/corpus/templates/booking-system/README.md +17 -0
  61. package/src/corpus/templates/booking-system/cloudgrid.yaml +248 -0
  62. package/src/corpus/templates/booking-system/index.md +86 -0
  63. package/src/corpus/templates/calendar-scheduler/AGENTS.md +226 -0
  64. package/src/corpus/templates/calendar-scheduler/README.md +25 -0
  65. package/src/corpus/templates/calendar-scheduler/cloudgrid.yaml +259 -0
  66. package/src/corpus/templates/calendar-scheduler/index.md +85 -0
  67. package/src/corpus/templates/changelog/cloudgrid.yaml +222 -4
  68. package/src/corpus/templates/changelog/index.md +3 -5
  69. package/src/corpus/templates/company-website/README.md +54 -0
  70. package/src/corpus/templates/company-website/cloudgrid.yaml +233 -0
  71. package/src/corpus/templates/company-website/index.html +309 -0
  72. package/src/corpus/templates/company-website/index.md +29 -0
  73. package/src/corpus/templates/course-platform/AGENTS.md +161 -0
  74. package/src/corpus/templates/course-platform/README.md +26 -0
  75. package/src/corpus/templates/course-platform/cloudgrid.yaml +237 -0
  76. package/src/corpus/templates/course-platform/index.md +70 -0
  77. package/src/corpus/templates/crm/cloudgrid.yaml +226 -8
  78. package/src/corpus/templates/crm/index.md +3 -0
  79. package/src/corpus/templates/dashboard/cloudgrid.yaml +228 -4
  80. package/src/corpus/templates/dashboard/index.md +3 -5
  81. package/src/corpus/templates/deck/cloudgrid.yaml +228 -4
  82. package/src/corpus/templates/deck/index.md +3 -5
  83. package/src/corpus/templates/directory/README.md +75 -0
  84. package/src/corpus/templates/directory/cloudgrid.yaml +233 -0
  85. package/src/corpus/templates/directory/index.md +334 -0
  86. package/src/corpus/templates/directory/services/web/app/api/entries/route.js +63 -0
  87. package/src/corpus/templates/directory/services/web/app/directory-list.js +146 -0
  88. package/src/corpus/templates/directory/services/web/app/layout.js +44 -0
  89. package/src/corpus/templates/directory/services/web/app/page.js +38 -0
  90. package/src/corpus/templates/directory/services/web/lib/db.js +32 -0
  91. package/src/corpus/templates/directory/services/web/package.json +16 -0
  92. package/src/corpus/templates/docs-site/cloudgrid.yaml +222 -4
  93. package/src/corpus/templates/docs-site/index.md +3 -5
  94. package/src/corpus/templates/erp/AGENTS.md +116 -0
  95. package/src/corpus/templates/erp/README.md +17 -0
  96. package/src/corpus/templates/erp/cloudgrid.yaml +235 -0
  97. package/src/corpus/templates/erp/index.md +81 -0
  98. package/src/corpus/templates/event-board/README.md +74 -0
  99. package/src/corpus/templates/event-board/cloudgrid.yaml +233 -0
  100. package/src/corpus/templates/event-board/index.md +299 -0
  101. package/src/corpus/templates/event-board/services/web/app/api/events/route.js +61 -0
  102. package/src/corpus/templates/event-board/services/web/app/event-form.js +118 -0
  103. package/src/corpus/templates/event-board/services/web/app/layout.js +39 -0
  104. package/src/corpus/templates/event-board/services/web/app/page.js +34 -0
  105. package/src/corpus/templates/event-board/services/web/lib/db.js +32 -0
  106. package/src/corpus/templates/event-board/services/web/package.json +16 -0
  107. package/src/corpus/templates/expense-tracker/README.md +76 -0
  108. package/src/corpus/templates/expense-tracker/cloudgrid.yaml +233 -0
  109. package/src/corpus/templates/expense-tracker/index.md +305 -0
  110. package/src/corpus/templates/expense-tracker/services/web/app/api/expenses/route.js +61 -0
  111. package/src/corpus/templates/expense-tracker/services/web/app/expense-form.js +145 -0
  112. package/src/corpus/templates/expense-tracker/services/web/app/layout.js +42 -0
  113. package/src/corpus/templates/expense-tracker/services/web/app/page.js +34 -0
  114. package/src/corpus/templates/expense-tracker/services/web/lib/db.js +32 -0
  115. package/src/corpus/templates/expense-tracker/services/web/package.json +16 -0
  116. package/src/corpus/templates/feature-request-board/README.md +87 -0
  117. package/src/corpus/templates/feature-request-board/cloudgrid.yaml +233 -0
  118. package/src/corpus/templates/feature-request-board/index.md +317 -0
  119. package/src/corpus/templates/feature-request-board/services/web/app/api/requests/route.js +92 -0
  120. package/src/corpus/templates/feature-request-board/services/web/app/board.js +91 -0
  121. package/src/corpus/templates/feature-request-board/services/web/app/layout.js +43 -0
  122. package/src/corpus/templates/feature-request-board/services/web/app/page.js +35 -0
  123. package/src/corpus/templates/feature-request-board/services/web/lib/db.js +32 -0
  124. package/src/corpus/templates/feature-request-board/services/web/package.json +16 -0
  125. package/src/corpus/templates/financial-dashboard/README.md +80 -0
  126. package/src/corpus/templates/financial-dashboard/cloudgrid.yaml +233 -0
  127. package/src/corpus/templates/financial-dashboard/index.md +326 -0
  128. package/src/corpus/templates/financial-dashboard/services/web/app/api/entries/route.js +63 -0
  129. package/src/corpus/templates/financial-dashboard/services/web/app/entry-board.js +130 -0
  130. package/src/corpus/templates/financial-dashboard/services/web/app/layout.js +46 -0
  131. package/src/corpus/templates/financial-dashboard/services/web/app/page.js +31 -0
  132. package/src/corpus/templates/financial-dashboard/services/web/lib/db.js +32 -0
  133. package/src/corpus/templates/financial-dashboard/services/web/package.json +16 -0
  134. package/src/corpus/templates/forum/AGENTS.md +196 -0
  135. package/src/corpus/templates/forum/README.md +12 -0
  136. package/src/corpus/templates/forum/cloudgrid.yaml +237 -0
  137. package/src/corpus/templates/forum/index.md +66 -0
  138. package/src/corpus/templates/hr-portal/AGENTS.md +174 -0
  139. package/src/corpus/templates/hr-portal/README.md +14 -0
  140. package/src/corpus/templates/hr-portal/cloudgrid.yaml +237 -0
  141. package/src/corpus/templates/hr-portal/index.md +56 -0
  142. package/src/corpus/templates/internal-tools-portal/AGENTS.md +201 -0
  143. package/src/corpus/templates/internal-tools-portal/README.md +17 -0
  144. package/src/corpus/templates/internal-tools-portal/cloudgrid.yaml +237 -0
  145. package/src/corpus/templates/internal-tools-portal/index.md +62 -0
  146. package/src/corpus/templates/inventory/README.md +71 -0
  147. package/src/corpus/templates/inventory/cloudgrid.yaml +233 -0
  148. package/src/corpus/templates/inventory/index.md +68 -0
  149. package/src/corpus/templates/inventory/services/web/app/api/items/route.js +88 -0
  150. package/src/corpus/templates/inventory/services/web/app/inventory-manager.js +101 -0
  151. package/src/corpus/templates/inventory/services/web/app/layout.js +36 -0
  152. package/src/corpus/templates/inventory/services/web/app/page.js +37 -0
  153. package/src/corpus/templates/inventory/services/web/lib/db.js +32 -0
  154. package/src/corpus/templates/inventory/services/web/package.json +16 -0
  155. package/src/corpus/templates/invoice/README.md +71 -0
  156. package/src/corpus/templates/invoice/cloudgrid.yaml +233 -0
  157. package/src/corpus/templates/invoice/index.md +68 -0
  158. package/src/corpus/templates/invoice/services/web/app/api/invoices/route.js +82 -0
  159. package/src/corpus/templates/invoice/services/web/app/invoice-manager.js +101 -0
  160. package/src/corpus/templates/invoice/services/web/app/layout.js +35 -0
  161. package/src/corpus/templates/invoice/services/web/app/page.js +34 -0
  162. package/src/corpus/templates/invoice/services/web/lib/db.js +32 -0
  163. package/src/corpus/templates/invoice/services/web/package.json +16 -0
  164. package/src/corpus/templates/job-board/README.md +71 -0
  165. package/src/corpus/templates/job-board/cloudgrid.yaml +233 -0
  166. package/src/corpus/templates/job-board/index.md +68 -0
  167. package/src/corpus/templates/job-board/services/web/app/api/jobs/route.js +66 -0
  168. package/src/corpus/templates/job-board/services/web/app/job-board.js +73 -0
  169. package/src/corpus/templates/job-board/services/web/app/layout.js +35 -0
  170. package/src/corpus/templates/job-board/services/web/app/page.js +32 -0
  171. package/src/corpus/templates/job-board/services/web/lib/db.js +32 -0
  172. package/src/corpus/templates/job-board/services/web/package.json +16 -0
  173. package/src/corpus/templates/kanban/cloudgrid.yaml +226 -8
  174. package/src/corpus/templates/kanban/index.md +3 -0
  175. package/src/corpus/templates/landing-page/cloudgrid.yaml +228 -4
  176. package/src/corpus/templates/landing-page/index.md +3 -5
  177. package/src/corpus/templates/lms/AGENTS.md +193 -0
  178. package/src/corpus/templates/lms/README.md +27 -0
  179. package/src/corpus/templates/lms/cloudgrid.yaml +237 -0
  180. package/src/corpus/templates/lms/index.md +79 -0
  181. package/src/corpus/templates/marketplace/AGENTS.md +208 -0
  182. package/src/corpus/templates/marketplace/README.md +22 -0
  183. package/src/corpus/templates/marketplace/cloudgrid.yaml +233 -0
  184. package/src/corpus/templates/marketplace/index.md +76 -0
  185. package/src/corpus/templates/membership-site/AGENTS.md +160 -0
  186. package/src/corpus/templates/membership-site/README.md +18 -0
  187. package/src/corpus/templates/membership-site/cloudgrid.yaml +236 -0
  188. package/src/corpus/templates/membership-site/index.md +66 -0
  189. package/src/corpus/templates/monitoring-dashboard/README.md +84 -0
  190. package/src/corpus/templates/monitoring-dashboard/cloudgrid.yaml +233 -0
  191. package/src/corpus/templates/monitoring-dashboard/index.md +328 -0
  192. package/src/corpus/templates/monitoring-dashboard/services/web/app/api/checks/route.js +76 -0
  193. package/src/corpus/templates/monitoring-dashboard/services/web/app/check-form.js +124 -0
  194. package/src/corpus/templates/monitoring-dashboard/services/web/app/layout.js +44 -0
  195. package/src/corpus/templates/monitoring-dashboard/services/web/app/page.js +49 -0
  196. package/src/corpus/templates/monitoring-dashboard/services/web/lib/db.js +32 -0
  197. package/src/corpus/templates/monitoring-dashboard/services/web/package.json +16 -0
  198. package/src/corpus/templates/online-store/AGENTS.md +153 -0
  199. package/src/corpus/templates/online-store/README.md +16 -0
  200. package/src/corpus/templates/online-store/cloudgrid.yaml +234 -0
  201. package/src/corpus/templates/online-store/index.md +81 -0
  202. package/src/corpus/templates/portfolio/cloudgrid.yaml +222 -4
  203. package/src/corpus/templates/portfolio/index.md +3 -5
  204. package/src/corpus/templates/product-catalog/README.md +74 -0
  205. package/src/corpus/templates/product-catalog/cloudgrid.yaml +232 -0
  206. package/src/corpus/templates/product-catalog/index.md +330 -0
  207. package/src/corpus/templates/product-catalog/services/web/app/api/products/route.js +69 -0
  208. package/src/corpus/templates/product-catalog/services/web/app/catalog.js +153 -0
  209. package/src/corpus/templates/product-catalog/services/web/app/layout.js +49 -0
  210. package/src/corpus/templates/product-catalog/services/web/app/page.js +36 -0
  211. package/src/corpus/templates/product-catalog/services/web/lib/db.js +32 -0
  212. package/src/corpus/templates/product-catalog/services/web/package.json +16 -0
  213. package/src/corpus/templates/product-launch/README.md +46 -0
  214. package/src/corpus/templates/product-launch/cloudgrid.yaml +234 -0
  215. package/src/corpus/templates/product-launch/index.html +320 -0
  216. package/src/corpus/templates/product-launch/index.md +34 -0
  217. package/src/corpus/templates/project-management/README.md +83 -0
  218. package/src/corpus/templates/project-management/cloudgrid.yaml +233 -0
  219. package/src/corpus/templates/project-management/index.md +346 -0
  220. package/src/corpus/templates/project-management/services/web/app/api/projects/route.js +48 -0
  221. package/src/corpus/templates/project-management/services/web/app/api/tasks/route.js +85 -0
  222. package/src/corpus/templates/project-management/services/web/app/board.js +182 -0
  223. package/src/corpus/templates/project-management/services/web/app/layout.js +45 -0
  224. package/src/corpus/templates/project-management/services/web/app/page.js +49 -0
  225. package/src/corpus/templates/project-management/services/web/lib/db.js +32 -0
  226. package/src/corpus/templates/project-management/services/web/package.json +16 -0
  227. package/src/corpus/templates/property-listings/README.md +76 -0
  228. package/src/corpus/templates/property-listings/cloudgrid.yaml +233 -0
  229. package/src/corpus/templates/property-listings/index.md +335 -0
  230. package/src/corpus/templates/property-listings/services/web/app/api/properties/route.js +73 -0
  231. package/src/corpus/templates/property-listings/services/web/app/layout.js +45 -0
  232. package/src/corpus/templates/property-listings/services/web/app/listings.js +152 -0
  233. package/src/corpus/templates/property-listings/services/web/app/page.js +53 -0
  234. package/src/corpus/templates/property-listings/services/web/lib/db.js +32 -0
  235. package/src/corpus/templates/property-listings/services/web/package.json +16 -0
  236. package/src/corpus/templates/quiz-platform/README.md +84 -0
  237. package/src/corpus/templates/quiz-platform/cloudgrid.yaml +233 -0
  238. package/src/corpus/templates/quiz-platform/index.md +313 -0
  239. package/src/corpus/templates/quiz-platform/services/web/app/api/questions/route.js +69 -0
  240. package/src/corpus/templates/quiz-platform/services/web/app/layout.js +47 -0
  241. package/src/corpus/templates/quiz-platform/services/web/app/page.js +37 -0
  242. package/src/corpus/templates/quiz-platform/services/web/app/quiz.js +188 -0
  243. package/src/corpus/templates/quiz-platform/services/web/lib/db.js +32 -0
  244. package/src/corpus/templates/quiz-platform/services/web/package.json +16 -0
  245. package/src/corpus/templates/report/cloudgrid.yaml +228 -4
  246. package/src/corpus/templates/report/index.md +3 -5
  247. package/src/corpus/templates/restaurant-reservations/AGENTS.md +258 -0
  248. package/src/corpus/templates/restaurant-reservations/README.md +17 -0
  249. package/src/corpus/templates/restaurant-reservations/cloudgrid.yaml +258 -0
  250. package/src/corpus/templates/restaurant-reservations/index.md +62 -0
  251. package/src/corpus/templates/revenue-dashboard/README.md +75 -0
  252. package/src/corpus/templates/revenue-dashboard/cloudgrid.yaml +233 -0
  253. package/src/corpus/templates/revenue-dashboard/index.md +307 -0
  254. package/src/corpus/templates/revenue-dashboard/services/web/app/api/sales/route.js +60 -0
  255. package/src/corpus/templates/revenue-dashboard/services/web/app/layout.js +40 -0
  256. package/src/corpus/templates/revenue-dashboard/services/web/app/page.js +87 -0
  257. package/src/corpus/templates/revenue-dashboard/services/web/app/sale-form.js +63 -0
  258. package/src/corpus/templates/revenue-dashboard/services/web/lib/db.js +32 -0
  259. package/src/corpus/templates/revenue-dashboard/services/web/package.json +16 -0
  260. package/src/corpus/templates/saas-marketing/cloudgrid.yaml +222 -4
  261. package/src/corpus/templates/saas-marketing/index.md +3 -5
  262. package/src/corpus/templates/semantic-search/AGENTS.md +192 -0
  263. package/src/corpus/templates/semantic-search/README.md +74 -0
  264. package/src/corpus/templates/semantic-search/cloudgrid.yaml +217 -0
  265. package/src/corpus/templates/semantic-search/index.md +97 -0
  266. package/src/corpus/templates/semantic-search/services/backend/requirements.txt +7 -0
  267. package/src/corpus/templates/semantic-search/services/backend/src/app/__init__.py +0 -0
  268. package/src/corpus/templates/semantic-search/services/backend/src/app/answer.py +90 -0
  269. package/src/corpus/templates/semantic-search/services/backend/src/app/auth.py +65 -0
  270. package/src/corpus/templates/semantic-search/services/backend/src/app/config.py +110 -0
  271. package/src/corpus/templates/semantic-search/services/backend/src/app/db.py +89 -0
  272. package/src/corpus/templates/semantic-search/services/backend/src/app/embeddings.py +44 -0
  273. package/src/corpus/templates/semantic-search/services/backend/src/app/indexing.py +222 -0
  274. package/src/corpus/templates/semantic-search/services/backend/src/app/main.py +259 -0
  275. package/src/corpus/templates/semantic-search/services/backend/src/app/search.py +222 -0
  276. package/src/corpus/templates/semantic-search/services/backend/src/app/source.py +259 -0
  277. package/src/corpus/templates/semantic-search/services/backend/src/main.py +12 -0
  278. package/src/corpus/templates/semantic-search/services/web/index.html +12 -0
  279. package/src/corpus/templates/semantic-search/services/web/package-lock.json +1680 -0
  280. package/src/corpus/templates/semantic-search/services/web/package.json +19 -0
  281. package/src/corpus/templates/semantic-search/services/web/src/App.jsx +55 -0
  282. package/src/corpus/templates/semantic-search/services/web/src/api.js +125 -0
  283. package/src/corpus/templates/semantic-search/services/web/src/components/Dashboard.jsx +61 -0
  284. package/src/corpus/templates/semantic-search/services/web/src/components/ManagerLogin.jsx +39 -0
  285. package/src/corpus/templates/semantic-search/services/web/src/components/SearchView.jsx +166 -0
  286. package/src/corpus/templates/semantic-search/services/web/src/components/Settings.jsx +148 -0
  287. package/src/corpus/templates/semantic-search/services/web/src/main.jsx +10 -0
  288. package/src/corpus/templates/semantic-search/services/web/src/styles.css +78 -0
  289. package/src/corpus/templates/semantic-search/services/web/vite.config.js +10 -0
  290. package/src/corpus/templates/status-page/cloudgrid.yaml +222 -4
  291. package/src/corpus/templates/status-page/index.md +3 -5
  292. package/src/corpus/templates/subscription-management/AGENTS.md +191 -0
  293. package/src/corpus/templates/subscription-management/README.md +21 -0
  294. package/src/corpus/templates/subscription-management/cloudgrid.yaml +236 -0
  295. package/src/corpus/templates/subscription-management/index.md +73 -0
  296. package/src/corpus/templates/task-manager/cloudgrid.yaml +226 -8
  297. package/src/corpus/templates/task-manager/index.md +3 -0
  298. package/src/corpus/templates/ticket-system/README.md +71 -0
  299. package/src/corpus/templates/ticket-system/cloudgrid.yaml +233 -0
  300. package/src/corpus/templates/ticket-system/index.md +68 -0
  301. package/src/corpus/templates/ticket-system/services/web/app/api/tickets/route.js +81 -0
  302. package/src/corpus/templates/ticket-system/services/web/app/layout.js +36 -0
  303. package/src/corpus/templates/ticket-system/services/web/app/page.js +35 -0
  304. package/src/corpus/templates/ticket-system/services/web/app/ticket-queue.js +112 -0
  305. package/src/corpus/templates/ticket-system/services/web/lib/db.js +32 -0
  306. package/src/corpus/templates/ticket-system/services/web/package.json +16 -0
  307. package/src/corpus/templates/time-tracking/README.md +81 -0
  308. package/src/corpus/templates/time-tracking/cloudgrid.yaml +233 -0
  309. package/src/corpus/templates/time-tracking/index.md +298 -0
  310. package/src/corpus/templates/time-tracking/services/web/app/api/entries/route.js +66 -0
  311. package/src/corpus/templates/time-tracking/services/web/app/entry-form.js +106 -0
  312. package/src/corpus/templates/time-tracking/services/web/app/layout.js +39 -0
  313. package/src/corpus/templates/time-tracking/services/web/app/page.js +30 -0
  314. package/src/corpus/templates/time-tracking/services/web/lib/db.js +32 -0
  315. package/src/corpus/templates/time-tracking/services/web/package.json +16 -0
  316. package/src/corpus/templates/travel-booking/AGENTS.md +207 -0
  317. package/src/corpus/templates/travel-booking/README.md +17 -0
  318. package/src/corpus/templates/travel-booking/cloudgrid.yaml +249 -0
  319. package/src/corpus/templates/travel-booking/index.md +88 -0
  320. package/src/corpus/templates/waitlist/cloudgrid.yaml +222 -4
  321. package/src/corpus/templates/waitlist/index.md +3 -5
  322. package/src/corpus/templates/web-app/cloudgrid.yaml +228 -4
  323. package/src/corpus/templates/web-app/index.md +3 -5
  324. package/src/corpus/workflows/ai-knowledge-base.md +137 -0
  325. package/src/corpus/workflows/analytics-dashboard.md +104 -0
  326. package/src/corpus/workflows/api-dashboard.md +102 -0
  327. package/src/corpus/workflows/appointment-booking.md +143 -0
  328. package/src/corpus/workflows/approval-workflow.md +124 -0
  329. package/src/corpus/workflows/billing-dashboard.md +120 -0
  330. package/src/corpus/workflows/blog-cms.md +104 -0
  331. package/src/corpus/workflows/booking-system.md +116 -0
  332. package/src/corpus/workflows/calendar-scheduler.md +134 -0
  333. package/src/corpus/workflows/company-website.md +46 -0
  334. package/src/corpus/workflows/course-platform.md +126 -0
  335. package/src/corpus/workflows/directory.md +101 -0
  336. package/src/corpus/workflows/erp.md +125 -0
  337. package/src/corpus/workflows/event-board.md +102 -0
  338. package/src/corpus/workflows/expense-tracker.md +102 -0
  339. package/src/corpus/workflows/feature-request-board.md +105 -0
  340. package/src/corpus/workflows/financial-dashboard.md +103 -0
  341. package/src/corpus/workflows/forum.md +128 -0
  342. package/src/corpus/workflows/hr-portal.md +115 -0
  343. package/src/corpus/workflows/internal-tools-portal.md +125 -0
  344. package/src/corpus/workflows/inventory.md +101 -0
  345. package/src/corpus/workflows/invoice.md +101 -0
  346. package/src/corpus/workflows/job-board.md +101 -0
  347. package/src/corpus/workflows/lms.md +113 -0
  348. package/src/corpus/workflows/marketplace.md +123 -0
  349. package/src/corpus/workflows/membership-site.md +128 -0
  350. package/src/corpus/workflows/monitoring-dashboard.md +104 -0
  351. package/src/corpus/workflows/online-store.md +114 -0
  352. package/src/corpus/workflows/product-catalog.md +105 -0
  353. package/src/corpus/workflows/product-launch.md +43 -0
  354. package/src/corpus/workflows/project-management.md +106 -0
  355. package/src/corpus/workflows/property-listings.md +101 -0
  356. package/src/corpus/workflows/quiz-platform.md +105 -0
  357. package/src/corpus/workflows/restaurant-reservations.md +123 -0
  358. package/src/corpus/workflows/revenue-dashboard.md +102 -0
  359. package/src/corpus/workflows/semantic-search.md +133 -0
  360. package/src/corpus/workflows/subscription-management.md +141 -0
  361. package/src/corpus/workflows/ticket-system.md +101 -0
  362. package/src/corpus/workflows/time-tracking.md +104 -0
  363. package/src/corpus/workflows/travel-booking.md +115 -0
  364. package/src/tools.js +73 -12
@@ -0,0 +1,44 @@
1
+ """Single config point for embeddings (OpenAI-compatible).
2
+
3
+ Isolated behind one module so the provider can be swapped by env var
4
+ (EMBEDDINGS_BASE_URL / EMBEDDINGS_MODEL / EMBEDDINGS_API_KEY). NEVER reads the
5
+ API key at import time — only inside embed(). If the key is missing, embed()
6
+ raises EmbeddingsUnavailable, which callers translate into a graceful degraded
7
+ state (lexical + metadata search still work).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import httpx
12
+
13
+ from . import config
14
+
15
+
16
+ class EmbeddingsUnavailable(RuntimeError):
17
+ """Raised when embeddings are requested but not configured/reachable."""
18
+
19
+
20
+ def embed(texts: list[str]) -> list[list[float]]:
21
+ """Return one embedding vector per input text.
22
+
23
+ Raises EmbeddingsUnavailable if EMBEDDINGS_API_KEY is not set.
24
+ """
25
+ if not texts:
26
+ return []
27
+ key = config.embeddings_api_key()
28
+ if not key:
29
+ raise EmbeddingsUnavailable(
30
+ "EMBEDDINGS_API_KEY is not set — semantic embedding is unavailable."
31
+ )
32
+ resp = httpx.post(
33
+ f"{config.embeddings_base_url()}/embeddings",
34
+ headers={"Authorization": f"Bearer {key}"},
35
+ json={"model": config.embeddings_model(), "input": texts},
36
+ timeout=60.0,
37
+ )
38
+ resp.raise_for_status()
39
+ data = resp.json()["data"]
40
+ return [row["embedding"] for row in data]
41
+
42
+
43
+ def embed_one(text: str) -> list[float]:
44
+ return embed([text])[0]
@@ -0,0 +1,222 @@
1
+ """Incremental indexing pipeline: discover -> extract -> chunk -> embed -> store.
2
+
3
+ Shared by the manager 'Refresh now' endpoint (and, once platform issue #1585
4
+ lands, a `type: cron` refresh service). It is resilient: if the source or
5
+ embeddings are not configured it records a clear, non-crashing report and
6
+ returns. Only new/changed files (by rev / content-hash) are reprocessed.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import datetime as dt
11
+ import os
12
+ import re
13
+
14
+ from . import config, db, embeddings, source
15
+
16
+ CHUNK_CHARS = 1200
17
+ CHUNK_OVERLAP = 150
18
+
19
+ # Generic date detection from filename or document head (best effort, nullable).
20
+ _DATE_RE = re.compile(r"(\d{4})[./-](\d{1,2})[./-](\d{1,2})") # 2026-07-05
21
+ _DATE_RE_ALT = re.compile(r"(\d{1,2})[./-](\d{1,2})[./-](\d{2,4})") # 05/07/2026
22
+
23
+
24
+ def _utcnow() -> dt.datetime:
25
+ return dt.datetime.now(dt.timezone.utc)
26
+
27
+
28
+ def _chunk_text(text: str) -> list[str]:
29
+ text = text.strip()
30
+ if not text:
31
+ return []
32
+ out: list[str] = []
33
+ start = 0
34
+ n = len(text)
35
+ while start < n:
36
+ end = min(start + CHUNK_CHARS, n)
37
+ out.append(text[start:end])
38
+ if end == n:
39
+ break
40
+ start = end - CHUNK_OVERLAP
41
+ return out
42
+
43
+
44
+ def _parse_metadata(name: str, text: str) -> dict:
45
+ """Best-effort title / date / collection extraction. All fields nullable.
46
+
47
+ Generalize this for your domain: `collection` is a free-form grouping tag
48
+ (folder, series, category), `date` is an ISO date if one can be found.
49
+ """
50
+ title = os.path.splitext(name)[0].strip()
51
+ # Collection tag = the leading non-numeric words of the title.
52
+ collection = re.sub(r"[\d].*$", "", title).strip() or None
53
+
54
+ date_iso = None
55
+ haystack = f"{name}\n{text[:2000]}"
56
+ m = _DATE_RE.search(haystack)
57
+ if m:
58
+ y, mo, d = m.groups()
59
+ try:
60
+ date_iso = dt.datetime(int(y), int(mo), int(d)).date().isoformat()
61
+ except ValueError:
62
+ date_iso = None
63
+ if date_iso is None:
64
+ m = _DATE_RE_ALT.search(haystack)
65
+ if m:
66
+ d, mo, y = m.groups()
67
+ if len(y) == 2:
68
+ y = "20" + y
69
+ try:
70
+ date_iso = dt.datetime(int(y), int(mo), int(d)).date().isoformat()
71
+ except ValueError:
72
+ date_iso = None
73
+
74
+ return {"title": title, "collection": collection, "date": date_iso}
75
+
76
+
77
+ def run_sync(triggered_by: str = "manual") -> dict:
78
+ """Run one incremental sync. Never raises on missing config — returns a report."""
79
+ started = _utcnow()
80
+ report: dict = {
81
+ "started_at": started,
82
+ "triggered_by": triggered_by,
83
+ "files": [],
84
+ "status": "ok",
85
+ "note": None,
86
+ }
87
+
88
+ # Mongo is required for indexing (and always injected). Fail loud only here.
89
+ db.ensure_indexes()
90
+
91
+ if not config.source_configured():
92
+ report["status"] = "skipped"
93
+ report["note"] = (
94
+ f"Source not configured (SOURCE_TYPE={config.source_type()!r} — "
95
+ "missing its secrets/config)."
96
+ )
97
+ _save_report(report)
98
+ return report
99
+
100
+ try:
101
+ src = source.get_source()
102
+ files = src.list_files()
103
+ except Exception as exc: # noqa: BLE001 - resilient by design
104
+ report["status"] = "error"
105
+ report["note"] = f"Source listing failed: {exc}"
106
+ _save_report(report)
107
+ return report
108
+
109
+ embeddings_ok = config.embeddings_configured()
110
+ if not embeddings_ok:
111
+ report["note"] = (
112
+ "Embeddings not configured — documents indexed for lexical/metadata "
113
+ "search only (no semantic vectors)."
114
+ )
115
+
116
+ for f in files:
117
+ outcome = _process_file(src, f, embeddings_ok)
118
+ report["files"].append(outcome)
119
+
120
+ report["finished_at"] = _utcnow()
121
+ _save_report(report)
122
+ return report
123
+
124
+
125
+ def _process_file(src: source.Source, f: source.SourceFile, embeddings_ok: bool) -> dict:
126
+ existing = db.documents().find_one({"source_filename": f.name})
127
+ if existing and existing.get("source_rev") == f.rev:
128
+ return {"file": f.name, "result": "unchanged"}
129
+
130
+ if f.is_audio and not f.is_text:
131
+ # Catalogue audio/media with a flag; do not search inside it.
132
+ doc = {
133
+ "title": os.path.splitext(f.name)[0],
134
+ "source_filename": f.name,
135
+ "source_rev": f.rev,
136
+ "file_path": f.path,
137
+ "collection": None,
138
+ "date": None,
139
+ "has_media": True,
140
+ "media_path": f.path,
141
+ "word_count": 0,
142
+ "indexed_at": _utcnow(),
143
+ }
144
+ doc_id = _upsert_document(doc)
145
+ db.chunks().delete_many({"document_id": doc_id})
146
+ return {"file": f.name, "result": "catalogued_media"}
147
+
148
+ if not f.is_text:
149
+ return {"file": f.name, "result": "skipped_unsupported"}
150
+
151
+ try:
152
+ raw = src.download(f.path)
153
+ text = source.extract_text(f.name, raw)
154
+ except Exception as exc: # noqa: BLE001
155
+ return {"file": f.name, "result": "error", "detail": str(exc)}
156
+
157
+ meta = _parse_metadata(f.name, text)
158
+ doc = {
159
+ **meta,
160
+ "source_filename": f.name,
161
+ "source_rev": f.rev,
162
+ "file_path": f.path,
163
+ "has_media": False,
164
+ "media_path": None,
165
+ "word_count": len(text.split()),
166
+ "indexed_at": _utcnow(),
167
+ }
168
+ doc_id = _upsert_document(doc)
169
+
170
+ # Rebuild chunks for this document.
171
+ db.chunks().delete_many({"document_id": doc_id})
172
+ parts = _chunk_text(text)
173
+ vectors: list[list[float] | None] = [None] * len(parts)
174
+ if embeddings_ok and parts:
175
+ try:
176
+ vectors = embeddings.embed(parts) # type: ignore[assignment]
177
+ except Exception as exc: # noqa: BLE001
178
+ return {
179
+ "file": f.name,
180
+ "result": "indexed_no_embeddings",
181
+ "detail": f"embedding failed: {exc}",
182
+ "chunks": len(parts),
183
+ }
184
+
185
+ docs = []
186
+ for i, content in enumerate(parts):
187
+ docs.append({
188
+ "document_id": doc_id,
189
+ "chunk_index": i,
190
+ "content": content,
191
+ "embedding": vectors[i] if i < len(vectors) else None,
192
+ "token_count": len(content.split()),
193
+ })
194
+ if docs:
195
+ db.chunks().insert_many(docs)
196
+
197
+ return {
198
+ "file": f.name,
199
+ "result": "indexed" if embeddings_ok else "indexed_no_embeddings",
200
+ "chunks": len(docs),
201
+ }
202
+
203
+
204
+ def _upsert_document(doc: dict):
205
+ db.documents().update_one(
206
+ {"source_filename": doc["source_filename"]},
207
+ {"$set": doc},
208
+ upsert=True,
209
+ )
210
+ return db.documents().find_one({"source_filename": doc["source_filename"]})["_id"]
211
+
212
+
213
+ def _save_report(report: dict) -> None:
214
+ try:
215
+ db.index_reports().insert_one(dict(report))
216
+ db.settings().update_one(
217
+ {"_id": "catalog"},
218
+ {"$set": {"last_refresh": report.get("finished_at") or report["started_at"]}},
219
+ upsert=True,
220
+ )
221
+ except Exception: # noqa: BLE001 - reporting must never crash the caller
222
+ pass
@@ -0,0 +1,259 @@
1
+ """FastAPI backend for the semantic-search document app.
2
+
3
+ Mounted at /backend by CloudGrid. Public search + answer endpoints, plus
4
+ manager-only admin (Settings, Usage Dashboard, Refresh now) enforced server-side.
5
+
6
+ Health is green as soon as Mongo connects — it does NOT require the document
7
+ source or the embeddings key. No secret is read at import time.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import datetime as dt
12
+
13
+ from fastapi import Depends, FastAPI
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from pydantic import BaseModel
16
+
17
+ from . import answer, auth, config, db, indexing, search
18
+
19
+ app = FastAPI(title="Semantic Search API")
20
+
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"], # frontend is served same-origin; open for safety
24
+ allow_methods=["*"],
25
+ allow_headers=["*"],
26
+ )
27
+
28
+
29
+ @app.on_event("startup")
30
+ def _ensure_indexes_on_startup() -> None:
31
+ # Create the chunks $text index + documents indexes at boot so $text search
32
+ # returns [] on an empty catalog instead of erroring (IndexNotFound / 500).
33
+ # Never block startup if Mongo is briefly unavailable — an index run recreates
34
+ # them.
35
+ try:
36
+ db.ensure_indexes()
37
+ except Exception as e: # noqa: BLE001
38
+ print(f"startup: ensure_indexes skipped ({e})")
39
+
40
+
41
+ # ── Health ────────────────────────────────────────────────────────────────────
42
+
43
+ @app.get("/backend/health")
44
+ def health():
45
+ """Green when Mongo connects. Independent of the source / embeddings key."""
46
+ try:
47
+ db.ping()
48
+ return {"status": "ok"}
49
+ except Exception as exc: # noqa: BLE001
50
+ return {"status": "degraded", "detail": str(exc)}
51
+
52
+
53
+ @app.get("/backend/status")
54
+ def status():
55
+ """Public capability snapshot — powers the frontend's 'not indexed / missing
56
+ secret' states without exposing any secret value."""
57
+ try:
58
+ db.ping()
59
+ mongo_ok = True
60
+ except Exception: # noqa: BLE001
61
+ mongo_ok = False
62
+ settings_doc = db.settings().find_one({"_id": "catalog"}) if mongo_ok else None
63
+ features = db.settings().find_one({"_id": "features"}) if mongo_ok else None
64
+ return {
65
+ "mongo": mongo_ok,
66
+ "source_type": config.source_type(),
67
+ "source_configured": config.source_configured(),
68
+ "embeddings_configured": config.embeddings_configured(),
69
+ "manager_auth_configured": config.manager_auth_configured(),
70
+ "answer_mode_enabled": bool((features or {}).get("answer_mode_enabled", True)),
71
+ "indexed_documents": db.documents().count_documents({}) if mongo_ok else 0,
72
+ "last_refresh": (settings_doc or {}).get("last_refresh"),
73
+ }
74
+
75
+
76
+ # ── Search (public) ───────────────────────────────────────────────────────────
77
+
78
+ @app.get("/backend/search")
79
+ def do_search(q: str = "", mode: str = "hybrid", collection: str | None = None,
80
+ date_from: str | None = None, date_to: str | None = None,
81
+ limit: int = 20):
82
+ result = search.search(q, mode=mode, collection=collection,
83
+ date_from=date_from, date_to=date_to, limit=limit)
84
+ _log_event("search", {"q": q, "mode": mode,
85
+ "zero_result": len(result["results"]) == 0})
86
+ return result
87
+
88
+
89
+ @app.get("/backend/count")
90
+ def do_count(collection: str | None = None, date_from: str | None = None,
91
+ date_to: str | None = None):
92
+ """Counting answered directly from the DB (countDocuments)."""
93
+ return {"count": search.count_documents(collection, date_from, date_to)}
94
+
95
+
96
+ @app.get("/backend/collections")
97
+ def list_collections():
98
+ values = [c for c in db.documents().distinct("collection") if c]
99
+ return {"collections": sorted(values)}
100
+
101
+
102
+ class AnswerRequest(BaseModel):
103
+ question: str
104
+
105
+
106
+ @app.post("/backend/answer")
107
+ def do_answer(req: AnswerRequest):
108
+ _log_event("answer", {"q": req.question})
109
+ return answer.compose_answer(req.question)
110
+
111
+
112
+ @app.get("/backend/documents/{document_id}/download")
113
+ def download_document(document_id: str):
114
+ """Stream a document's text through the service — source paths never exposed."""
115
+ from bson import ObjectId
116
+ from fastapi import HTTPException
117
+ from fastapi.responses import PlainTextResponse
118
+
119
+ try:
120
+ doc = db.documents().find_one({"_id": ObjectId(document_id)})
121
+ except Exception: # noqa: BLE001
122
+ raise HTTPException(status_code=404, detail="Not found.")
123
+ if not doc:
124
+ raise HTTPException(status_code=404, detail="Not found.")
125
+ parts = db.chunks().find({"document_id": doc["_id"]}).sort("chunk_index", 1)
126
+ text = "".join(p["content"] for p in parts)
127
+ return PlainTextResponse(text, headers={
128
+ "Content-Disposition": f'attachment; filename="{doc.get("title","document")}.txt"'
129
+ })
130
+
131
+
132
+ # ── Manager auth ──────────────────────────────────────────────────────────────
133
+
134
+ class LoginRequest(BaseModel):
135
+ password: str
136
+
137
+
138
+ @app.post("/backend/admin/login")
139
+ def admin_login(req: LoginRequest):
140
+ token = auth.login(req.password)
141
+ return {"token": token}
142
+
143
+
144
+ # ── Manager: Settings ─────────────────────────────────────────────────────────
145
+
146
+ @app.get("/backend/admin/settings", dependencies=[Depends(auth.require_manager)])
147
+ def get_settings():
148
+ features = db.settings().find_one({"_id": "features"}) or {}
149
+ synonyms = db.settings().find_one({"_id": "synonyms"}) or {}
150
+ catalog = db.settings().find_one({"_id": "catalog"}) or {}
151
+ return {
152
+ "answer_mode_enabled": bool(features.get("answer_mode_enabled", True)),
153
+ "synonyms": synonyms.get("map", {}),
154
+ "last_refresh": catalog.get("last_refresh"),
155
+ }
156
+
157
+
158
+ class FeatureUpdate(BaseModel):
159
+ answer_mode_enabled: bool | None = None
160
+
161
+
162
+ @app.put("/backend/admin/settings", dependencies=[Depends(auth.require_manager)])
163
+ def update_settings(req: FeatureUpdate):
164
+ update = {k: v for k, v in req.dict().items() if v is not None}
165
+ if update:
166
+ db.settings().update_one({"_id": "features"}, {"$set": update}, upsert=True)
167
+ return {"ok": True}
168
+
169
+
170
+ class SynonymUpdate(BaseModel):
171
+ map: dict[str, list[str]]
172
+
173
+
174
+ @app.put("/backend/admin/synonyms", dependencies=[Depends(auth.require_manager)])
175
+ def update_synonyms(req: SynonymUpdate):
176
+ db.settings().update_one({"_id": "synonyms"}, {"$set": {"map": req.map}}, upsert=True)
177
+ return {"ok": True}
178
+
179
+
180
+ class PasswordChange(BaseModel):
181
+ current: str
182
+ new: str
183
+
184
+
185
+ @app.post("/backend/admin/password", dependencies=[Depends(auth.require_manager)])
186
+ def change_password(req: PasswordChange):
187
+ new_hash = auth.change_password(req.current, req.new)
188
+ # Return the hash for the manager to store as MANAGER_PASSWORD_HASH.
189
+ return {
190
+ "new_hash": new_hash,
191
+ "note": "Store this as the MANAGER_PASSWORD_HASH secret "
192
+ "(`grid secrets set MANAGER_PASSWORD_HASH=<hash>`) to persist it.",
193
+ }
194
+
195
+
196
+ @app.get("/backend/admin/index-report", dependencies=[Depends(auth.require_manager)])
197
+ def index_report():
198
+ reports = list(db.index_reports().find().sort("started_at", -1).limit(10))
199
+ for r in reports:
200
+ r["_id"] = str(r["_id"])
201
+ return {"reports": reports}
202
+
203
+
204
+ @app.post("/backend/admin/refresh", dependencies=[Depends(auth.require_manager)])
205
+ def refresh_now():
206
+ """Manager-only on-demand incremental sync.
207
+
208
+ This is the supported refresh path today. A scheduled `type: cron` refresh
209
+ service is a follow-up (blocked on platform issue #1585); see AGENTS.md.
210
+ """
211
+ report = indexing.run_sync(triggered_by="manager")
212
+ report.pop("_id", None)
213
+ return report
214
+
215
+
216
+ # ── Manager: Usage Dashboard ──────────────────────────────────────────────────
217
+
218
+ @app.get("/backend/admin/usage", dependencies=[Depends(auth.require_manager)])
219
+ def usage():
220
+ ev = db.events()
221
+ total_searches = ev.count_documents({"kind": "search"})
222
+ by_mode = list(ev.aggregate([
223
+ {"$match": {"kind": "search"}},
224
+ {"$group": {"_id": "$meta.mode", "count": {"$sum": 1}}},
225
+ ]))
226
+ top_queries = list(ev.aggregate([
227
+ {"$match": {"kind": "search", "meta.q": {"$ne": ""}}},
228
+ {"$group": {"_id": "$meta.q", "count": {"$sum": 1}}},
229
+ {"$sort": {"count": -1}}, {"$limit": 10},
230
+ ]))
231
+ zero_result = list(ev.aggregate([
232
+ {"$match": {"kind": "search", "meta.zero_result": True}},
233
+ {"$group": {"_id": "$meta.q", "count": {"$sum": 1}}},
234
+ {"$sort": {"count": -1}}, {"$limit": 10},
235
+ ]))
236
+ unique_users = len(ev.distinct("anon_id", {"kind": "search"}))
237
+ return {
238
+ "total_searches": total_searches,
239
+ "unique_users": unique_users,
240
+ "by_mode": {row["_id"] or "unknown": row["count"] for row in by_mode},
241
+ "top_queries": [{"query": r["_id"], "count": r["count"]} for r in top_queries],
242
+ "zero_result_queries": [{"query": r["_id"], "count": r["count"]} for r in zero_result],
243
+ "indexed_documents": db.documents().count_documents({}),
244
+ "last_refresh": (db.settings().find_one({"_id": "catalog"}) or {}).get("last_refresh"),
245
+ }
246
+
247
+
248
+ # ── internal ──────────────────────────────────────────────────────────────────
249
+
250
+ def _log_event(kind: str, meta: dict) -> None:
251
+ try:
252
+ db.events().insert_one({
253
+ "kind": kind,
254
+ "meta": meta,
255
+ "anon_id": meta.get("anon_id", "anon"),
256
+ "at": dt.datetime.now(dt.timezone.utc),
257
+ })
258
+ except Exception: # noqa: BLE001 - telemetry must never break a request
259
+ pass