@k2b/cloud 0.6.0-rc.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 (540) hide show
  1. package/README.md +98 -0
  2. package/package.json +128 -0
  3. package/public/logo.svg +1 -0
  4. package/scripts/app-favicon.test.ts +49 -0
  5. package/scripts/app-favicon.ts +51 -0
  6. package/scripts/build.ts +205 -0
  7. package/scripts/preload.ts +164 -0
  8. package/scripts/sync-recovery-smoke.ts +200 -0
  9. package/src/_internal/app-favicon.ts +2 -0
  10. package/src/_internal/app-presentation.ts +61 -0
  11. package/src/_internal/app-workspace-state.ts +24 -0
  12. package/src/_internal/bounded-json.ts +47 -0
  13. package/src/_internal/build-metadata.ts +9 -0
  14. package/src/_internal/capabilities.ts +1118 -0
  15. package/src/_internal/define-app.ts +801 -0
  16. package/src/_internal/heartbeat.ts +167 -0
  17. package/src/_internal/help-catalog.ts +241 -0
  18. package/src/_internal/help.ts +95 -0
  19. package/src/_internal/nats-connection.ts +34 -0
  20. package/src/_internal/page-responses.ts +40 -0
  21. package/src/_internal/process-sync.ts +117 -0
  22. package/src/_internal/readiness.ts +3 -0
  23. package/src/_internal/registry-validation.ts +136 -0
  24. package/src/_internal/registry-watch.ts +52 -0
  25. package/src/_internal/registry.ts +338 -0
  26. package/src/_internal/runtime-compatibility.ts +64 -0
  27. package/src/_internal/runtime-context.ts +84 -0
  28. package/src/_internal/runtime-watcher.ts +78 -0
  29. package/src/_internal/static-assets.ts +86 -0
  30. package/src/_internal/status-preserving-ssr.ts +49 -0
  31. package/src/access/PermissionEditor.tsx +486 -0
  32. package/src/access/ResourceApiKeys.tsx +271 -0
  33. package/src/access/messages.ts +121 -0
  34. package/src/access/ui.ts +16 -0
  35. package/src/account/Avatar.tsx +28 -0
  36. package/src/account/EntitySearch.tsx +363 -0
  37. package/src/account/avatar-upload-dialog.tsx +197 -0
  38. package/src/account/avatar-upload.ts +149 -0
  39. package/src/account/ui.ts +3 -0
  40. package/src/ai/admin.ts +47 -0
  41. package/src/ai/approval-routes.ts +130 -0
  42. package/src/ai/approvals.ts +139 -0
  43. package/src/ai/assistant-models.ts +61 -0
  44. package/src/ai/attachments.ts +40 -0
  45. package/src/ai/browser.ts +92 -0
  46. package/src/ai/capabilities.ts +714 -0
  47. package/src/ai/capability-execution.ts +151 -0
  48. package/src/ai/chat/blocks.render.test.tsx +1108 -0
  49. package/src/ai/chat/blocks.tsx +730 -0
  50. package/src/ai/chat/builtin-tools.tsx +428 -0
  51. package/src/ai/chat/capability-table.tsx +87 -0
  52. package/src/ai/chat/composer-adapter.ts +169 -0
  53. package/src/ai/chat/file-tools.tsx +81 -0
  54. package/src/ai/chat/message-actions.tsx +370 -0
  55. package/src/ai/chat/message-utils.ts +479 -0
  56. package/src/ai/chat/messages.ts +43 -0
  57. package/src/ai/chat/presentation.tsx +395 -0
  58. package/src/ai/chat/primitives.tsx +3 -0
  59. package/src/ai/chat/tool-disclosure.tsx +41 -0
  60. package/src/ai/chat/turn-navigator-utils.ts +28 -0
  61. package/src/ai/chat/turn-navigator.tsx +164 -0
  62. package/src/ai/chat/user-message.tsx +149 -0
  63. package/src/ai/chat/visual-tools.tsx +435 -0
  64. package/src/ai/chat/web-tools.tsx +202 -0
  65. package/src/ai/chat-task-contracts.ts +113 -0
  66. package/src/ai/chat-tasks.ts +952 -0
  67. package/src/ai/client/controller.ts +1211 -0
  68. package/src/ai/client/file-source.ts +95 -0
  69. package/src/ai/client/live-connection.ts +140 -0
  70. package/src/ai/client/projection.ts +178 -0
  71. package/src/ai/client/transport.ts +111 -0
  72. package/src/ai/compaction.ts +182 -0
  73. package/src/ai/credentials.ts +124 -0
  74. package/src/ai/default-tool-contracts.ts +108 -0
  75. package/src/ai/default-tools.ts +127 -0
  76. package/src/ai/enrich.ts +280 -0
  77. package/src/ai/executor.ts +1624 -0
  78. package/src/ai/fetch-file-tool.ts +313 -0
  79. package/src/ai/file-context.ts +92 -0
  80. package/src/ai/file-media-type.ts +28 -0
  81. package/src/ai/file-mount.ts +16 -0
  82. package/src/ai/file-tools.ts +566 -0
  83. package/src/ai/files-store.ts +390 -0
  84. package/src/ai/firecrawl-tools.ts +305 -0
  85. package/src/ai/grids-skill.ts +126 -0
  86. package/src/ai/http.ts +265 -0
  87. package/src/ai/index.ts +337 -0
  88. package/src/ai/kit-skill.ts +22 -0
  89. package/src/ai/limits.ts +5 -0
  90. package/src/ai/live-events.ts +289 -0
  91. package/src/ai/live-messages.ts +45 -0
  92. package/src/ai/live-outbox.ts +102 -0
  93. package/src/ai/live-routes.ts +412 -0
  94. package/src/ai/live.ts +2 -0
  95. package/src/ai/maintenance.ts +302 -0
  96. package/src/ai/markdown-pdf-tool.ts +95 -0
  97. package/src/ai/memories.ts +605 -0
  98. package/src/ai/memory-learning-runs.ts +222 -0
  99. package/src/ai/memory-learning.ts +619 -0
  100. package/src/ai/memory-tool.ts +120 -0
  101. package/src/ai/memory-workflow-evidence.ts +158 -0
  102. package/src/ai/migrate.ts +2082 -0
  103. package/src/ai/model-access-migrate.ts +30 -0
  104. package/src/ai/model-access.ts +191 -0
  105. package/src/ai/personal-agent.ts +6 -0
  106. package/src/ai/prefs.ts +72 -0
  107. package/src/ai/project-tool.ts +94 -0
  108. package/src/ai/projects-routes.ts +277 -0
  109. package/src/ai/projects.ts +902 -0
  110. package/src/ai/protocol.ts +279 -0
  111. package/src/ai/provider.ts +73 -0
  112. package/src/ai/public-projection.ts +25 -0
  113. package/src/ai/resource-markers.ts +31 -0
  114. package/src/ai/resource-refs.ts +70 -0
  115. package/src/ai/routes.ts +1131 -0
  116. package/src/ai/runtime-tools.ts +39 -0
  117. package/src/ai/runtime.ts +544 -0
  118. package/src/ai/settings.ts +529 -0
  119. package/src/ai/short-id.ts +45 -0
  120. package/src/ai/skill-catalog.ts +73 -0
  121. package/src/ai/skill-format.ts +145 -0
  122. package/src/ai/skill-seeds.ts +430 -0
  123. package/src/ai/skill-tool.ts +81 -0
  124. package/src/ai/skills-routes.ts +143 -0
  125. package/src/ai/skills.ts +934 -0
  126. package/src/ai/solid.ts +27 -0
  127. package/src/ai/store.ts +3113 -0
  128. package/src/ai/stream.ts +308 -0
  129. package/src/ai/structured-runs.ts +66 -0
  130. package/src/ai/structured.ts +186 -0
  131. package/src/ai/system-prompt.ts +170 -0
  132. package/src/ai/task-prompt.ts +22 -0
  133. package/src/ai/timeline.ts +148 -0
  134. package/src/ai/tool-audit.ts +177 -0
  135. package/src/ai/tool-result-budget.ts +16 -0
  136. package/src/ai/tools.ts +155 -0
  137. package/src/ai/types.ts +967 -0
  138. package/src/ai/ui.tsx +39 -0
  139. package/src/ai/usage-migrate.ts +81 -0
  140. package/src/ai/usage.ts +315 -0
  141. package/src/ai/validate.ts +54 -0
  142. package/src/ai/vision-tool.ts +79 -0
  143. package/src/api/accounts-entities.ts +174 -0
  144. package/src/api/admin-ai-projects.ts +74 -0
  145. package/src/api/admin-ai-skills.ts +76 -0
  146. package/src/api/admin-ai-usage.ts +42 -0
  147. package/src/api/admin-core-settings.ts +508 -0
  148. package/src/api/admin-identity.ts +148 -0
  149. package/src/api/admin-lifecycle.ts +206 -0
  150. package/src/api/admin-linux-identities.ts +66 -0
  151. package/src/api/announcements.ts +131 -0
  152. package/src/api/app-approval.ts +129 -0
  153. package/src/api/apps.ts +51 -0
  154. package/src/api/auth/schemas.ts +54 -0
  155. package/src/api/auth.ts +370 -0
  156. package/src/api/capabilities.ts +658 -0
  157. package/src/api/help-messages.ts +19 -0
  158. package/src/api/help.ts +78 -0
  159. package/src/api/index.ts +108 -0
  160. package/src/api/mcp.ts +744 -0
  161. package/src/api/me-rail.ts +45 -0
  162. package/src/api/me.ts +753 -0
  163. package/src/api/search/schemas.ts +79 -0
  164. package/src/api/search.bench.ts +185 -0
  165. package/src/api/search.ts +407 -0
  166. package/src/api/sync-ops.ts +79 -0
  167. package/src/api/widgets.ts +143 -0
  168. package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
  169. package/src/browser/CloudResourceSearch.tsx +791 -0
  170. package/src/browser/app-approval-vault.ts +305 -0
  171. package/src/browser/app-approval.ts +234 -0
  172. package/src/browser/live-websocket.ts +268 -0
  173. package/src/browser/locale-preference.ts +25 -0
  174. package/src/browser/notification-target.ts +27 -0
  175. package/src/browser/notification-timeout.ts +14 -0
  176. package/src/browser/notifications.ts +181 -0
  177. package/src/browser/resource-clipboard.ts +70 -0
  178. package/src/browser/resource-picker.tsx +39 -0
  179. package/src/browser/resource-search-messages.ts +55 -0
  180. package/src/browser/resource-search.ts +27 -0
  181. package/src/capabilities/client.ts +149 -0
  182. package/src/capabilities/response.ts +39 -0
  183. package/src/capabilities/server.ts +215 -0
  184. package/src/capabilities/signals.ts +4 -0
  185. package/src/capabilities/testing.ts +12 -0
  186. package/src/capabilities/types.ts +32 -0
  187. package/src/cli/access.ts +499 -0
  188. package/src/cli/account.ts +382 -0
  189. package/src/cli/admin/account-administration.ts +45 -0
  190. package/src/cli/admin/account-categories.ts +49 -0
  191. package/src/cli/admin/ai-usage.ts +118 -0
  192. package/src/cli/admin/app-sign-in.ts +49 -0
  193. package/src/cli/admin/data.ts +343 -0
  194. package/src/cli/admin/documentation.ts +34 -0
  195. package/src/cli/admin/gateway.ts +155 -0
  196. package/src/cli/admin/index.ts +61 -0
  197. package/src/cli/admin/instance.ts +250 -0
  198. package/src/cli/admin/jobs.ts +223 -0
  199. package/src/cli/admin/legal.ts +122 -0
  200. package/src/cli/admin/linux.ts +53 -0
  201. package/src/cli/admin/logs.ts +324 -0
  202. package/src/cli/admin/metrics.ts +142 -0
  203. package/src/cli/admin/nats.ts +167 -0
  204. package/src/cli/admin/notifications.ts +516 -0
  205. package/src/cli/admin/shared.ts +113 -0
  206. package/src/cli/admin/sync.ts +188 -0
  207. package/src/cli/admin/telemetry.ts +314 -0
  208. package/src/cli/admin/webhooks.ts +222 -0
  209. package/src/cli/admin/workflows.ts +367 -0
  210. package/src/cli/apps.ts +40 -0
  211. package/src/cli/capabilities.ts +239 -0
  212. package/src/cli/commands.ts +604 -0
  213. package/src/cli/index.ts +61 -0
  214. package/src/cli/locale.ts +31 -0
  215. package/src/cli/output.ts +58 -0
  216. package/src/clients/core.ts +20 -0
  217. package/src/config/env.ts +54 -0
  218. package/src/config/index.ts +6 -0
  219. package/src/contracts/account-categories.ts +62 -0
  220. package/src/contracts/announcements.ts +121 -0
  221. package/src/contracts/app-approval.ts +197 -0
  222. package/src/contracts/app.ts +162 -0
  223. package/src/contracts/capabilities.ts +484 -0
  224. package/src/contracts/index.ts +14 -0
  225. package/src/contracts/notification-live.ts +66 -0
  226. package/src/contracts/notification-types.ts +235 -0
  227. package/src/contracts/posix.ts +69 -0
  228. package/src/contracts/profile.ts +85 -0
  229. package/src/contracts/rail-preferences.ts +53 -0
  230. package/src/contracts/registry.ts +110 -0
  231. package/src/contracts/resource-clipboard.ts +33 -0
  232. package/src/contracts/settings-types.ts +98 -0
  233. package/src/contracts/shared.ts +408 -0
  234. package/src/contracts/user-notifications.ts +101 -0
  235. package/src/contracts/widgets.ts +183 -0
  236. package/src/desktop/index.ts +704 -0
  237. package/src/desktop/solid.tsx +938 -0
  238. package/src/index.ts +58 -0
  239. package/src/server/actor.ts +41 -0
  240. package/src/server/api/index.ts +2 -0
  241. package/src/server/api/respond.ts +96 -0
  242. package/src/server/api-client.ts +54 -0
  243. package/src/server/app-context.ts +39 -0
  244. package/src/server/help.ts +187 -0
  245. package/src/server/index.ts +89 -0
  246. package/src/server/locale.ts +55 -0
  247. package/src/server/middleware/auth.ts +386 -0
  248. package/src/server/middleware/index.ts +28 -0
  249. package/src/server/middleware/invocation.ts +55 -0
  250. package/src/server/middleware/middleware.ts +65 -0
  251. package/src/server/middleware/openapi.ts +126 -0
  252. package/src/server/middleware/rate-limit.ts +125 -0
  253. package/src/server/middleware/request-logger.ts +41 -0
  254. package/src/server/middleware/route-template.ts +46 -0
  255. package/src/server/middleware/runtime.ts +22 -0
  256. package/src/server/middleware/settings.ts +55 -0
  257. package/src/server/middleware/validator.ts +49 -0
  258. package/src/server/middleware/workload.ts +22 -0
  259. package/src/server/ratelimit.ts +151 -0
  260. package/src/server/request-cookies.ts +18 -0
  261. package/src/server/services/access.ts +644 -0
  262. package/src/server/services/freeipa/client.ts +137 -0
  263. package/src/server/services/freeipa/index.ts +9 -0
  264. package/src/server/services/freeipa/session.ts +123 -0
  265. package/src/server/services/freeipa/test-certificates.ts +44 -0
  266. package/src/server/services/freeipa/tls.ts +44 -0
  267. package/src/server/services/freeipa/transport.ts +116 -0
  268. package/src/server/services/freeipa/util.ts +59 -0
  269. package/src/server/services/geo.ts +198 -0
  270. package/src/server/services/index.ts +48 -0
  271. package/src/server/services/pagination.ts +22 -0
  272. package/src/server/services/services.ts +13 -0
  273. package/src/server/time.ts +33 -0
  274. package/src/services/account-category-policy.ts +51 -0
  275. package/src/services/account-lifecycle/audit.ts +41 -0
  276. package/src/services/account-lifecycle/index.ts +940 -0
  277. package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
  278. package/src/services/account-lifecycle/notification-sender.ts +13 -0
  279. package/src/services/account-lifecycle/scheduler.ts +506 -0
  280. package/src/services/account-model.ts +22 -0
  281. package/src/services/accounts/app.ts +1671 -0
  282. package/src/services/accounts/authz.ts +44 -0
  283. package/src/services/accounts/avatar.ts +120 -0
  284. package/src/services/accounts/base-group.ts +11 -0
  285. package/src/services/accounts/base-user.ts +46 -0
  286. package/src/services/accounts/duplicate-emails.ts +82 -0
  287. package/src/services/accounts/entities.ts +622 -0
  288. package/src/services/accounts/group-sql.ts +106 -0
  289. package/src/services/accounts/groups.ts +272 -0
  290. package/src/services/accounts/index.ts +14 -0
  291. package/src/services/accounts/ipa-data.ts +66 -0
  292. package/src/services/accounts/lifecycle.ts +2 -0
  293. package/src/services/accounts/local-groups.ts +499 -0
  294. package/src/services/accounts/model.ts +129 -0
  295. package/src/services/accounts/notification-sender.ts +31 -0
  296. package/src/services/accounts/posix.ts +386 -0
  297. package/src/services/accounts/request-policy.ts +11 -0
  298. package/src/services/accounts/switching.ts +126 -0
  299. package/src/services/accounts/users.ts +745 -0
  300. package/src/services/announcements/index.ts +224 -0
  301. package/src/services/app-approval.ts +498 -0
  302. package/src/services/audit/index.ts +438 -0
  303. package/src/services/auth-flows/index.ts +15 -0
  304. package/src/services/auth-flows/ipa.ts +197 -0
  305. package/src/services/auth-flows/magic-link.ts +185 -0
  306. package/src/services/auth-flows/notification-sender.ts +10 -0
  307. package/src/services/auth-flows/password-reset.ts +268 -0
  308. package/src/services/auth-flows/proxy-return.ts +49 -0
  309. package/src/services/document-extraction.ts +148 -0
  310. package/src/services/freeipa-config.ts +179 -0
  311. package/src/services/gateway.ts +186 -0
  312. package/src/services/identity/constants.ts +18 -0
  313. package/src/services/identity/index.ts +52 -0
  314. package/src/services/identity/invocation-actor.ts +120 -0
  315. package/src/services/identity/invocation-authority.ts +53 -0
  316. package/src/services/identity/invocation-operations.ts +15 -0
  317. package/src/services/identity/invocation-token.ts +276 -0
  318. package/src/services/identity/key-config.ts +41 -0
  319. package/src/services/identity/key-ring.ts +530 -0
  320. package/src/services/identity/metrics.ts +29 -0
  321. package/src/services/identity/routes.ts +18 -0
  322. package/src/services/identity/runtime-config.ts +88 -0
  323. package/src/services/identity/session-token.ts +152 -0
  324. package/src/services/identity/signer-refresh.ts +44 -0
  325. package/src/services/identity/workload-auth.ts +64 -0
  326. package/src/services/index.ts +187 -0
  327. package/src/services/ipa/auth.ts +216 -0
  328. package/src/services/ipa/connection.ts +57 -0
  329. package/src/services/ipa/effective-groups.ts +70 -0
  330. package/src/services/ipa/groups.ts +722 -0
  331. package/src/services/ipa/guard.ts +17 -0
  332. package/src/services/ipa/index.ts +17 -0
  333. package/src/services/ipa/posix.ts +19 -0
  334. package/src/services/ipa/profile.ts +132 -0
  335. package/src/services/ipa/runtime.ts +28 -0
  336. package/src/services/ipa/search.ts +154 -0
  337. package/src/services/ipa/service-account.ts +15 -0
  338. package/src/services/ipa/sync-planning.ts +100 -0
  339. package/src/services/ipa/sync.ts +862 -0
  340. package/src/services/ipa/users.ts +806 -0
  341. package/src/services/legal-consent.ts +76 -0
  342. package/src/services/logging/index.ts +477 -0
  343. package/src/services/logging/redaction.ts +20 -0
  344. package/src/services/logging/trace.ts +1231 -0
  345. package/src/services/mandates/index.ts +855 -0
  346. package/src/services/mandates/policy.ts +85 -0
  347. package/src/services/network-security.ts +31 -0
  348. package/src/services/notifications/batches.ts +722 -0
  349. package/src/services/notifications/browser.ts +252 -0
  350. package/src/services/notifications/catalog.ts +113 -0
  351. package/src/services/notifications/channels.ts +104 -0
  352. package/src/services/notifications/dispatcher.ts +139 -0
  353. package/src/services/notifications/email.ts +118 -0
  354. package/src/services/notifications/index.ts +633 -0
  355. package/src/services/notifications/live.ts +50 -0
  356. package/src/services/notifications/observability.ts +407 -0
  357. package/src/services/notifications/platform.ts +421 -0
  358. package/src/services/notifications/runtime.ts +120 -0
  359. package/src/services/notifications/user.ts +248 -0
  360. package/src/services/notifications/web-push-transport.ts +135 -0
  361. package/src/services/oauth-tokens.ts +209 -0
  362. package/src/services/outbox.ts +187 -0
  363. package/src/services/pdf/gotenberg.ts +290 -0
  364. package/src/services/pdf/index.ts +43 -0
  365. package/src/services/pdf/markdown.ts +209 -0
  366. package/src/services/pdf/template-preview.ts +122 -0
  367. package/src/services/postgres.ts +76 -0
  368. package/src/services/providers/index.ts +27 -0
  369. package/src/services/providers/local/auth.ts +51 -0
  370. package/src/services/providers/local/index.ts +4 -0
  371. package/src/services/providers/local/users.ts +263 -0
  372. package/src/services/rail-preferences.ts +27 -0
  373. package/src/services/runtime-lifecycle.ts +191 -0
  374. package/src/services/secrets.ts +10 -0
  375. package/src/services/service-account-credentials.ts +891 -0
  376. package/src/services/service-accounts.ts +184 -0
  377. package/src/services/session/index.ts +236 -0
  378. package/src/services/session/test-fixture.ts +18 -0
  379. package/src/services/session/user.ts +144 -0
  380. package/src/services/settings/api.ts +61 -0
  381. package/src/services/settings/app.ts +86 -0
  382. package/src/services/settings/core-settings.ts +796 -0
  383. package/src/services/settings/crypto.ts +69 -0
  384. package/src/services/settings/defaults.ts +374 -0
  385. package/src/services/settings/index.ts +215 -0
  386. package/src/services/settings/namespace.ts +9 -0
  387. package/src/services/settings/snapshot.ts +49 -0
  388. package/src/services/settings/store.ts +241 -0
  389. package/src/services/settings/templates.ts +4 -0
  390. package/src/services/sync-ops.ts +403 -0
  391. package/src/services/topic-cursor.ts +22 -0
  392. package/src/services/weather/forecast.ts +323 -0
  393. package/src/services/weather/geo.ts +119 -0
  394. package/src/services/weather/index.ts +67 -0
  395. package/src/services/weather/location.ts +24 -0
  396. package/src/services/weather/locations.ts +133 -0
  397. package/src/services/weather/migrate.ts +45 -0
  398. package/src/services/weather/settings.ts +66 -0
  399. package/src/services/weather/short-id.ts +57 -0
  400. package/src/services/weather/types.ts +61 -0
  401. package/src/services/weather/ui.ts +50 -0
  402. package/src/services/webauthn.ts +385 -0
  403. package/src/shared/account-action-notice.ts +70 -0
  404. package/src/shared/account-display.ts +15 -0
  405. package/src/shared/account-session.ts +15 -0
  406. package/src/shared/ai-platform-prompt.ts +134 -0
  407. package/src/shared/ai-usage.ts +49 -0
  408. package/src/shared/app-presentation.ts +48 -0
  409. package/src/shared/app-url.ts +12 -0
  410. package/src/shared/branding.ts +14 -0
  411. package/src/shared/capability-messages.ts +70 -0
  412. package/src/shared/email-html.ts +54 -0
  413. package/src/shared/format.ts +91 -0
  414. package/src/shared/help.ts +68 -0
  415. package/src/shared/icons.ts +400 -0
  416. package/src/shared/index.ts +54 -0
  417. package/src/shared/locale.ts +48 -0
  418. package/src/shared/login-method.ts +44 -0
  419. package/src/shared/markdown/client.ts +130 -0
  420. package/src/shared/markdown/extensions/code.ts +105 -0
  421. package/src/shared/markdown/extensions/guided-help.ts +100 -0
  422. package/src/shared/markdown/extensions/images.ts +79 -0
  423. package/src/shared/markdown/extensions/info-blocks.ts +97 -0
  424. package/src/shared/markdown/extensions/katex.ts +120 -0
  425. package/src/shared/markdown/extensions/links.ts +43 -0
  426. package/src/shared/markdown/extensions/mark.ts +48 -0
  427. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  428. package/src/shared/markdown/extensions/tables.ts +109 -0
  429. package/src/shared/markdown/extensions/task-list.ts +53 -0
  430. package/src/shared/markdown/formula.ts +1187 -0
  431. package/src/shared/markdown/index.ts +193 -0
  432. package/src/shared/markdown/shared.ts +36 -0
  433. package/src/shared/mock-cover.ts +130 -0
  434. package/src/shared/network-address.ts +106 -0
  435. package/src/shared/redirect.ts +56 -0
  436. package/src/shared/template-rendering.ts +140 -0
  437. package/src/shared/theme.ts +68 -0
  438. package/src/shared/time.ts +13 -0
  439. package/src/ssr/AdminLayout.tsx +53 -0
  440. package/src/ssr/AdminSidebar.tsx +50 -0
  441. package/src/ssr/AppLaunchpad.island.tsx +236 -0
  442. package/src/ssr/AppWorkspaceController.island.tsx +21 -0
  443. package/src/ssr/BrowserNotifications.island.tsx +135 -0
  444. package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
  445. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
  446. package/src/ssr/GlobalSearchDialog.tsx +35 -0
  447. package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
  448. package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
  449. package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
  450. package/src/ssr/Layout.render.test.tsx +148 -0
  451. package/src/ssr/Layout.tsx +292 -0
  452. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  453. package/src/ssr/LayoutFooter.tsx +22 -0
  454. package/src/ssr/LayoutHeader.tsx +81 -0
  455. package/src/ssr/LayoutHelp.tsx +869 -0
  456. package/src/ssr/LayoutPreferences.island.tsx +36 -0
  457. package/src/ssr/LayoutRail.tsx +58 -0
  458. package/src/ssr/MinimalLayout.tsx +46 -0
  459. package/src/ssr/PageError.tsx +29 -0
  460. package/src/ssr/ProfilePreferences.island.tsx +92 -0
  461. package/src/ssr/RailApps.island.tsx +61 -0
  462. package/src/ssr/RailEditor.tsx +293 -0
  463. package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
  464. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  465. package/src/ssr/admin-active-link.ts +47 -0
  466. package/src/ssr/admin-navigation.ts +78 -0
  467. package/src/ssr/app-appearance.ts +23 -0
  468. package/src/ssr/app-navigation.ts +41 -0
  469. package/src/ssr/help-messages.ts +104 -0
  470. package/src/ssr/index.ts +14 -0
  471. package/src/ssr/islands/SearchBar.island.tsx +80 -0
  472. package/src/ssr/islands/index.ts +16 -0
  473. package/src/ssr/layout-context.ts +17 -0
  474. package/src/ssr/layout-help-markdown.ts +9 -0
  475. package/src/ssr/layout-help-navigation.ts +24 -0
  476. package/src/ssr/layout-help-url.ts +5 -0
  477. package/src/ssr/layout-runtime.ts +22 -0
  478. package/src/ssr/page-error-messages.ts +25 -0
  479. package/src/ssr/platform-messages.ts +135 -0
  480. package/src/ssr/preference-controller.ts +44 -0
  481. package/src/ssr/profile-preferences-messages.ts +29 -0
  482. package/src/ssr/rail-context.ts +21 -0
  483. package/src/ssr/rail-messages.ts +66 -0
  484. package/src/ssr/rail-navigation.ts +60 -0
  485. package/src/ssr/runtime.ts +31 -0
  486. package/src/ssr/url-filter.ts +127 -0
  487. package/src/styles/base-popover.css +36 -0
  488. package/src/styles/effects.css +851 -0
  489. package/src/styles/global.css +268 -0
  490. package/src/styles/input.css +58 -0
  491. package/src/styles/tokens.css +230 -0
  492. package/src/styles/utilities-buttons.css +405 -0
  493. package/src/styles/utilities-data.css +33 -0
  494. package/src/styles/utilities-feedback.css +232 -0
  495. package/src/styles/utilities-layout.css +603 -0
  496. package/src/styles/utilities-markdown-editor.css +525 -0
  497. package/src/styles/utilities-markdown-table.css +234 -0
  498. package/src/styles/utilities-navigation.css +1054 -0
  499. package/src/types/ambient.d.ts +24 -0
  500. package/src/workflows/ai/index.ts +17 -0
  501. package/src/workflows/ai/runtime.ts +286 -0
  502. package/src/workflows/ai/store.ts +314 -0
  503. package/src/workflows/ai/types.ts +111 -0
  504. package/src/workflows/ai-actions.ts +269 -0
  505. package/src/workflows/authoring.ts +88 -0
  506. package/src/workflows/builtins.ts +193 -0
  507. package/src/workflows/contracts.ts +280 -0
  508. package/src/workflows/definition.ts +334 -0
  509. package/src/workflows/editor.ts +113 -0
  510. package/src/workflows/index.ts +71 -0
  511. package/src/workflows/language/binder.ts +45 -0
  512. package/src/workflows/language/canonical.ts +29 -0
  513. package/src/workflows/language/compiler.ts +396 -0
  514. package/src/workflows/language/expressions.ts +34 -0
  515. package/src/workflows/language/index.ts +20 -0
  516. package/src/workflows/language/references.ts +55 -0
  517. package/src/workflows/language/schema.ts +134 -0
  518. package/src/workflows/language/strict-yaml.ts +131 -0
  519. package/src/workflows/manifest.ts +24 -0
  520. package/src/workflows/module.ts +12 -0
  521. package/src/workflows/runtime/coordinator.ts +115 -0
  522. package/src/workflows/runtime/executor.ts +1003 -0
  523. package/src/workflows/runtime/index.ts +60 -0
  524. package/src/workflows/runtime/instant.ts +8 -0
  525. package/src/workflows/runtime/ports.ts +213 -0
  526. package/src/workflows/runtime/schedule.ts +203 -0
  527. package/src/workflows/runtime/trigger.ts +37 -0
  528. package/src/workflows/runtime/wake.ts +13 -0
  529. package/src/workflows/store/actions.ts +438 -0
  530. package/src/workflows/store/budget.ts +145 -0
  531. package/src/workflows/store/definitions.ts +345 -0
  532. package/src/workflows/store/events.ts +383 -0
  533. package/src/workflows/store/index.ts +107 -0
  534. package/src/workflows/store/observability.ts +594 -0
  535. package/src/workflows/store/runs.ts +975 -0
  536. package/src/workflows/store/transaction.ts +23 -0
  537. package/src/workflows/store/worker.ts +439 -0
  538. package/src/workflows/testing/conformance.ts +72 -0
  539. package/src/workflows/testing/index.ts +11 -0
  540. package/src/workflows/testing/process-fixtures.ts +275 -0
