@k2b/cloud 0.42.0 → 0.44.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 (352) hide show
  1. package/package.json +13 -5
  2. package/scripts/app-favicon.ts +7 -6
  3. package/scripts/build.ts +3 -0
  4. package/scripts/installable-assets.ts +61 -0
  5. package/scripts/preload.ts +3 -0
  6. package/src/_internal/app-access-path.ts +12 -0
  7. package/src/_internal/app-access-request.ts +13 -0
  8. package/src/_internal/app-access-resources.ts +141 -0
  9. package/src/_internal/app-event-catalog.ts +40 -0
  10. package/src/_internal/app-event-schemas.ts +127 -0
  11. package/src/_internal/application-package.ts +40 -0
  12. package/src/_internal/built-in-app-ids.json +27 -0
  13. package/src/_internal/capabilities.ts +185 -12
  14. package/src/_internal/capability-origin.ts +4 -0
  15. package/src/_internal/capability-streams.ts +10 -5
  16. package/src/_internal/define-app.ts +159 -45
  17. package/src/_internal/installable-assets.ts +90 -0
  18. package/src/_internal/installable-head.ts +22 -0
  19. package/src/_internal/object-store-area.ts +9 -0
  20. package/src/_internal/object-store-config.ts +101 -0
  21. package/src/_internal/object-store-key.ts +14 -0
  22. package/src/_internal/page-responses.ts +34 -3
  23. package/src/_internal/process-identity.ts +7 -2
  24. package/src/_internal/registry-validation.ts +15 -1
  25. package/src/_internal/registry.ts +44 -1
  26. package/src/_internal/runtime-context.ts +9 -1
  27. package/src/_internal/runtime-watcher.ts +8 -0
  28. package/src/_internal/status-preserving-ssr.ts +12 -1
  29. package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
  30. package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
  31. package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
  32. package/src/access/AppAccessEditor.tsx +380 -0
  33. package/src/access/PermissionEditor.browser-harness.tsx +22 -2
  34. package/src/access/PermissionEditor.tsx +386 -77
  35. package/src/access/PrincipalAvatar.tsx +63 -0
  36. package/src/access/PrincipalPicker.tsx +29 -8
  37. package/src/access/ResourceApiKeys.tsx +3 -4
  38. package/src/access/app-access-adapter.ts +51 -0
  39. package/src/access/app-access-messages.ts +172 -0
  40. package/src/access/messages.ts +14 -0
  41. package/src/access/ui.ts +4 -1
  42. package/src/account/Avatar.tsx +21 -3
  43. package/src/account/ProfilePictureDialog.tsx +699 -0
  44. package/src/account/profile-picture-messages.ts +269 -0
  45. package/src/account/ui.ts +8 -1
  46. package/src/ai/admin.ts +3 -0
  47. package/src/ai/app-frame.ts +12 -0
  48. package/src/ai/application-tasks.ts +101 -0
  49. package/src/ai/assistant-models.ts +126 -64
  50. package/src/ai/capabilities.ts +19 -4
  51. package/src/ai/capability-execution.ts +5 -2
  52. package/src/ai/chart-block.ts +63 -30
  53. package/src/ai/chat/chart-block.tsx +12 -3
  54. package/src/ai/chat/composer-adapter.ts +8 -7
  55. package/src/ai/chat/message-actions.tsx +1 -1
  56. package/src/ai/chat/message-utils.ts +2 -2
  57. package/src/ai/chat/presentation.tsx +22 -1
  58. package/src/ai/chat-quotas.ts +5 -4
  59. package/src/ai/chat-tasks.ts +2 -2
  60. package/src/ai/client/controller.ts +11 -10
  61. package/src/ai/client/projection.ts +3 -3
  62. package/src/ai/code-capability-routes.ts +1 -0
  63. package/src/ai/compaction.ts +3 -3
  64. package/src/ai/credentials.ts +13 -114
  65. package/src/ai/default-tool-contracts.ts +4 -3
  66. package/src/ai/dictation-runtime.ts +4 -4
  67. package/src/ai/enrich.ts +2 -2
  68. package/src/ai/executor.ts +38 -27
  69. package/src/ai/http.ts +17 -29
  70. package/src/ai/index.ts +11 -5
  71. package/src/ai/inference-calls.ts +6 -4
  72. package/src/ai/maintenance.ts +1 -1
  73. package/src/ai/memory-learning-runs.ts +5 -5
  74. package/src/ai/memory-learning.ts +3 -3
  75. package/src/ai/message-queue.ts +1 -2
  76. package/src/ai/migrate.ts +75 -42
  77. package/src/ai/model-access-migrate.ts +3 -17
  78. package/src/ai/model-access.ts +44 -85
  79. package/src/ai/model-card.tsx +134 -0
  80. package/src/ai/model-catalog.ts +359 -0
  81. package/src/ai/model-chooser-messages.ts +88 -0
  82. package/src/ai/model-chooser.tsx +577 -0
  83. package/src/ai/model-delete.ts +234 -0
  84. package/src/ai/model-fixture.ts +39 -0
  85. package/src/ai/model-icon.tsx +36 -0
  86. package/src/ai/model-overview.ts +53 -0
  87. package/src/ai/model-usage.ts +45 -0
  88. package/src/ai/openai-api-messages.ts +0 -4
  89. package/src/ai/openai-api-slots.ts +10 -2
  90. package/src/ai/openai-api.ts +83 -16
  91. package/src/ai/personal-agent.ts +1 -1
  92. package/src/ai/prefs.ts +12 -10
  93. package/src/ai/projects-routes.ts +2 -1
  94. package/src/ai/projects.ts +13 -12
  95. package/src/ai/protocol.ts +2 -2
  96. package/src/ai/provider-connections.ts +161 -0
  97. package/src/ai/provider-errors.ts +138 -0
  98. package/src/ai/provider-fetch.ts +4 -0
  99. package/src/ai/provider-retry.ts +1 -1
  100. package/src/ai/provider.ts +2 -1
  101. package/src/ai/quota-limits.ts +36 -0
  102. package/src/ai/quota-provider.ts +11 -2
  103. package/src/ai/quota-report.ts +7 -4
  104. package/src/ai/quotas-migrate.ts +1 -0
  105. package/src/ai/quotas.ts +19 -8
  106. package/src/ai/recent-resources.ts +6 -0
  107. package/src/ai/request-headers.ts +14 -87
  108. package/src/ai/resource-refs.ts +6 -2
  109. package/src/ai/routes.ts +17 -18
  110. package/src/ai/runtime.ts +50 -22
  111. package/src/ai/settings.ts +93 -580
  112. package/src/ai/store.ts +45 -23
  113. package/src/ai/stream.ts +1 -1
  114. package/src/ai/structured-runs.ts +2 -2
  115. package/src/ai/structured.ts +29 -46
  116. package/src/ai/task-contracts.ts +25 -5
  117. package/src/ai/task-execution.ts +15 -6
  118. package/src/ai/task-model.ts +9 -0
  119. package/src/ai/transcription.ts +37 -21
  120. package/src/ai/turn-failure.ts +24 -2
  121. package/src/ai/types.ts +50 -62
  122. package/src/ai/ui.tsx +13 -0
  123. package/src/ai/usage.ts +8 -8
  124. package/src/ai/validate.ts +8 -7
  125. package/src/ai/view-block.ts +2 -2
  126. package/src/api/accounts-entities.ts +40 -16
  127. package/src/api/admin-ai-quotas.ts +34 -56
  128. package/src/api/admin-ai-usage.ts +1 -1
  129. package/src/api/admin-ai.ts +173 -0
  130. package/src/api/admin-core-settings.ts +68 -244
  131. package/src/api/admin-directory-shares.ts +55 -0
  132. package/src/api/admin-identity.ts +81 -2
  133. package/src/api/app-access.ts +85 -0
  134. package/src/api/app-approval.ts +3 -1
  135. package/src/api/app-event-delivery.ts +51 -0
  136. package/src/api/app-events.ts +123 -0
  137. package/src/api/app-resource-access.ts +48 -0
  138. package/src/api/auth/account-mismatch.ts +24 -0
  139. package/src/api/auth.ts +26 -3
  140. package/src/api/capabilities.ts +127 -15
  141. package/src/api/capability-streams.ts +2 -5
  142. package/src/api/identity-child-invocation.ts +145 -0
  143. package/src/api/index.ts +11 -2
  144. package/src/api/mcp.ts +180 -5
  145. package/src/api/me.ts +38 -17
  146. package/src/api/pwa-phone.ts +87 -48
  147. package/src/api/pwa.ts +34 -8
  148. package/src/api/resource-links.ts +83 -6
  149. package/src/api/resource-resolve.ts +50 -0
  150. package/src/api/search/schemas.ts +3 -1
  151. package/src/api/search.ts +9 -7
  152. package/src/api/widgets.ts +1 -0
  153. package/src/app-pwa/authority.ts +75 -0
  154. package/src/app-pwa/contracts.ts +177 -0
  155. package/src/app-pwa/core-query-error.ts +20 -0
  156. package/src/app-pwa/index.ts +23 -0
  157. package/src/app-pwa/protocol.ts +27 -0
  158. package/src/app-pwa/schema.ts +59 -0
  159. package/src/app-pwa/service.ts +861 -0
  160. package/src/app-pwa/transaction.ts +338 -0
  161. package/src/app-pwa/transport-input.ts +57 -0
  162. package/src/app-pwa/transport.ts +235 -0
  163. package/src/approvals/messages.ts +2 -2
  164. package/src/browser/app-access-recovery.ts +44 -0
  165. package/src/browser/app-session.ts +14 -3
  166. package/src/browser/cloud-id.ts +86 -0
  167. package/src/browser/live-websocket.ts +5 -0
  168. package/src/browser/pwa-drafts.ts +57 -0
  169. package/src/browser/resource-clipboard.ts +4 -0
  170. package/src/browser/resource-links.ts +52 -0
  171. package/src/browser/resource-picker.tsx +3 -2
  172. package/src/capabilities/calendar-sources.ts +302 -0
  173. package/src/capabilities/executions.ts +10 -3
  174. package/src/capabilities/migrate.ts +4 -0
  175. package/src/capabilities/nested.ts +43 -0
  176. package/src/capabilities/server-invoke.ts +202 -0
  177. package/src/capabilities/server.ts +26 -187
  178. package/src/capabilities/testing.ts +5 -2
  179. package/src/cli/access.ts +6 -5
  180. package/src/cli/account.ts +57 -0
  181. package/src/cli/admin/agents.ts +66 -0
  182. package/src/cli/admin/ai-models.ts +199 -0
  183. package/src/cli/admin/ai-quotas.ts +6 -102
  184. package/src/cli/admin/ai-usage.ts +4 -4
  185. package/src/cli/admin/app-credentials.ts +5 -3
  186. package/src/cli/admin/index.ts +7 -3
  187. package/src/cli/app-access.ts +60 -0
  188. package/src/cli/apps.ts +89 -26
  189. package/src/cli/avatar.ts +64 -0
  190. package/src/cli/capabilities.ts +22 -1
  191. package/src/cli/events.ts +320 -0
  192. package/src/cli/index.ts +1 -1
  193. package/src/config/env.ts +40 -0
  194. package/src/contracts/app-access.ts +52 -5
  195. package/src/contracts/app-approval.ts +8 -1
  196. package/src/contracts/app-details.ts +89 -0
  197. package/src/contracts/app-events.ts +51 -0
  198. package/src/contracts/app.ts +24 -5
  199. package/src/contracts/calendar-source.ts +274 -0
  200. package/src/contracts/calendar-view.ts +35 -0
  201. package/src/contracts/capabilities.ts +164 -3
  202. package/src/contracts/geo-admin.ts +2 -0
  203. package/src/contracts/geo-route.ts +0 -11
  204. package/src/contracts/geo.ts +14 -2
  205. package/src/contracts/index.ts +6 -0
  206. package/src/contracts/installable.ts +40 -0
  207. package/src/contracts/notification-types.ts +36 -9
  208. package/src/contracts/profile.ts +32 -0
  209. package/src/contracts/pwa-builds.ts +14 -0
  210. package/src/contracts/pwa-location.ts +38 -0
  211. package/src/contracts/pwa.ts +24 -3
  212. package/src/contracts/registry.ts +8 -0
  213. package/src/contracts/resource-clipboard.ts +35 -0
  214. package/src/contracts/resource-pages.ts +140 -0
  215. package/src/contracts/resource-resolution.ts +13 -0
  216. package/src/contracts/shared.ts +4 -0
  217. package/src/contracts/user-notifications.ts +2 -0
  218. package/src/events/app-event-delivery.ts +100 -0
  219. package/src/events/app-events.testing.ts +65 -0
  220. package/src/events/app-events.ts +134 -0
  221. package/src/events/core-user-created.ts +24 -0
  222. package/src/events/index.ts +9 -0
  223. package/src/events/testing.ts +2 -0
  224. package/src/index.ts +2 -1
  225. package/src/server/api/respond.ts +8 -3
  226. package/src/server/index.ts +1 -0
  227. package/src/server/middleware/app-access.ts +7 -6
  228. package/src/server/middleware/auth.ts +168 -66
  229. package/src/server/middleware/index.ts +1 -0
  230. package/src/server/middleware/invocation.ts +4 -2
  231. package/src/server/middleware/workload.ts +3 -3
  232. package/src/server/oauth-scope.ts +10 -0
  233. package/src/server/public-storage.ts +106 -0
  234. package/src/server/services/access.ts +8 -0
  235. package/src/services/accounts/app.ts +35 -3
  236. package/src/services/accounts/avatar.ts +342 -35
  237. package/src/services/accounts/base-group.ts +4 -5
  238. package/src/services/accounts/directory-shares.ts +157 -0
  239. package/src/services/accounts/entities.ts +50 -6
  240. package/src/services/accounts/groups.ts +3 -1
  241. package/src/services/accounts/identities.ts +64 -12
  242. package/src/services/accounts/local-groups.ts +1 -1
  243. package/src/services/accounts/users.ts +23 -15
  244. package/src/services/app-access-management.ts +519 -0
  245. package/src/services/app-access.ts +25 -7
  246. package/src/services/app-approval.ts +15 -4
  247. package/src/services/app-details.ts +224 -0
  248. package/src/services/app-discovery.ts +2 -1
  249. package/src/services/app-event-shapes.ts +139 -0
  250. package/src/services/app-events/accept.ts +116 -0
  251. package/src/services/app-events/admin.ts +250 -0
  252. package/src/services/app-events/declarations.ts +187 -0
  253. package/src/services/app-events/deliver.ts +218 -0
  254. package/src/services/app-events.ts +92 -0
  255. package/src/services/config-conflict.ts +11 -0
  256. package/src/services/core-user-events.ts +1 -0
  257. package/src/services/freeipa-config.ts +9 -0
  258. package/src/services/geo/admin.ts +4 -1
  259. package/src/services/geo/ledger.ts +47 -14
  260. package/src/services/geo/route-chain.ts +40 -18
  261. package/src/services/geo/route-estimate.ts +19 -0
  262. package/src/services/geo/route-plan.ts +0 -2
  263. package/src/services/geo/route-service.ts +57 -39
  264. package/src/services/geo/service.ts +51 -25
  265. package/src/services/identity/core-origin.ts +12 -0
  266. package/src/services/identity/index.ts +2 -1
  267. package/src/services/identity/invocation-actor.ts +50 -2
  268. package/src/services/identity/invocation-authority.ts +38 -1
  269. package/src/services/identity/invocation-token.ts +255 -50
  270. package/src/services/identity/issue-invocation.ts +9 -1
  271. package/src/services/identity/models-only-auth.ts +13 -4
  272. package/src/services/identity/nested-invocation.ts +128 -0
  273. package/src/services/image-metadata.ts +65 -1
  274. package/src/services/index.ts +24 -1
  275. package/src/services/ipa/sync.ts +6 -2
  276. package/src/services/ipa/users.ts +4 -1
  277. package/src/services/mandates/policy.ts +19 -12
  278. package/src/services/notifications/browser.ts +4 -0
  279. package/src/services/notifications/catalog.ts +5 -2
  280. package/src/services/notifications/platform.ts +7 -2
  281. package/src/services/notifications/user.ts +18 -7
  282. package/src/services/oauth-tokens.ts +28 -2
  283. package/src/services/object-store.ts +217 -0
  284. package/src/services/providers/local/users.ts +5 -0
  285. package/src/services/pwa-devices.ts +147 -37
  286. package/src/services/service-account-credentials.ts +5 -2
  287. package/src/services/session/index.ts +14 -0
  288. package/src/services/settings/core-settings.ts +91 -47
  289. package/src/services/settings/index.ts +8 -0
  290. package/src/services/settings/versions.ts +26 -0
  291. package/src/shared/ai-models.ts +274 -0
  292. package/src/shared/ai-quotas.ts +2 -2
  293. package/src/shared/ai-usage.ts +2 -2
  294. package/src/shared/index.ts +2 -0
  295. package/src/shared-text/documents.ts +25 -8
  296. package/src/shared-text/log.ts +1 -1
  297. package/src/shared-text/text-diff.ts +75 -0
  298. package/src/ssr/AdminArea.tsx +131 -0
  299. package/src/ssr/AdminLayout.tsx +11 -3
  300. package/src/ssr/AdminSidebar.tsx +7 -7
  301. package/src/ssr/AppAccessDenied.tsx +114 -0
  302. package/src/ssr/AppBadge.tsx +14 -0
  303. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  304. package/src/ssr/AppPwaRuntime.island.tsx +82 -0
  305. package/src/ssr/CloudShell.island.tsx +25 -4
  306. package/src/ssr/Layout.tsx +15 -7
  307. package/src/ssr/PwaLayout.tsx +73 -8
  308. package/src/ssr/PwaPartCount.island.tsx +23 -0
  309. package/src/ssr/PwaRuntime.island.tsx +85 -6
  310. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  311. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  312. package/src/ssr/ShellRuntime.island.tsx +5 -0
  313. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  314. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
  315. package/src/ssr/admin-active-link.ts +16 -0
  316. package/src/ssr/admin-navigation.ts +26 -20
  317. package/src/ssr/app-badges.ts +31 -7
  318. package/src/ssr/app-pwa-document.tsx +16 -0
  319. package/src/ssr/app-pwa-messages.ts +17 -0
  320. package/src/ssr/index.ts +8 -0
  321. package/src/ssr/layout-context.ts +1 -1
  322. package/src/ssr/page-trail-runtime.ts +3 -1
  323. package/src/ssr/platform-messages.ts +9 -1
  324. package/src/ssr/pwa-messages.ts +10 -2
  325. package/src/ssr/rail-navigation.ts +5 -1
  326. package/src/ssr/settings-draft.ts +153 -0
  327. package/src/ssr/shell-data.tsx +2 -0
  328. package/src/styles/global.css +1 -0
  329. package/src/styles/profile-picture.css +270 -0
  330. package/src/styles/utilities-navigation.css +6 -0
  331. package/src/workflows/ai/policy.ts +49 -0
  332. package/src/workflows/ai/runtime.ts +131 -21
  333. package/src/workflows/ai/store.ts +283 -35
  334. package/src/workflows/ai/types.ts +8 -2
  335. package/src/workflows/ai-actions.ts +148 -120
  336. package/src/workflows/authoring.ts +31 -0
  337. package/src/workflows/contracts.ts +1 -0
  338. package/src/workflows/definition.ts +2 -0
  339. package/src/workflows/editor.ts +15 -0
  340. package/src/workflows/index.ts +2 -1
  341. package/src/workflows/language/compiler.ts +34 -3
  342. package/src/workflows/runtime/executor.ts +132 -33
  343. package/src/workflows/runtime/index.ts +2 -0
  344. package/src/workflows/runtime/ports.ts +5 -1
  345. package/src/workflows/store/actions.ts +1 -1
  346. package/src/workflows/store/definitions.ts +5 -0
  347. package/src/workflows/store/runs.ts +5 -3
  348. package/src/workflows/store/worker.ts +7 -2
  349. package/src/ai/model-pricing.ts +0 -43
  350. package/src/ai/model-request-settings.ts +0 -98
  351. package/src/ai/settings-messages.ts +0 -129
  352. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -2,7 +2,8 @@ import { normalizeRedirectTo } from "../shared";