@@ -0,0 +1,323 @@
1
+ import { redis } from "bun";
2
+ import { logger } from "../logging";
3
+ import { coreSettings } from "../settings/api";
4
+ import type { CurrentWeather, DailyForecast, HourlyForecast, WeatherData, WeatherIcon } from "./types";
5
+
6
+ const log = logger("weather");
7
+
8
+ const BRIGHTSKY_API = "https://api.brightsky.dev";
9
+ const BRIGHTSKY_TIMEOUT_MS = 400;
10
+
11
+ export type ForecastLocationConfig = {
12
+ lat?: string;
13
+ lon?: string;
14
+ };
15
+
16
+ type ResolvedForecastLocation = {
17
+ lat: string;
18
+ lon: string;
19
+ };
20
+
21
+ const resolveForecastLocation = async (config?: ForecastLocationConfig): Promise<ResolvedForecastLocation | null> => {
22
+ const lat = (config?.lat ?? (await coreSettings.get<string>("weather.default_lat"))).trim();
23
+ const lon = (config?.lon ?? (await coreSettings.get<string>("weather.default_lon"))).trim();
24
+
25
+ if (!lat || !lon) {
26
+ log.error("Weather default coordinates are not configured", {
27
+ missing: [!lat ? "weather.default_lat" : null, !lon ? "weather.default_lon" : null].filter(Boolean),
28
+ });
29
+ return null;
30
+ }
31
+
32
+ return { lat, lon };
33
+ };
34
+
35
+ /** Brightsky current_weather API response */
36
+ type BrightskyCurrentResponse = {
37
+ weather: {
38
+ timestamp: string;
39
+ source_id: number;
40
+ cloud_cover: number | null;
41
+ condition: string | null;
42
+ dew_point: number | null;
43
+ icon: WeatherIcon | null;
44
+ precipitation_10: number | null;
45
+ precipitation_30: number | null;
46
+ precipitation_60: number | null;
47
+ pressure_msl: number | null;
48
+ relative_humidity: number | null;
49
+ solar_10: number | null;
50
+ solar_30: number | null;
51
+ solar_60: number | null;
52
+ sunshine_30: number | null;
53
+ sunshine_60: number | null;
54
+ temperature: number | null;
55
+ visibility: number | null;
56
+ wind_direction_10: number | null;
57
+ wind_direction_30: number | null;
58
+ wind_direction_60: number | null;
59
+ wind_gust_direction_10: number | null;
60
+ wind_gust_direction_30: number | null;
61
+ wind_gust_direction_60: number | null;
62
+ wind_gust_speed_10: number | null;
63
+ wind_gust_speed_30: number | null;
64
+ wind_gust_speed_60: number | null;
65
+ wind_speed_10: number | null;
66
+ wind_speed_30: number | null;
67
+ wind_speed_60: number | null;
68
+ };
69
+ sources: Array<{
70
+ id: number;
71
+ dwd_station_id: string;
72
+ station_name: string;
73
+ lat: number;
74
+ lon: number;
75
+ height: number;
76
+ distance: number;
77
+ }>;
78
+ };
79
+
80
+ /** Brightsky weather (forecast) API response */
81
+ type BrightskyWeatherResponse = {
82
+ weather: Array<{
83
+ timestamp: string;
84
+ temperature: number | null;
85
+ icon: WeatherIcon | null;
86
+ precipitation: number | null;
87
+ precipitation_probability: number | null;
88
+ wind_speed: number | null;
89
+ cloud_cover: number | null;
90
+ sunshine: number | null;
91
+ }>;
92
+ sources: Array<{
93
+ station_name: string;
94
+ }>;
95
+ };
96
+
97
+ /** Get cache key for location (default or custom). */
98
+ const getCacheKey = (lat: string, lon: string): string => {
99
+ return `weather:${lat}:${lon}`;
100
+ };
101
+
102
+ /** Fetch current weather from Brightsky API. */
103
+ const fetchCurrentFromApi = async (lat: string, lon: string): Promise<CurrentWeather | null> => {
104
+ const controller = new AbortController();
105
+ const timeout = setTimeout(() => controller.abort(), BRIGHTSKY_TIMEOUT_MS);
106
+
107
+ try {
108
+ const url = `${BRIGHTSKY_API}/current_weather?lat=${lat}&lon=${lon}`;
109
+ const response = await fetch(url, { signal: controller.signal });
110
+
111
+ if (!response.ok) {
112
+ log.error("Brightsky API error", { status: response.status });
113
+ return null;
114
+ }
115
+
116
+ const data: BrightskyCurrentResponse = await response.json();
117
+
118
+ if (!data.weather || data.weather.temperature === null) {
119
+ log.error("Invalid Brightsky response");
120
+ return null;
121
+ }
122
+
123
+ const w = data.weather;
124
+ const source = data.sources?.[0];
125
+
126
+ return {
127
+ temperature: Math.round(w.temperature!),
128
+ icon: w.icon ?? "cloudy",
129
+ cloudCover: w.cloud_cover ?? 0,
130
+ windSpeed: Math.round(w.wind_speed_10 ?? w.wind_speed_30 ?? 0),
131
+ windGust: w.wind_gust_speed_10 ? Math.round(w.wind_gust_speed_10) : null,
132
+ windDirection: w.wind_direction_10 ?? w.wind_direction_30 ?? null,
133
+ humidity: w.relative_humidity,
134
+ precipitation: w.precipitation_60 ?? 0,
135
+ pressure: w.pressure_msl != null ? Math.round(w.pressure_msl) : null,
136
+ visibility: w.visibility != null ? Math.round(w.visibility) : null,
137
+ dewPoint: w.dew_point != null ? Math.round(w.dew_point * 10) / 10 : null,
138
+ sunshine: w.sunshine_60 != null ? w.sunshine_60 : null,
139
+ stationName: source?.station_name ?? "Unknown",
140
+ timestamp: w.timestamp,
141
+ };
142
+ } catch (error) {
143
+ log.error("Failed to fetch from Brightsky", {
144
+ error: error instanceof Error ? error.message : String(error),
145
+ timeoutMs: BRIGHTSKY_TIMEOUT_MS,
146
+ });
147
+ return null;
148
+ } finally {
149
+ clearTimeout(timeout);
150
+ }
151
+ };
152
+
153
+ /** Get current weather, using Redis cache. */
154
+ export const getCurrentWeather = async (config?: ForecastLocationConfig): Promise<CurrentWeather | null> => {
155
+ const location = await resolveForecastLocation(config);
156
+ if (!location) return null;
157
+
158
+ const { lat, lon } = location;
159
+ const cacheKey = getCacheKey(lat, lon);
160
+
161
+ const cached = await redis.get(cacheKey);
162
+ if (cached) {
163
+ try {
164
+ return JSON.parse(cached) as CurrentWeather;
165
+ } catch {
166
+ // Invalid cache, will refetch
167
+ }
168
+ }
169
+
170
+ const result = await fetchCurrentFromApi(lat, lon);
171
+ if (!result) return null;
172
+
173
+ const ttl = Math.round(((await coreSettings.get<number>("weather.cache_minutes")) ?? 30) * 60);
174
+ await redis.set(cacheKey, JSON.stringify(result), "EX", ttl);
175
+
176
+ return result;
177
+ };
178
+
179
+ /** Get most common icon from array. */
180
+ const getMostCommonIcon = (icons: WeatherIcon[]): WeatherIcon => {
181
+ if (icons.length === 0) return "cloudy";
182
+ const counts = new Map<WeatherIcon, number>();
183
+ for (const icon of icons) {
184
+ counts.set(icon, (counts.get(icon) ?? 0) + 1);
185
+ }
186
+ let maxCount = 0;
187
+ let result: WeatherIcon = "cloudy";
188
+ for (const [icon, count] of counts) {
189
+ if (count > maxCount) {
190
+ maxCount = count;
191
+ result = icon;
192
+ }
193
+ }
194
+ return result;
195
+ };
196
+
197
+ /** Fetch weather forecast from Brightsky API. */
198
+ const fetchForecastFromApi = async (lat: string, lon: string): Promise<{ hourly: HourlyForecast[]; daily: DailyForecast[] } | null> => {
199
+ const controller = new AbortController();
200
+ const timeout = setTimeout(() => controller.abort(), BRIGHTSKY_TIMEOUT_MS);
201
+
202
+ try {
203
+ const now = new Date();
204
+ const endDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
205
+
206
+ const dateStr = now.toISOString().split("T")[0];
207
+ const endDateStr = endDate.toISOString().split("T")[0];
208
+
209
+ const url = `${BRIGHTSKY_API}/weather?lat=${lat}&lon=${lon}&date=${dateStr}&last_date=${endDateStr}`;
210
+ const response = await fetch(url, { signal: controller.signal });
211
+
212
+ if (!response.ok) {
213
+ log.error("Brightsky forecast API error", { status: response.status });
214
+ return null;
215
+ }
216
+
217
+ const data: BrightskyWeatherResponse = await response.json();
218
+
219
+ if (!data.weather || data.weather.length === 0) {
220
+ return null;
221
+ }
222
+
223
+ const hourly: HourlyForecast[] = data.weather
224
+ .filter((w) => w.temperature !== null && new Date(w.timestamp) >= now)
225
+ .slice(0, 12)
226
+ .map((w) => ({
227
+ timestamp: w.timestamp,
228
+ temperature: Math.round(w.temperature ?? 0),
229
+ icon: w.icon ?? "cloudy",
230
+ precipitation: w.precipitation ?? 0,
231
+ precipitationProbability: w.precipitation_probability,
232
+ windSpeed: Math.round(w.wind_speed ?? 0),
233
+ cloudCover: w.cloud_cover ?? 0,
234
+ }));
235
+
236
+ const dailyMap = new Map<
237
+ string,
238
+ {
239
+ temps: number[];
240
+ icons: WeatherIcon[];
241
+ precip: number;
242
+ precipProb: number[];
243
+ sunshine: number;
244
+ }
245
+ >();
246
+
247
+ for (const w of data.weather) {
248
+ if (w.temperature === null) continue;
249
+ const date = w.timestamp.split("T")[0];
250
+ if (!date) continue;
251
+ const existing = dailyMap.get(date) ?? {
252
+ temps: [],
253
+ icons: [],
254
+ precip: 0,
255
+ precipProb: [],
256
+ sunshine: 0,
257
+ };
258
+ existing.temps.push(w.temperature);
259
+ if (w.icon) existing.icons.push(w.icon);
260
+ existing.precip += w.precipitation ?? 0;
261
+ if (w.precipitation_probability != null) {
262
+ existing.precipProb.push(w.precipitation_probability);
263
+ }
264
+ existing.sunshine += w.sunshine ?? 0;
265
+ dailyMap.set(date, existing);
266
+ }
267
+
268
+ const daily: DailyForecast[] = Array.from(dailyMap.entries())
269
+ .slice(0, 7)
270
+ .map(([date, day]) => ({
271
+ date,
272
+ tempMin: Math.round(Math.min(...day.temps)),
273
+ tempMax: Math.round(Math.max(...day.temps)),
274
+ icon: getMostCommonIcon(day.icons),
275
+ precipitation: Math.round(day.precip * 10) / 10,
276
+ precipitationProbability: day.precipProb.length > 0 ? Math.max(...day.precipProb) : null,
277
+ sunshine: Math.round(day.sunshine),
278
+ }));
279
+
280
+ return { hourly, daily };
281
+ } catch (error) {
282
+ log.error("Failed to fetch forecast", {
283
+ error: error instanceof Error ? error.message : String(error),
284
+ timeoutMs: BRIGHTSKY_TIMEOUT_MS,
285
+ });
286
+ return null;
287
+ } finally {
288
+ clearTimeout(timeout);
289
+ }
290
+ };
291
+
292
+ /** Get full weather data including forecasts. */
293
+ export const getWeatherData = async (config?: ForecastLocationConfig): Promise<WeatherData | null> => {
294
+ const location = await resolveForecastLocation(config);
295
+ if (!location) return null;
296
+
297
+ const { lat, lon } = location;
298
+ const cacheKey = `weather:full:${lat}:${lon}`;
299
+
300
+ const cached = await redis.get(cacheKey);
301
+ if (cached) {
302
+ try {
303
+ return JSON.parse(cached) as WeatherData;
304
+ } catch {
305
+ // Invalid cache, will refetch
306
+ }
307
+ }
308
+
309
+ const [current, forecast] = await Promise.all([getCurrentWeather(location), fetchForecastFromApi(lat, lon)]);
310
+
311
+ if (!current) return null;
312
+
313
+ const result: WeatherData = {
314
+ current,
315
+ hourly: forecast?.hourly ?? [],
316
+ daily: forecast?.daily ?? [],
317
+ };
318
+
319
+ const ttl = Math.round(((await coreSettings.get<number>("weather.cache_minutes")) ?? 30) * 60);
320
+ await redis.set(cacheKey, JSON.stringify(result), "EX", ttl);
321
+
322
+ return result;
323
+ };
@@ -0,0 +1,119 @@
1
+ import { err, fail, ok, type PageParams, type Paginated, type Result } from "@k2b/stdlib";
2
+ import { type GeoPlace, geoService } from "../../server";
3
+ import * as settings from "../settings";
4
+
5
+ export const WEATHER_COUNTRY_CODE = "DE";
6
+
7
+ export type WeatherCity = {
8
+ name: string;
9
+ lat: number;
10
+ lon: number;
11
+ country?: string;
12
+ state?: string;
13
+ };
14
+
15
+ const unavailable = <T>(): Result<T> =>
16
+ fail({
17
+ code: "WEATHER_CITY_SEARCH_UNAVAILABLE",
18
+ message: "Weather city search is unavailable",
19
+ status: 500,
20
+ });
21
+
22
+ /**
23
+ * Adapts generic geo place data to the weather app's city model.
24
+ */
25
+ const mapPlace = (place: GeoPlace): WeatherCity => ({
26
+ name: place.name,
27
+ lat: place.lat,
28
+ lon: place.lon,
29
+ country: place.country,
30
+ state: place.state,
31
+ });
32
+
33
+ /**
34
+ * Resolves and validates the configured geo service base URL.
35
+ */
36
+ const getGeoBaseUrl = async (): Promise<Result<string>> => {
37
+ const geoUrl = (await settings.get<string>("weather.geo_url")).trim();
38
+ if (!geoUrl) {
39
+ return fail(err.internal("Geo API URL is not configured. Set weather.geo_url."));
40
+ }
41
+ return ok(geoUrl.replace(/\/$/, ""));
42
+ };
43
+
44
+ /**
45
+ * Searches city candidates via the geo backend and enforces weather-specific country constraints.
46
+ */
47
+ const list = async (config: {
48
+ pagination?: PageParams;
49
+ signal?: AbortSignal;
50
+ filter: {
51
+ query: string;
52
+ country?: string;
53
+ };
54
+ }): Promise<Result<Paginated<WeatherCity>>> => {
55
+ const query = config.filter.query.trim();
56
+ if (!query) {
57
+ return ok({
58
+ items: [],
59
+ page: config.pagination?.page ?? 1,
60
+ perPage: config.pagination?.perPage ?? 20,
61
+ total: 0,
62
+ hasNext: false,
63
+ });
64
+ }
65
+
66
+ const requestedCountry = config.filter.country?.trim().toUpperCase() ?? WEATHER_COUNTRY_CODE;
67
+ if (requestedCountry !== WEATHER_COUNTRY_CODE) {
68
+ return fail(err.badInput("Only German city search is supported (country=DE)."));
69
+ }
70
+
71
+ const baseUrlResult = await getGeoBaseUrl();
72
+ if (!baseUrlResult.ok) return unavailable();
73
+ const geoUrl = baseUrlResult.data;
74
+ const result = await geoService.place.list({
75
+ baseUrl: geoUrl,
76
+ pagination: config.pagination,
77
+ signal: config.signal,
78
+ filter: {
79
+ query,
80
+ country: WEATHER_COUNTRY_CODE,
81
+ featureClass: "P",
82
+ },
83
+ });
84
+ if (!result.ok) return unavailable();
85
+
86
+ return ok({
87
+ ...result.data,
88
+ items: result.data.items.map(mapPlace),
89
+ });
90
+ };
91
+
92
+ /**
93
+ * Resolves one place by coordinates via the geo backend.
94
+ */
95
+ const get = async (config: { lat: number; lon: number }): Promise<Result<WeatherCity | null>> => {
96
+ const baseUrlResult = await getGeoBaseUrl();
97
+ if (!baseUrlResult.ok) return baseUrlResult;
98
+ const result = await geoService.place.get({
99
+ baseUrl: baseUrlResult.data,
100
+ lat: config.lat,
101
+ lon: config.lon,
102
+ });
103
+ if (!result.ok) return result;
104
+
105
+ const place = result.data;
106
+ if (!place) return ok(null);
107
+ if (place.featureClass !== undefined && place.featureClass !== "P") {
108
+ return ok(null);
109
+ }
110
+
111
+ return ok(mapPlace(place));
112
+ };
113
+
114
+ export const weatherCityService = {
115
+ list,
116
+ get,
117
+ };
118
+
119
+ export type WeatherCityService = typeof weatherCityService;
@@ -0,0 +1,67 @@
1
+ import { err, fail, ok, type Result } from "@k2b/stdlib";
2
+ import { registerSettings, toLegacySettingDefs } from "../settings/defaults";
3
+ import { getCurrentWeather, getWeatherData } from "./forecast";
4
+ import { WEATHER_COUNTRY_CODE } from "./geo";
5
+ import { weatherLocationService } from "./location";
6
+ import { weatherLocationsService } from "./locations";
7
+ import { WEATHER_SETTINGS } from "./settings";
8
+ import type { WeatherData } from "./types";
9
+ import { weatherUiService } from "./ui";
10
+
11
+ registerSettings(toLegacySettingDefs(WEATHER_SETTINGS));
12
+
13
+ /**
14
+ * Resolves a city name to coordinates and returns weather for the first match.
15
+ */
16
+ const getByCityName = async (config: { query: string }): Promise<Result<WeatherData>> => {
17
+ const query = config.query.trim();
18
+ if (!query) {
19
+ return fail(err.badInput("City query is required"));
20
+ }
21
+
22
+ const cityResult = await weatherLocationService.city.list({
23
+ pagination: { page: 1, perPage: 1 },
24
+ filter: {
25
+ query,
26
+ country: WEATHER_COUNTRY_CODE,
27
+ },
28
+ });
29
+ if (!cityResult.ok) return cityResult;
30
+
31
+ const city = cityResult.data.items[0];
32
+ if (!city) {
33
+ return fail(err.notFound("City"));
34
+ }
35
+
36
+ const weather = await getWeatherData({
37
+ lat: String(city.lat),
38
+ lon: String(city.lon),
39
+ });
40
+ if (!weather) {
41
+ return fail(err.notFound("Weather data for city"));
42
+ }
43
+
44
+ return ok(weather);
45
+ };
46
+
47
+ export const weatherService = {
48
+ forecast: {
49
+ get: getWeatherData,
50
+ current: {
51
+ get: getCurrentWeather,
52
+ },
53
+ getByCityName,
54
+ },
55
+ location: weatherLocationService,
56
+ locations: weatherLocationsService,
57
+ ui: weatherUiService,
58
+ };
59
+
60
+ export type WeatherService = typeof weatherService;
61
+ export type {
62
+ CurrentWeather,
63
+ DailyForecast,
64
+ HourlyForecast,
65
+ WeatherData,
66
+ WeatherIcon,
67
+ } from "./types";
@@ -0,0 +1,24 @@
1
+ import { weatherCityService } from "./geo";
2
+ import { weatherLocationsService } from "./locations";
3
+
4
+ /** Weather location cookie name. */
5
+ export const WEATHER_LOCATION_COOKIE = "weather_location";
6
+
7
+ /** Parse weather location from cookie value. */
8
+ export const parseLocationCookie = (cookieValue: string | undefined): { lat: string; lon: string } | undefined => {
9
+ if (!cookieValue) return undefined;
10
+ const [lat, lon] = cookieValue.split(",");
11
+ if (!lat || !lon) return undefined;
12
+ return { lat, lon };
13
+ };
14
+
15
+ export const weatherLocationService = {
16
+ saved: weatherLocationsService,
17
+ city: weatherCityService,
18
+ cookie: {
19
+ name: WEATHER_LOCATION_COOKIE,
20
+ parse: parseLocationCookie,
21
+ },
22
+ };
23
+
24
+ export type WeatherLocationService = typeof weatherLocationService;
@@ -0,0 +1,133 @@
1
+ import { err, fail, ok, type PageParams, type Paginated, paginate, type Result } from "@k2b/stdlib";
2
+ import { sql } from "bun";
3
+ import { logger } from "../logging";
4
+ import { withWeatherLocationShortId } from "./short-id";
5
+
6
+ const log = logger("weather");
7
+
8
+ export type Location = {
9
+ id: string;
10
+ name: string;
11
+ state: string | null;
12
+ lat: number;
13
+ lon: number;
14
+ };
15
+
16
+ /**
17
+ * Stores one user-owned weather location and returns the persisted row.
18
+ */
19
+ const create = async (config: {
20
+ userId: string;
21
+ data: {
22
+ name: string;
23
+ state?: string;
24
+ lat: number;
25
+ lon: number;
26
+ };
27
+ }): Promise<Result<Location>> => {
28
+ try {
29
+ const location = await sql.begin(async (tx) => {
30
+ await tx`SELECT pg_advisory_xact_lock(hashtext('cloud.weather.location-short-id-backfill'))`;
31
+ return withWeatherLocationShortId(async (shortId) => {
32
+ const [row] = await tx`
33
+ INSERT INTO weather_locations (short_id, user_id, name, state, lat, lon)
34
+ VALUES (${shortId}, ${config.userId}, ${config.data.name}, ${config.data.state ?? null}, ${config.data.lat}, ${config.data.lon})
35
+ ON CONFLICT (short_id) DO NOTHING
36
+ RETURNING short_id AS id, name, state, lat, lon
37
+ `;
38
+ return (row as Location | undefined) ?? null;
39
+ });
40
+ });
41
+ return ok(location);
42
+ } catch (error) {
43
+ log.error("Failed to create location", {
44
+ error: error instanceof Error ? error.message : String(error),
45
+ });
46
+ return fail(err.internal("Failed to create location"));
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Deletes one saved location owned by the user and reports `NOT_FOUND` when no row matches.
52
+ */
53
+ const remove = async (config: { id: string; userId: string }): Promise<Result<void>> => {
54
+ try {
55
+ const result = await sql`
56
+ DELETE FROM weather_locations
57
+ WHERE short_id = ${config.id} AND user_id = ${config.userId}
58
+ RETURNING short_id
59
+ `;
60
+
61
+ if (result.length === 0) {
62
+ return fail(err.notFound("Location"));
63
+ }
64
+
65
+ return ok();
66
+ } catch (error) {
67
+ log.error("Failed to delete location", {
68
+ error: error instanceof Error ? error.message : String(error),
69
+ });
70
+ return fail(err.internal("Failed to delete location"));
71
+ }
72
+ };
73
+
74
+ /**
75
+ * Lists all saved weather locations for one user with optional search and pagination.
76
+ */
77
+ const list = async (config: { userId: string; pagination?: PageParams; filter?: { query?: string } }): Promise<Paginated<Location>> => {
78
+ const query = config.filter?.query?.trim().toLowerCase() ?? "";
79
+ const queryMatch =
80
+ query.length > 0
81
+ ? sql`(
82
+ POSITION(${query} IN LOWER(name)) > 0
83
+ OR POSITION(${query} IN LOWER(COALESCE(state, ''))) > 0
84
+ )`
85
+ : sql`true`;
86
+ const pagination = config.pagination ? paginate(config.pagination) : null;
87
+
88
+ const [countRow] = await sql<{ count: number }[]>`
89
+ SELECT COUNT(*)::int AS count
90
+ FROM weather_locations
91
+ WHERE user_id = ${config.userId}
92
+ AND ${queryMatch}
93
+ `;
94
+
95
+ const items = (await sql`
96
+ SELECT short_id AS id, name, state, lat, lon
97
+ FROM weather_locations
98
+ WHERE user_id = ${config.userId}
99
+ AND ${queryMatch}
100
+ ORDER BY created_at ASC, id ASC
101
+ LIMIT ${pagination?.perPage ?? null}
102
+ OFFSET ${pagination?.offset ?? 0}
103
+ `) as Location[];
104
+ const total = countRow?.count ?? 0;
105
+ const page = pagination?.page ?? 1;
106
+ const perPage = pagination?.perPage ?? total;
107
+ return {
108
+ items,
109
+ page,
110
+ perPage,
111
+ total,
112
+ hasNext: pagination !== null && page * perPage < total,
113
+ };
114
+ };
115
+
116
+ /**
117
+ * Returns one saved location for the owning user, or `null` if it is missing/inaccessible.
118
+ */
119
+ const get = async (config: { id: string; userId: string }): Promise<Location | null> => {
120
+ const [location] = await sql`
121
+ SELECT short_id AS id, name, state, lat, lon
122
+ FROM weather_locations
123
+ WHERE short_id = ${config.id} AND user_id = ${config.userId}
124
+ `;
125
+ return (location as Location) ?? null;
126
+ };
127
+
128
+ export const weatherLocationsService = {
129
+ list,
130
+ get,
131
+ create,
132
+ remove,
133
+ };