2
2
  import { platformMessages } from "./platform-messages";
3
3
  import type { RuntimeContext } from "./runtime";
4
4
 
5
- export type AdminLink = { href: string; icon: string; label: string };
5
+ /** `match` lists further paths the link stands for, such as the pages of an area. */
6
+ export type AdminLink = { href: string; icon: string; label: string; match?: string[] };
6
7
  export type AdminGroup = { label: string; links: AdminLink[] };
7
8
 
8
9
  const normalizeAdminHref = (href: string | undefined): string | undefined => {
@@ -13,23 +14,12 @@ const normalizeAdminHref = (href: string | undefined): string | undefined => {
13
14
  export const buildAdminGroups = (apps: readonly RuntimeContext["apps"][number][], locale = "en"): AdminGroup[] => {
14
15
  const t = platformMessages.resolve([locale]).t;
15
16
  const settingsLinks: AdminLink[] = [
16
- { href: "/admin/settings?tab=general", icon: "ti-app-window", label: t.general },
17
+ { href: "/admin/settings?tab=storage", icon: "ti-database", label: t.storage },
17
18
  { href: "/admin/outgoing-mail", icon: "ti-mail", label: t.outgoingMail },
18
- { href: "/admin/settings?tab=pdf-rendering", icon: "ti-file-type-pdf", label: t.pdfRendering },
19
- { href: "/admin/places-and-maps", icon: "ti-map-2", label: t.placesAndMaps },
20
19
  { href: "/admin/settings?tab=email-templates", icon: "ti-template", label: t.emailTemplates },
21
20
  { href: "/admin/settings?tab=security", icon: "ti-shield-lock", label: t.security },
22
- { href: "/admin/settings?tab=legal", icon: "ti-file-text", label: t.legal },
23
- ];
24
- const aiLinks: AdminLink[] = [
25
- { href: "/admin/settings?tab=ai-general", icon: "ti-adjustments", label: t.general },
26
- { href: "/admin/settings?tab=ai-providers", icon: "ti-sparkles", label: t.providers },
27
- { href: "/admin/settings?tab=ai-quotas", icon: "ti-gauge", label: t.aiQuotas },
28
- { href: "/admin/settings?tab=ai-usage", icon: "ti-chart-histogram", label: t.aiUsage },
29
- { href: "/admin/settings?tab=ai-skills", icon: "ti-wand", label: t.skills },
30
- { href: "/admin/settings?tab=ai-projects", icon: "ti-folders", label: t.projects },
31
- { href: "/admin/settings?tab=ai-jobs", icon: "ti-activity", label: t.backgroundJobs },
32
21
  ];
22
+ // Areas show as one entry each. The AI area belongs to Core; Observability exists when an app contributes it.
33
23
  const contributedApps = apps
34
24
  .map((app) => ({
35
25
  app,
@@ -46,10 +36,6 @@ export const buildAdminGroups = (apps: readonly RuntimeContext["apps"][number][]
46
36
  }))
47
37
  .sort((a, b) => a.app.name.localeCompare(b.app.name));
48
38
 
49
- aiLinks.push(
50
- ...contributedApps.flatMap(({ groups }) => groups.filter((group) => group.section === "ai").flatMap((group) => group.links)),
51
- );
52
-
53
39
  const appsWithGroups = new Set(contributedApps.filter(({ groups }) => groups.length > 0).map(({ app }) => app.id));
54
40
  const appLinks = apps
55
41
  .filter((app) => !appsWithGroups.has(app.id))
@@ -59,18 +45,39 @@ export const buildAdminGroups = (apps: readonly RuntimeContext["apps"][number][]
59
45
  })
60
46
  .sort((a, b) => a.label.localeCompare(b.label));
61
47
 
48
+ const observabilityPaths = contributedApps.flatMap(({ groups }) =>
49
+ groups.filter((group) => group.section === "observability").flatMap((group) => group.links.map((link) => link.href)),
50
+ );
51
+ const areaLinks: AdminLink[] = [
52
+ { href: "/admin/base", icon: "ti-adjustments", label: t.baseSettings },
53
+ { href: "/admin/ai", icon: "ti-sparkles", label: t.ai },
54
+ ...(observabilityPaths.length > 0
55
+ ? [
56
+ {
57
+ // Gateway Ops owns the area's start; without it, the entry opens the first contributed page.
58
+ href: observabilityPaths.includes("/admin/observability") ? "/admin/observability" : observabilityPaths[0]!,
59
+ icon: "ti-heart-rate-monitor",
60
+ label: t.observability,
61
+ match: observabilityPaths,
62
+ },
63
+ ]
64
+ : []),
65
+ ];
66
+
62
67
  return [
63
68
  {
64
69
  label: t.general,
65
70
  links: [
66
71
  { href: "/admin", icon: "ti-dashboard", label: t.overview },
72
+ { href: "/admin/apps", icon: "ti-apps", label: t.apps },
67
73
  { href: "/admin/app-credentials", icon: "ti-key", label: t.appCredentials },
68
74
  { href: "/admin/rail", icon: "ti-layout-sidebar", label: t.rail },
69
75
  { href: "/admin/announcements", icon: "ti-speakerphone", label: t.announcements },
70
76
  ],
71
77
  },
78
+ { label: t.cloudAreas, links: areaLinks },
72
79
  ...contributedApps.flatMap(({ groups }) =>
73
- groups.filter((group) => group.section !== "ai").map(({ label, links }) => ({ label, links })),
80
+ groups.filter((group) => group.section === undefined).map(({ label, links }) => ({ label, links })),
74
81
  ),
75
82
  {
76
83
  label: t.userManagement,
@@ -82,7 +89,6 @@ export const buildAdminGroups = (apps: readonly RuntimeContext["apps"][number][]
82
89
  { href: "/admin/settings?tab=freeipa", icon: "ti-building-fortress", label: "FreeIPA" },
83
90
  ],
84
91
  },
85
- { label: t.ai, links: aiLinks },
86
92
  { label: t.settings, links: settingsLinks },
87
93
  ...(appLinks.length > 0 ? [{ label: t.appAdmin, links: appLinks }] : []),
88
94
  ];
@@ -14,6 +14,11 @@ export const APP_BADGE_INTERVAL_MS = 60_000;
14
14
  // Keyed by endpoint, so a pinned app shortcut and the app's own entry share one
15
15
  // count. Only the browser ever writes it; server renders always see no badge.
16
16
  const [counts, setCounts] = createSignal<ReadonlyMap<string, number>>(new Map());
17
+ // The line a Cloud app part's summary route returns beside its count, keyed like the counts.
18
+ const [texts, setTexts] = createSignal<ReadonlyMap<string, string>>(new Map());
19
+
20
+ /** The text of a `PwaPartSummary` route once the browser has read it; undefined before or when the read failed. */
21
+ export const appPartSummaryText = (endpoint: string): string | undefined => texts().get(endpoint);
17
22
 
18
23
  const [approvalCounts, setApprovalCounts] = createSignal<ApprovalCounts | null>(null);
19
24
 
@@ -37,16 +42,28 @@ export const parseAppBadge = (body: unknown): number => {
37
42
  return typeof count === "number" && Number.isSafeInteger(count) && count > 0 ? count : 0;
38
43
  };
39
44
 
40
- const readBadge = async (endpoint: string, signal: AbortSignal): Promise<number> => {
45
+ /** A one-line text of at most 200 characters from a `PwaPartSummary` body, else undefined. */
46
+ export const parsePartSummaryText = (body: unknown): string | undefined => {
47
+ if (typeof body !== "object" || body === null || !("text" in body)) return undefined;
48
+ const text = body.text;
49
+ return typeof text === "string" && text.trim() !== "" && text.length <= 200 && !/[\r\n]/.test(text) ? text.trim() : undefined;
50
+ };
51
+
52
+ type BadgeRead = { count: number; text?: string };
53
+
54
+ const readBadge = async (endpoint: string, signal: AbortSignal): Promise<BadgeRead> => {
41
55
  try {
42
56
  const response = await fetch(endpoint, { credentials: "same-origin", headers: { accept: "application/json" }, signal });
43
- if (response.status === 200) return parseAppBadge(await response.json());
57
+ if (response.status === 200) {
58
+ const body: unknown = await response.json();
59
+ return { count: parseAppBadge(body), text: parsePartSummaryText(body) };
60
+ }
44
61
  // Release the response without buffering an unused, potentially unbounded error body.
45
62
  await response.body?.cancel();
46
- return 0;
63
+ return { count: 0 };
47
64
  } catch {
48
65
  // An unreachable or failing app shows no badge rather than a stale one.
49
- return 0;
66
+ return { count: 0 };
50
67
  }
51
68
  };
52
69
 
@@ -108,10 +125,17 @@ export const watchAppBadges = (endpoints: readonly string[]): (() => void) => {
108
125
  running.set(endpoint, false);
109
126
  const signal = AbortSignal.any([stopped.signal, AbortSignal.timeout(APP_BADGE_INTERVAL_MS)]);
110
127
  const result = endpoint === APPROVAL_COUNTS_PATH ? readApprovalCounts(signal) : readBadge(endpoint, signal);
111
- void result.then((count) => {
128
+ void result.then((answer) => {
112
129
  if (stopped.signal.aborted) return;
113
- if (typeof count === "number") setCounts((previous) => new Map(previous).set(endpoint, count));
114
- else setApprovalCounts(count);
130
+ if ("count" in answer) {
131
+ setCounts((previous) => new Map(previous).set(endpoint, answer.count));
132
+ setTexts((previous) => {
133
+ const next = new Map(previous);
134
+ if (answer.text === undefined) next.delete(endpoint);
135
+ else next.set(endpoint, answer.text);
136
+ return next;
137
+ });
138
+ } else setApprovalCounts(answer);
115
139
  const queued = running.get(endpoint);
116
140
  running.delete(endpoint);
117
141
  if (queued && document.visibilityState !== "hidden") read(endpoint);
@@ -0,0 +1,16 @@
1
+ import type { RenderFn } from "@k2b/ssr";
2
+ import { LocaleProvider } from "@k2b/ui";
3
+ import type { AppInstallable } from "../contracts/installable";
4
+ import AppPwaRuntime from "./AppPwaRuntime.island";
5
+
6
+ /** Called by the framework, even when the application renders its own layout. */
7
+ export const appPwaDocument =
8
+ (render: RenderFn, app: AppInstallable, locale: string): RenderFn =>
9
+ () => (
10
+ <>
11
+ {render()}
12
+ <LocaleProvider locale={locale}>
13
+ <AppPwaRuntime app={app} />
14
+ </LocaleProvider>
15
+ </>
16
+ );
@@ -0,0 +1,17 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+
3
+ export const appPwaMessages = i18n.define({
4
+ baseLocale: "en",
5
+ messages: {
6
+ en: {
7
+ title: ({ name }: { name: string }) => `Install ${name}`,
8
+ workerFailed: "Offline support is unavailable. You can continue online or try again.",
9
+ retry: "Try again",
10
+ },
11
+ de: {
12
+ title: ({ name }: { name: string }) => `${name} installieren`,
13
+ workerFailed: "Die Offline-Unterstützung ist nicht verfügbar. Du kannst online fortfahren oder es erneut versuchen.",
14
+ retry: "Erneut versuchen",
15
+ },
16
+ },
17
+ });
package/src/ssr/index.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  // Server-only exports (these transitively import bun:sql via services)
2
2
  // Do NOT import from this barrel in .island.tsx or .client.tsx files!
3
3
 
4
+ export {
5
+ AdminArea,
6
+ type AdminAreaGroup,
7
+ type AdminAreaItem,
8
+ AdminAreaPane,
9
+ AdminAreaSaveStatusFoot,
10
+ AdminAreaSaveStatusHead,
11
+ } from "./AdminArea";
4
12
  export { default as AdminLayout } from "./AdminLayout";
5
13
  export {
6
14
  hasDedicatedRuntimeRoute,
@@ -10,7 +10,7 @@ export type LayoutContext = {
10
10
  get(key: "user"): User | undefined;
11
11
  get(key: "panelConsent"): PanelConsentSnapshot | undefined;
12
12
  get(key: "railPreferences"): RailSnapshot | undefined;
13
- get(key: "page"): { theme?: "light" | "dark"; title?: string; pwa?: { name: string } };
13
+ get(key: "page"): { theme?: "light" | "dark"; title?: string; pwa?: { name: string }; pwaAddress?: string; pwaOwner?: string };
14
14
  get(key: "runtime"): RuntimeContext;
15
15
  get(key: "announcements"): LayoutAnnouncementsState | undefined;
16
16
  /** Every application settings snapshot includes Core's app metadata. */
@@ -304,7 +304,9 @@ export const installPageTrailRuntime = (labels: PageUpLabels): (() => void) => {
304
304
  window.addEventListener("ssr:nav-swapped", restoreArrivingPage);
305
305
  const onCapture = () => rememberPage();
306
306
  const onNavigate = (event: NavigateEvent) => {
307
- if (event.navigationType === "traverse") rememberPage();
307
+ // Same-address history (for example a closing sheet) stays on this page.
308
+ // Only a departure needs a new scroll snapshot; reading it can force layout.
309
+ if (event.navigationType === "traverse" && event.destination.url !== location.href) rememberPage();
308
310
  };
309
311
  const onKey = (event: KeyboardEvent) => {
310
312
  if (event.key === "Enter") rememberPage();
@@ -42,6 +42,9 @@ export const platformMessages = i18n.define({
42
42
  announcements: "Announcements",
43
43
  appCredentials: "App credentials",
44
44
  ai: "AI",
45
+ observability: "Observability",
46
+ baseSettings: "Base settings",
47
+ cloudAreas: "Cloud",
45
48
  settings: "Settings",
46
49
  appAdmin: "App administration",
47
50
  userManagement: "Accounts & sign-in",
@@ -49,6 +52,7 @@ export const platformMessages = i18n.define({
49
52
  accountRegistration: "Registration & requests",
50
53
  accountOperations: "Operations",
51
54
  linuxAccess: "Linux identities",
55
+ storage: "Storage",
52
56
  pdfRendering: "PDF rendering",
53
57
  placesAndMaps: "Places and maps",
54
58
  emailTemplates: "Email templates",
@@ -142,8 +146,11 @@ export const platformMessages = i18n.define({
142
146
  general: "Allgemein",
143
147
  rail: "App-Leiste",
144
148
  announcements: "Ankündigungen",
145
- appCredentials: "App-Zugänge",
149
+ appCredentials: "App-Schlüssel",
146
150
  ai: "KI",
151
+ observability: "Observability",
152
+ baseSettings: "Grundeinstellungen",
153
+ cloudAreas: "Cloud",
147
154
  settings: "Einstellungen",
148
155
  appAdmin: "App-Administration",
149
156
  userManagement: "Accounts & Anmeldung",
@@ -151,6 +158,7 @@ export const platformMessages = i18n.define({
151
158
  accountRegistration: "Registrierung & Anfragen",
152
159
  accountOperations: "Betrieb",
153
160
  linuxAccess: "Linux-Identitäten",
161
+ storage: "Speicher",
154
162
  pdfRendering: "PDF-Erstellung",
155
163
  placesAndMaps: "Orte und Karten",
156
164
  emailTemplates: "E-Mail-Vorlagen",
@@ -7,17 +7,25 @@ export const pwaMessages = i18n.define({
7
7
  en: {
8
8
  tabs: "App",
9
9
  start: "Start",
10
+ apps: "Apps",
11
+ settings: "Settings",
10
12
  offline: "You're offline",
13
+ newVersion: ({ cloud }: { cloud: string }) => `New version of ${cloud}`,
14
+ reload: "Reload",
11
15
  otherAccount: ({ cloud, name }: { cloud: string; name: string }) =>
12
- `Chrome on this phone is signed in to ${cloud} as ${name}. Changes in the app may be saved to that account. Sign out of ${cloud} in Chrome.`,
16
+ `Chrome on this device is signed in to ${cloud} as ${name}. Changes in the app may be saved to that account. Sign out of ${cloud} in Chrome.`,
13
17
  errorAction: "Back to Start",
14
18
  },
15
19
  de: {
16
20
  tabs: "App",
17
21
  start: "Start",
22
+ apps: "Apps",
23
+ settings: "Einstellungen",
18
24
  offline: "Du bist offline",
25
+ newVersion: ({ cloud }: { cloud: string }) => `Neue Version von ${cloud}`,
26
+ reload: "Neu laden",
19
27
  otherAccount: ({ cloud, name }: { cloud: string; name: string }) =>
20
- `Chrome auf diesem Telefon ist bei ${cloud} als ${name} angemeldet. Änderungen in der App werden womöglich in diesem Konto gespeichert. Melde dich in Chrome von ${cloud} ab.`,
28
+ `Chrome auf diesem Gerät ist bei ${cloud} als ${name} angemeldet. Änderungen in der App werden womöglich in diesem Konto gespeichert. Melde dich in Chrome von ${cloud} ab.`,
21
29
  errorAction: "Zurück zum Start",
22
30
  },
23
31
  },
@@ -79,6 +79,9 @@ export const phoneBarLinks = (navigation: { shortcuts: RailLink[]; apps: RailLin
79
79
  return [...links.slice(0, limit - 1), links[current]!];
80
80
  };
81
81
 
82
+ /** A phone bar item, and where its app's count comes from, like the rail icon's. */
83
+ export type PhoneBarItem = TabBarItem & { counted?: { endpoint?: string; appId?: string } };
84
+
82
85
  /** Start and four apps fill the phone bar's five places. */
83
86
  const PHONE_BAR_APPS = 4;
84
87
 
@@ -93,7 +96,7 @@ export const phoneBarItems = (
93
96
  navigation: { shortcuts: RailLink[]; apps: RailLink[] },
94
97
  currentPath: string,
95
98
  home: { label: string; path: string },
96
- ): TabBarItem[] => {
99
+ ): PhoneBarItem[] => {
97
100
  const links = phoneBarLinks(navigation, currentPath, PHONE_BAR_APPS);
98
101
  const items = links.map((link) => ({
99
102
  id: link.id,
@@ -101,6 +104,7 @@ export const phoneBarItems = (
101
104
  icon: link.iconClass,
102
105
  href: link.href,
103
106
  current: railLinkActive(link, currentPath),
107
+ ...(link.badge || link.appId ? { counted: { endpoint: link.badge, appId: link.appId } } : {}),
104
108
  }));
105
109
  const pathname = new URL(currentPath, "https://cloud.invalid").pathname;
106
110
  const start = new URL(home.path, "https://cloud.invalid").pathname;
@@ -0,0 +1,153 @@
1
+ import type { SaveStatusState } from "@k2b/ui";
2
+ import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
3
+
4
+ /**
5
+ * A settings form saves with the button in its level head, outside the form island. Both islands read this store: the
6
+ * form registers its state and actions, `SettingsSaveStatus` renders them. One form per page registers at a time.
7
+ */
8
+ export type SettingsSaveControls = {
9
+ state: SaveStatusState;
10
+ savedAt?: string;
11
+ invalidCount?: number;
12
+ save: () => void;
13
+ discard: () => void;
14
+ };
15
+
16
+ const [controls, setControls] = createSignal<SettingsSaveControls | null>(null);
17
+ export const settingsSaveControls = controls;
18
+
19
+ /** A save that did not succeed: refused with a reason, or with an unclear outcome. */
20
+ export type SettingsDraftOutcome = { kind: "failed" | "unknown"; message: string };
21
+
22
+ /** Thrown by `save` when the server refused the write; anything else counts as an unclear outcome. */
23
+ export class SaveRejected extends Error {}
24
+
25
+ /**
26
+ * The error for a non-OK save response. A 4xx refused the write. A 5xx can come after the value was stored (for
27
+ * example when a follow-up step failed), so its outcome stays unclear and the person checks the current state.
28
+ */
29
+ export const saveFailure = (status: number, message: string): Error =>
30
+ status >= 400 && status < 500 ? new SaveRejected(message) : new Error(message);
31
+
32
+ /**
33
+ * For endpoints that write in one step and answer afterwards: an error body with a message comes from the handler,
34
+ * which did not store the value, so the save is refused and can be tried again. A response without one (a proxy
35
+ * page, a crash) leaves the outcome unclear.
36
+ */
37
+ export const oneStepFailure = (status: number, body: { message?: string } | null, fallback: string): Error =>
38
+ body?.message ? new SaveRejected(body.message) : saveFailure(status, fallback);
39
+
40
+ type Options<T> = {
41
+ initial: T;
42
+ /** Field errors of a draft; Save stays disabled while there are any. */
43
+ validate?: (value: T) => Record<string, string>;
44
+ /** True when saving is needed although the values did not change, such as a confirmed replacement. */
45
+ pending?: () => boolean;
46
+ /**
47
+ * Writes the draft and returns what is stored now. A save that writes several values one after another reports
48
+ * each stored step with `commit`, so a later failure keeps what already reached the server as the saved state.
49
+ */
50
+ save: (value: T, commit: (stored: T) => void) => Promise<T>;
51
+ locale: () => string;
52
+ /** Runs with Discard, for state the form keeps beside the draft, such as staged resets. */
53
+ onDiscard?: () => void;
54
+ };
55
+
56
+ /**
57
+ * A settings form that saves with a button: the draft, its field errors and the outcome of the last save. It registers
58
+ * with the level head, which shows the status with Discard and Save. A failed save keeps the draft; when the
59
+ * connection breaks, the outcome stays unclear and the person checks the current state instead of saving twice.
60
+ */
61
+ export const createSettingsDraft = <T>(options: Options<T>) => {
62
+ const [stored, setStored] = createSignal<T>(options.initial);
63
+ const [draft, setDraft] = createSignal<T>(options.initial);
64
+ const [saving, setSaving] = createSignal(false);
65
+ const [outcome, setOutcome] = createSignal<SettingsDraftOutcome | null>(null);
66
+ const [savedAt, setSavedAt] = createSignal<string | undefined>(undefined);
67
+ const errors = createMemo(() => options.validate?.(draft()) ?? {});
68
+ const dirty = createMemo(() => JSON.stringify(draft()) !== JSON.stringify(stored()) || options.pending?.() === true);
69
+ const state = createMemo<SaveStatusState>(() => {
70
+ if (saving()) return "saving";
71
+ if (outcome()?.kind === "unknown") return "unknown";
72
+ if (Object.keys(errors()).length > 0) return "invalid";
73
+ if (outcome()?.kind === "failed") return "failed";
74
+ return dirty() ? "draft" : "saved";
75
+ });
76
+
77
+ const save = async () => {
78
+ if (saving() || !dirty() || Object.keys(errors()).length > 0 || outcome()?.kind === "unknown") return;
79
+ setSaving(true);
80
+ setOutcome(null);
81
+ const submitted = draft();
82
+ // Edits made while the request runs stay; only an untouched draft takes the stored values.
83
+ const settle = (next: T) => {
84
+ setStored(() => next);
85
+ if (draft() === submitted) setDraft(() => next);
86
+ };
87
+ try {
88
+ // A step only moves the saved state; the draft keeps the values still on their way.
89
+ settle(await options.save(submitted, (step) => setStored(() => step)));
90
+ setSavedAt(new Intl.DateTimeFormat(options.locale(), { hour: "2-digit", minute: "2-digit" }).format(new Date()));
91
+ } catch (error) {
92
+ setOutcome({
93
+ kind: error instanceof SaveRejected ? "failed" : "unknown",
94
+ message: error instanceof Error ? error.message : String(error),
95
+ });
96
+ } finally {
97
+ setSaving(false);
98
+ }
99
+ };
100
+ const discard = () => {
101
+ setDraft(() => stored());
102
+ setOutcome((current) => (current?.kind === "unknown" ? current : null));
103
+ options.onDiscard?.();
104
+ };
105
+ /**
106
+ * Takes newer stored values without losing edits: a field still at its old saved value moves to the new one, an
107
+ * edited field keeps the person's value. `adopt` names fields that take the new value regardless.
108
+ */
109
+ const rebase = (next: T, adopt: readonly string[] = []) => {
110
+ const previous = stored() as Record<string, unknown>;
111
+ const current = draft() as Record<string, unknown>;
112
+ const merged: Record<string, unknown> = { ...current };
113
+ for (const [key, value] of Object.entries(next as Record<string, unknown>)) {
114
+ if (adopt.includes(key) || JSON.stringify(current[key]) === JSON.stringify(previous[key])) merged[key] = value;
115
+ }
116
+ setStored(() => next);
117
+ setDraft(() => merged as T);
118
+ };
119
+
120
+ // Leaving the page with unsaved changes asks first, as the settings pages did before.
121
+ if (typeof window !== "undefined") {
122
+ const guard = (event: BeforeUnloadEvent) => {
123
+ if (state() === "saved") return;
124
+ event.preventDefault();
125
+ };
126
+ window.addEventListener("beforeunload", guard);
127
+ onCleanup(() => window.removeEventListener("beforeunload", guard));
128
+ }
129
+
130
+ createEffect(() =>
131
+ setControls({
132
+ state: state(),
133
+ savedAt: savedAt(),
134
+ invalidCount: Object.keys(errors()).length,
135
+ save: () => void save(),
136
+ discard,
137
+ }),
138
+ );
139
+ onCleanup(() => setControls(null));
140
+
141
+ return {
142
+ draft,
143
+ update: (change: Partial<T>) => setDraft((current) => ({ ...current, ...change })),
144
+ errors,
145
+ outcome,
146
+ saving,
147
+ save,
148
+ discard,
149
+ rebase,
150
+ /** The values last read from the server. */
151
+ stored,
152
+ };
153
+ };
@@ -9,6 +9,8 @@ import type { PanelProvider } from "./app-navigation";
9
9
  import type { RailApp } from "./rail-navigation";
10
10
 
11
11
  export type ShellData = {
12
+ /** Canonical public Cloud origin; omitted until app.url is configured. */
13
+ cloudOrigin?: string;
12
14
  panel?: { providers: PanelProvider[]; provider?: string; context: PanelPageContext | null; consent?: PanelConsentSnapshot };
13
15
  /** The authorized navigation catalog, sorted for the request locale: the rail, the app grid and the phone menu show it. */
14
16
  apps: RailApp[];
@@ -32,6 +32,7 @@
32
32
  @import "./input.css";
33
33
  @import "./resource-search.css";
34
34
  @import "./file-chooser.css";
35
+ @import "./profile-picture.css";
35
36
  @import "./panel.css";
36
37
 
37
38
  @theme {