@k2b/cloud 0.43.0 → 0.45.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 (308) hide show
  1. package/package.json +11 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/GroupCoverage.tsx +20 -5
  21. package/src/access/PermissionEditor.tsx +20 -5
  22. package/src/access/PrincipalAvatar.tsx +30 -21
  23. package/src/access/PrincipalPicker.tsx +14 -4
  24. package/src/access/ResourceApiKeys.tsx +3 -4
  25. package/src/access/app-access-messages.ts +6 -0
  26. package/src/access/messages.ts +4 -4
  27. package/src/account/EntitySearch.tsx +12 -0
  28. package/src/account/ProfilePictureDialog.tsx +699 -0
  29. package/src/account/profile-picture-messages.ts +269 -0
  30. package/src/account/ui.ts +7 -0
  31. package/src/ai/admin.ts +3 -0
  32. package/src/ai/app-frame.ts +12 -0
  33. package/src/ai/application-tasks.ts +101 -0
  34. package/src/ai/assistant-models.ts +126 -64
  35. package/src/ai/capabilities.ts +19 -4
  36. package/src/ai/chart-block.ts +63 -30
  37. package/src/ai/chat/chart-block.tsx +12 -3
  38. package/src/ai/chat/composer-adapter.ts +8 -7
  39. package/src/ai/chat/message-actions.tsx +1 -1
  40. package/src/ai/chat/message-utils.ts +2 -2
  41. package/src/ai/chat/presentation.tsx +22 -1
  42. package/src/ai/chat-quotas.ts +5 -4
  43. package/src/ai/chat-tasks.ts +2 -2
  44. package/src/ai/client/controller.ts +11 -10
  45. package/src/ai/client/projection.ts +3 -3
  46. package/src/ai/compaction.ts +3 -3
  47. package/src/ai/credentials.ts +13 -114
  48. package/src/ai/default-tool-contracts.ts +4 -3
  49. package/src/ai/dictation-runtime.ts +10 -6
  50. package/src/ai/enrich.ts +2 -2
  51. package/src/ai/executor.ts +40 -25
  52. package/src/ai/http.ts +17 -29
  53. package/src/ai/index.ts +12 -5
  54. package/src/ai/inference-calls.ts +5 -4
  55. package/src/ai/maintenance.ts +1 -1
  56. package/src/ai/memory-learning-runs.ts +5 -5
  57. package/src/ai/memory-learning.ts +3 -3
  58. package/src/ai/message-queue.ts +1 -2
  59. package/src/ai/migrate.ts +75 -42
  60. package/src/ai/model-access-migrate.ts +3 -17
  61. package/src/ai/model-access.ts +44 -85
  62. package/src/ai/model-card.tsx +134 -0
  63. package/src/ai/model-catalog.ts +359 -0
  64. package/src/ai/model-chooser-messages.ts +88 -0
  65. package/src/ai/model-chooser.tsx +577 -0
  66. package/src/ai/model-delete.ts +234 -0
  67. package/src/ai/model-fixture.ts +39 -0
  68. package/src/ai/model-icon.tsx +36 -0
  69. package/src/ai/model-overview.ts +53 -0
  70. package/src/ai/model-usage.ts +45 -0
  71. package/src/ai/openai-api-messages.ts +0 -4
  72. package/src/ai/openai-api.ts +26 -9
  73. package/src/ai/personal-agent.ts +1 -1
  74. package/src/ai/prefs.ts +12 -10
  75. package/src/ai/project-tool.ts +4 -4
  76. package/src/ai/projects-authorization.ts +53 -0
  77. package/src/ai/projects-routes.ts +58 -47
  78. package/src/ai/projects.ts +110 -88
  79. package/src/ai/protocol.ts +2 -2
  80. package/src/ai/provider-connections.ts +161 -0
  81. package/src/ai/provider-errors.ts +55 -6
  82. package/src/ai/provider-fetch.ts +4 -0
  83. package/src/ai/provider.ts +1 -1
  84. package/src/ai/quota-limits.ts +36 -0
  85. package/src/ai/quota-provider.ts +4 -2
  86. package/src/ai/quota-report.ts +7 -4
  87. package/src/ai/quotas.ts +19 -8
  88. package/src/ai/recent-resources.ts +9 -3
  89. package/src/ai/request-headers.ts +14 -87
  90. package/src/ai/resource-refs.ts +6 -2
  91. package/src/ai/routes.ts +47 -48
  92. package/src/ai/runtime.ts +50 -22
  93. package/src/ai/settings.ts +93 -580
  94. package/src/ai/skill-tool.ts +4 -4
  95. package/src/ai/skills-routes.ts +27 -26
  96. package/src/ai/skills.ts +120 -81
  97. package/src/ai/store.ts +45 -23
  98. package/src/ai/stream.ts +1 -1
  99. package/src/ai/structured-runs.ts +2 -2
  100. package/src/ai/structured.ts +21 -44
  101. package/src/ai/task-contracts.ts +25 -5
  102. package/src/ai/task-execution.ts +15 -6
  103. package/src/ai/task-model.ts +9 -0
  104. package/src/ai/transcription.ts +27 -22
  105. package/src/ai/types.ts +50 -62
  106. package/src/ai/ui.tsx +13 -0
  107. package/src/ai/usage.ts +8 -8
  108. package/src/ai/validate.ts +8 -7
  109. package/src/ai/view-block.ts +2 -2
  110. package/src/api/accounts-entities.ts +22 -3
  111. package/src/api/admin-ai-quotas.ts +34 -56
  112. package/src/api/admin-ai-skills.ts +4 -1
  113. package/src/api/admin-ai-usage.ts +1 -1
  114. package/src/api/admin-ai.ts +173 -0
  115. package/src/api/admin-core-settings.ts +68 -244
  116. package/src/api/app-approval.ts +3 -1
  117. package/src/api/app-event-delivery.ts +51 -0
  118. package/src/api/app-events.ts +123 -0
  119. package/src/api/app-resource-access.ts +48 -0
  120. package/src/api/auth/account-mismatch.ts +24 -0
  121. package/src/api/auth.ts +35 -5
  122. package/src/api/capabilities.ts +28 -4
  123. package/src/api/index.ts +7 -1
  124. package/src/api/mcp.ts +175 -2
  125. package/src/api/me.ts +6 -4
  126. package/src/api/pwa-phone.ts +75 -26
  127. package/src/api/pwa.ts +34 -8
  128. package/src/api/resource-links.ts +83 -6
  129. package/src/api/resource-resolve.ts +50 -0
  130. package/src/api/search/schemas.ts +3 -1
  131. package/src/api/search.ts +9 -7
  132. package/src/app-pwa/contracts.ts +4 -0
  133. package/src/app-pwa/core-query-error.ts +20 -0
  134. package/src/app-pwa/index.ts +1 -0
  135. package/src/app-pwa/service.ts +91 -0
  136. package/src/app-pwa/transport.ts +22 -0
  137. package/src/approvals/messages.ts +2 -2
  138. package/src/browser/cloud-id.ts +86 -0
  139. package/src/browser/resource-clipboard.ts +4 -0
  140. package/src/browser/resource-links.ts +52 -0
  141. package/src/browser/session.ts +39 -0
  142. package/src/browser/shared-text-history-validation.ts +151 -0
  143. package/src/browser/shared-text-history.ts +464 -0
  144. package/src/browser/shared-text-offline-authorized.ts +29 -0
  145. package/src/browser/shared-text-offline.ts +351 -0
  146. package/src/browser/shared-text-provider.ts +714 -0
  147. package/src/browser/shared-text-table-contract.ts +150 -0
  148. package/src/browser/shared-text.ts +19 -0
  149. package/src/capabilities/server-invoke.ts +2 -11
  150. package/src/capabilities/testing.ts +5 -2
  151. package/src/cli/access.ts +6 -5
  152. package/src/cli/admin/ai-models.ts +199 -0
  153. package/src/cli/admin/ai-quotas.ts +6 -102
  154. package/src/cli/admin/ai-usage.ts +4 -4
  155. package/src/cli/admin/app-credentials.ts +5 -3
  156. package/src/cli/admin/index.ts +5 -2
  157. package/src/cli/apps.ts +89 -26
  158. package/src/cli/capabilities.ts +22 -1
  159. package/src/cli/events.ts +320 -0
  160. package/src/config/env.ts +40 -0
  161. package/src/contracts/app-access.ts +26 -2
  162. package/src/contracts/app-details.ts +89 -0
  163. package/src/contracts/app-events.ts +51 -0
  164. package/src/contracts/app.ts +21 -5
  165. package/src/contracts/capabilities.ts +143 -3
  166. package/src/contracts/index.ts +2 -0
  167. package/src/contracts/notification-types.ts +36 -9
  168. package/src/contracts/pwa.ts +17 -1
  169. package/src/contracts/registry.ts +6 -0
  170. package/src/contracts/resource-clipboard.ts +35 -0
  171. package/src/contracts/resource-pages.ts +140 -0
  172. package/src/contracts/resource-resolution.ts +13 -0
  173. package/src/contracts/shared.ts +6 -1
  174. package/src/contracts/user-notifications.ts +2 -0
  175. package/src/events/app-event-delivery.ts +100 -0
  176. package/src/events/app-events.testing.ts +65 -0
  177. package/src/events/app-events.ts +134 -0
  178. package/src/events/core-user-created.ts +24 -0
  179. package/src/events/index.ts +9 -0
  180. package/src/events/testing.ts +2 -0
  181. package/src/index.ts +1 -1
  182. package/src/server/api/respond.ts +17 -4
  183. package/src/server/index.ts +2 -0
  184. package/src/server/middleware/app-access.ts +1 -1
  185. package/src/server/middleware/auth.ts +21 -7
  186. package/src/server/middleware/index.ts +1 -0
  187. package/src/server/middleware/invocation.ts +2 -0
  188. package/src/server/public-storage.ts +106 -0
  189. package/src/server/services/access.ts +31 -10
  190. package/src/server/services/index.ts +1 -0
  191. package/src/services/accounts/app.ts +36 -13
  192. package/src/services/accounts/entities.ts +43 -19
  193. package/src/services/accounts/group-sql.ts +23 -0
  194. package/src/services/accounts/groups.ts +19 -14
  195. package/src/services/accounts/identities.ts +70 -11
  196. package/src/services/accounts/identity-reconciliation.ts +3 -1
  197. package/src/services/accounts/local-groups.ts +69 -4
  198. package/src/services/accounts/users.ts +45 -14
  199. package/src/services/app-access.ts +25 -7
  200. package/src/services/app-details.ts +224 -0
  201. package/src/services/app-discovery.ts +2 -1
  202. package/src/services/app-event-shapes.ts +139 -0
  203. package/src/services/app-events/accept.ts +116 -0
  204. package/src/services/app-events/admin.ts +250 -0
  205. package/src/services/app-events/declarations.ts +187 -0
  206. package/src/services/app-events/deliver.ts +218 -0
  207. package/src/services/app-events.ts +92 -0
  208. package/src/services/config-conflict.ts +11 -0
  209. package/src/services/core-user-events.ts +1 -0
  210. package/src/services/freeipa-config.ts +9 -0
  211. package/src/services/geo/ledger.ts +28 -10
  212. package/src/services/geo/service.ts +43 -24
  213. package/src/services/identity/core-origin.ts +12 -0
  214. package/src/services/identity/invocation-actor.ts +20 -2
  215. package/src/services/identity/invocation-authority.ts +4 -0
  216. package/src/services/identity/invocation-token.ts +238 -56
  217. package/src/services/identity/issue-invocation.ts +6 -1
  218. package/src/services/image-metadata.ts +65 -1
  219. package/src/services/index.ts +13 -1
  220. package/src/services/ipa/sync.ts +6 -2
  221. package/src/services/ipa/users.ts +4 -1
  222. package/src/services/mandates/index.ts +2 -2
  223. package/src/services/mandates/policy.ts +20 -12
  224. package/src/services/notifications/browser.ts +4 -0
  225. package/src/services/notifications/catalog.ts +5 -2
  226. package/src/services/notifications/platform.ts +7 -2
  227. package/src/services/notifications/user.ts +18 -7
  228. package/src/services/oauth-tokens.ts +28 -2
  229. package/src/services/object-store.ts +217 -0
  230. package/src/services/providers/local/users.ts +5 -0
  231. package/src/services/pwa-devices.ts +147 -37
  232. package/src/services/session/index.ts +26 -3
  233. package/src/services/settings/core-settings.ts +71 -43
  234. package/src/services/settings/index.ts +8 -0
  235. package/src/services/settings/versions.ts +26 -0
  236. package/src/shared/ai-models.ts +274 -0
  237. package/src/shared/ai-quotas.ts +2 -2
  238. package/src/shared/ai-usage.ts +2 -2
  239. package/src/shared/email-html.ts +1 -0
  240. package/src/shared/index.ts +2 -0
  241. package/src/shared/markdown/extensions/katex.ts +8 -1
  242. package/src/shared/markdown/extensions/tables.ts +14 -1
  243. package/src/shared/markdown/index.ts +22 -11
  244. package/src/shared-text/anchors.ts +29 -0
  245. package/src/shared-text/awareness.ts +87 -0
  246. package/src/shared-text/changes.ts +27 -0
  247. package/src/shared-text/documents.ts +433 -65
  248. package/src/shared-text/events.ts +12 -2
  249. package/src/shared-text/index.ts +16 -1
  250. package/src/shared-text/log.ts +24 -5
  251. package/src/shared-text/snapshots.ts +69 -23
  252. package/src/shared-text/structural-history.ts +377 -0
  253. package/src/shared-text/table-actions.ts +432 -0
  254. package/src/shared-text/text-diff.ts +75 -0
  255. package/src/shared-text/types.ts +24 -5
  256. package/src/ssr/AdminArea.tsx +131 -0
  257. package/src/ssr/AdminLayout.tsx +9 -3
  258. package/src/ssr/AdminSidebar.tsx +7 -7
  259. package/src/ssr/AppAccessDenied.tsx +2 -0
  260. package/src/ssr/AppBadge.tsx +14 -0
  261. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  262. package/src/ssr/CloudShell.island.tsx +44 -7
  263. package/src/ssr/Layout.tsx +9 -7
  264. package/src/ssr/MobileProfileActions.tsx +2 -2
  265. package/src/ssr/ProfilePreferences.tsx +2 -1
  266. package/src/ssr/PwaLayout.tsx +16 -1
  267. package/src/ssr/PwaPartCount.island.tsx +23 -0
  268. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  269. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  270. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  271. package/src/ssr/admin-active-link.ts +16 -0
  272. package/src/ssr/admin-navigation.ts +26 -31
  273. package/src/ssr/app-badges.ts +31 -7
  274. package/src/ssr/app-launchpad.tsx +6 -1
  275. package/src/ssr/index.ts +8 -0
  276. package/src/ssr/page-trail-runtime.ts +3 -1
  277. package/src/ssr/platform-messages.ts +11 -1
  278. package/src/ssr/profile-actions.ts +3 -6
  279. package/src/ssr/pwa-messages.ts +2 -2
  280. package/src/ssr/rail-navigation.ts +5 -1
  281. package/src/ssr/settings-draft.ts +153 -0
  282. package/src/ssr/shell-data.tsx +3 -1
  283. package/src/styles/global.css +1 -0
  284. package/src/styles/profile-picture.css +270 -0
  285. package/src/styles/utilities-navigation.css +6 -0
  286. package/src/workflows/ai/policy.ts +49 -0
  287. package/src/workflows/ai/runtime.ts +131 -21
  288. package/src/workflows/ai/store.ts +283 -35
  289. package/src/workflows/ai/types.ts +8 -2
  290. package/src/workflows/ai-actions.ts +148 -120
  291. package/src/workflows/authoring.ts +31 -0
  292. package/src/workflows/contracts.ts +1 -0
  293. package/src/workflows/definition.ts +2 -0
  294. package/src/workflows/editor.ts +15 -0
  295. package/src/workflows/index.ts +2 -1
  296. package/src/workflows/language/compiler.ts +34 -3
  297. package/src/workflows/language/expressions.ts +3 -2
  298. package/src/workflows/runtime/executor.ts +134 -34
  299. package/src/workflows/runtime/index.ts +2 -0
  300. package/src/workflows/runtime/ports.ts +5 -1
  301. package/src/workflows/store/actions.ts +1 -1
  302. package/src/workflows/store/definitions.ts +5 -0
  303. package/src/workflows/store/runs.ts +5 -3
  304. package/src/workflows/store/worker.ts +7 -2
  305. package/src/ai/model-pricing.ts +0 -43
  306. package/src/ai/model-request-settings.ts +0 -98
  307. package/src/ai/settings-messages.ts +0 -129
  308. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -0,0 +1,131 @@
1
+ import { AppWorkspace, StatusBadge } from "@k2b/ui";
2
+ import type { JSX } from "solid-js";
3
+ import AdminLayout from "./AdminLayout";
4
+ import SettingsSaveStatus from "./SettingsSaveStatus.island";
5
+
6
+ /** One row of an area's first column. `mark` shows a dot when the section needs attention. */
7
+ export type AdminAreaItem = {
8
+ id: string;
9
+ label: string;
10
+ href: string;
11
+ icon: string;
12
+ description?: string;
13
+ mark?: "warning" | "error";
14
+ };
15
+ export type AdminAreaGroup = { title?: string; items: AdminAreaItem[] };
16
+
17
+ type AdminAreaProps = {
18
+ c: Parameters<typeof AdminLayout>[0]["c"];
19
+ /** The area's name, the head of the first column. */
20
+ label: string;
21
+ /** Accessible name of the first column's navigation, for example "Observability sections". */
22
+ sectionsLabel: string;
23
+ /** The area's start; the first column's head links there. */
24
+ href: string;
25
+ groups: AdminAreaGroup[];
26
+ /** Id of the open section's item. */
27
+ current: string;
28
+ /** Page title, usually the open section's label. */
29
+ title: string;
30
+ /** Spoken with a mark in the first column, for example "Needs attention". */
31
+ markLabel?: string;
32
+ /** Id of the deepest open level: "section", or a deeper pane id the page adds. */
33
+ level?: string;
34
+ /** The section's panes, starting with one whose id is "section". */
35
+ children: JSX.Element;
36
+ };
37
+
38
+ /**
39
+ * An administration area: the admin layout, the area's sections as the first column, and the open section beside
40
+ * it as `AppWorkspace` levels. The owner builds each section's content by hand; a wide list adds `minSize`, so it
41
+ * pushes the first column aside instead of being squeezed.
42
+ */
43
+ export function AdminArea(props: AdminAreaProps) {
44
+ return (
45
+ <AdminLayout c={props.c} title={props.title} ownMain>
46
+ <AppWorkspace.Main level={props.level ?? "section"}>
47
+ <AppWorkspace.MainPane id="area" label={props.label} href={props.href} defaultSize={248} minSize={232} maxSize={320}>
48
+ <nav aria-label={props.sectionsLabel}>
49
+ {props.groups.map((group) => (
50
+ <AppWorkspace.SidebarSection title={group.title}>
51
+ {group.items.map((item) => (
52
+ <AppWorkspace.SidebarItem
53
+ href={item.href}
54
+ navigation="document"
55
+ active={props.current === item.id}
56
+ icon={item.icon}
57
+ description={item.description}
58
+ meta={
59
+ item.mark ? (
60
+ <span title={props.markLabel}>
61
+ <StatusBadge tone={item.mark} variant="dot" label="" />
62
+ <span class="sr-only">{props.markLabel}</span>
63
+ </span>
64
+ ) : undefined
65
+ }
66
+ >
67
+ {item.label}
68
+ </AppWorkspace.SidebarItem>
69
+ ))}
70
+ </AppWorkspace.SidebarSection>
71
+ ))}
72
+ </nav>
73
+ </AppWorkspace.MainPane>
74
+ {props.children}
75
+ </AppWorkspace.Main>
76
+ </AdminLayout>
77
+ );
78
+ }
79
+
80
+ /** The save status in a level head. A phone has no room there; `AdminAreaSaveStatusFoot` shows it at the bottom. */
81
+ export const AdminAreaSaveStatusHead = () => (
82
+ <div class="hidden md:block">
83
+ <SettingsSaveStatus />
84
+ </div>
85
+ );
86
+ export const AdminAreaSaveStatusFoot = () => (
87
+ <div class="sticky bottom-0 flex justify-end bg-(--k2b-surface) px-4 py-3 md:hidden">
88
+ <SettingsSaveStatus />
89
+ </div>
90
+ );
91
+
92
+ /** A section that fills the width beside the first column, such as a dashboard or one form. */
93
+ export function AdminAreaPane(props: {
94
+ label: string;
95
+ description?: JSX.Element;
96
+ /** Before the title, such as a step back inside the section. */
97
+ lead?: JSX.Element;
98
+ actions?: JSX.Element;
99
+ /** A form that saves with a button: its status, Discard and Save sit in the head, on a phone at the bottom. */
100
+ saveStatus?: boolean;
101
+ /** With a detail level beside it: the address of this level without the selection. */
102
+ href?: string;
103
+ /** A wide list declares the room it needs; the first column then gives way before the list is squeezed. */
104
+ minSize?: number;
105
+ children: JSX.Element;
106
+ }) {
107
+ return (
108
+ <AppWorkspace.MainPane
109
+ id="section"
110
+ label={props.label}
111
+ description={props.description}
112
+ lead={props.lead}
113
+ actions={
114
+ props.saveStatus ? (
115
+ <div class="flex items-center gap-2">
116
+ {props.actions}
117
+ <AdminAreaSaveStatusHead />
118
+ </div>
119
+ ) : (
120
+ props.actions
121
+ )
122
+ }
123
+ href={props.href}
124
+ minSize={props.minSize}
125
+ defaultSize={props.minSize}
126
+ >
127
+ <div class="app-rows px-[var(--ui-space-shell)] pb-[var(--ui-space-shell)]">{props.children}</div>
128
+ {props.saveStatus ? <AdminAreaSaveStatusFoot /> : null}
129
+ </AppWorkspace.MainPane>
130
+ );
131
+ }
@@ -30,6 +30,8 @@ type Props = {
30
30
  scroll?: boolean;
31
31
  /** Page-owned insets when its layout needs more room than the default shell spacing. */
32
32
  contentClass?: string;
33
+ /** The page renders its own `AppWorkspace.Main`, such as one in level mode. */
34
+ ownMain?: boolean;
33
35
  };
34
36
  export default function AdminLayout(props: Props) {
35
37
  // Keep children lazy so Layout installs the request locale before rendering them.
@@ -48,9 +50,13 @@ export default function AdminLayout(props: Props) {
48
50
  <AppWorkspace mobileSurface="flush" class="min-h-0 flex-1" resizable={false}>
49
51
  <AdminSidebar currentPath={currentPath} apps={runtime.apps} />
50
52
  <AppWorkspace.Content>
51
- <AppWorkspace.Main scroll={scroll} class={props.contentClass ?? (scroll ? "p-[var(--ui-space-shell)]" : undefined)}>
52
- {props.children}
53
- </AppWorkspace.Main>
53
+ {props.ownMain ? (
54
+ props.children
55
+ ) : (
56
+ <AppWorkspace.Main scroll={scroll} class={props.contentClass ?? (scroll ? "p-[var(--ui-space-shell)]" : undefined)}>
57
+ {props.children}
58
+ </AppWorkspace.Main>
59
+ )}
54
60
  </AppWorkspace.Content>
55
61
  </AppWorkspace>
56
62
  </Layout>
@@ -1,15 +1,15 @@
1
1
  import { AppWorkspace, useLocale } from "@k2b/ui";
2
- import { activeAdminHref } from "./admin-active-link";
2
+ import { activeAdminLink } from "./admin-active-link";
3
3
  import { type AdminLink, buildAdminGroups } from "./admin-navigation";
4
4
  import { platformMessages } from "./platform-messages";
5
5
  import type { RuntimeContext } from "./runtime";
6
6
  import WorkspaceNavigation from "./WorkspaceNavigation.island";
7
7
 
8
8
  const AdminNavigation = (props: { currentPath: string; groups: ReturnType<typeof buildAdminGroups> }) => {
9
- const activeHref = activeAdminHref(
9
+ const activeHref = activeAdminLink(
10
10
  props.currentPath,
11
- props.groups.flatMap((group) => group.links.map((link: AdminLink) => link.href)),
12
- );
11
+ props.groups.flatMap((group) => group.links),
12
+ )?.href;
13
13
  return (
14
14
  <>
15
15
  {props.groups.map((group) => (
@@ -45,10 +45,10 @@ export default function AdminSidebar({ currentPath, apps }: { currentPath: strin
45
45
  icon: `ti ${link.icon}`,
46
46
  active:
47
47
  link.href ===
48
- activeAdminHref(
48
+ activeAdminLink(
49
49
  currentPath,
50
- groups().flatMap((entry) => entry.links.map((entry) => entry.href)),
51
- ),
50
+ groups().flatMap((entry) => entry.links),
51
+ )?.href,
52
52
  })),
53
53
  }))}
54
54
  />
@@ -40,6 +40,8 @@ export function AppAccessDeniedContent(props: {
40
40
  <p class="mb-4 text-[15px] text-secondary">
41
41
  {props.revoked ? (
42
42
  t().revokedDescription
43
+ ) : props.app.access?.kind === "resources" ? (
44
+ t().deniedResources
43
45
  ) : grants() ? (
44
46
  t().deniedGrants
45
47
  ) : (
@@ -18,6 +18,20 @@ export const useAppBadgeDescription = (input: () => { endpoint?: string; appId?:
18
18
  * takes layout space, so appearing, growing to "99+", or clearing moves
19
19
  * nothing. Decorative: the link carries the description.
20
20
  */
21
+ /**
22
+ * The count on a phone bar item: the rail's count, laid over the tab icon's corner, and its description for screen
23
+ * readers after the item's label.
24
+ */
25
+ export function TabBarAppBadge(props: { endpoint?: string; appId?: string }) {
26
+ const description = useAppBadgeDescription(() => props);
27
+ return (
28
+ <Show when={description()}>
29
+ <AppBadge endpoint={props.endpoint} appId={props.appId} />
30
+ <span class="sr-only">, {description()}</span>
31
+ </Show>
32
+ );
33
+ }
34
+
21
35
  export function AppBadge(props: { endpoint?: string; appId?: string }) {
22
36
  const count = () => appBadgeCount(props);
23
37
  return (
@@ -29,7 +29,7 @@ export type AppLaunchpadLegalLink = {
29
29
  };
30
30
 
31
31
  export type AppLaunchpadContext = {
32
- profile?: { name: string; theme: CloudThemePreference; appHref?: string };
32
+ profile?: { userId?: string; name: string; theme: CloudThemePreference; appHref?: string };
33
33
  apps: AppLaunchpadApp[];
34
34
  legalLinks: AppLaunchpadLegalLink[];
35
35
  };
@@ -42,7 +42,7 @@ const appIconPalette: readonly [AppIconPaletteEntry, ...AppIconPaletteEntry[]] =
42
42
  { from: "#7c3aed" },
43
43
  { from: "#d97706" },
44
44
  { from: "#e11d48" },
45
- { from: "#0891b2" },
45
+ { from: "#c026d3" },
46
46
  { from: "#52525b" },
47
47
  ];
48
48
 
@@ -1,6 +1,8 @@
1
1
  import { TabBar, useLocale } from "@k2b/ui";
2
2
  import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from "solid-js";
3
3
  import { isServer, Portal } from "solid-js/web";
4
+ import { TabBarAppBadge } from "./AppBadge";
5
+ import { appBadgeCount } from "./app-badges";
4
6
  import { AppLaunchpadButton, setAppLaunchpadContext } from "./app-launchpad";
5
7
  import GlobalSearchTrigger from "./GlobalSearchTrigger";
6
8
  import LayoutBreadcrumbs from "./LayoutBreadcrumbs";
@@ -35,8 +37,9 @@ export default function CloudShell(props: CloudShellProps) {
35
37
  /**
36
38
  * The phone bar's live copy, mounted with the rail's island so the shell adds no island instance. It renders nothing
37
39
  * while the server's bar is current, so mounting moves and refocuses nothing. When the saved rail preferences or the
38
- * URL (a fragment, Back and Forward, an in-page navigation) change what the bar shows, it renders the new bar into the
39
- * bar's row, and CSS hides the server's.
40
+ * URL (a fragment, Back and Forward, an in-page navigation) change what the bar shows, or an app in it has a count,
41
+ * it renders the new bar into the bar's row, and CSS hides the server's. Counts, like the rail's, exist only in the
42
+ * browser.
40
43
  */
41
44
  function BarNavigation(props: { currentPath: string }) {
42
45
  if (isServer) return null;
@@ -51,6 +54,12 @@ function BarNavigation(props: { currentPath: string }) {
51
54
  phoneBarItems(projectRailNavigation(data.apps, rail, locale()), path, { label: t().home, path: data.home ?? "/" });
52
55
  const served = JSON.stringify(itemsFor(initial, props.currentPath));
53
56
  const items = createMemo(() => itemsFor(settings(), currentPath()));
57
+ const counted = () => items().some((item) => item.counted && appBadgeCount(item.counted) > 0);
58
+ const live = createMemo(() =>
59
+ items().map((item) =>
60
+ item.counted ? { ...item, badge: <TabBarAppBadge endpoint={item.counted.endpoint} appId={item.counted.appId} /> } : item,
61
+ ),
62
+ );
54
63
  onMount(() => {
55
64
  const sync = () => setSettings(readRailContext()?.settings ?? initial);
56
65
  const syncLocation = () => setCurrentPath(`${window.location.pathname}${window.location.search}${window.location.hash}`);
@@ -76,10 +85,22 @@ function BarNavigation(props: { currentPath: string }) {
76
85
  });
77
86
  const row = document.querySelector<HTMLElement>(".cloud-tab-bar__row");
78
87
  if (!row) return null;
88
+ // Swapping bars keeps a focused link: the bar that takes over focuses the same destination, once CSS shows it.
89
+ const [shown, setShown] = createSignal(false);
90
+ const focusIn = (selector: string, tab: string) =>
91
+ [...row.querySelectorAll<HTMLElement>(`${selector} a[data-tab]`)].find((link) => link.dataset.tab === tab)?.focus();
92
+ createEffect(() => {
93
+ const next = counted() || JSON.stringify(items()) !== served;
94
+ if (next === shown()) return;
95
+ const focused = document.activeElement;
96
+ const tab = focused instanceof HTMLElement && row.contains(focused) ? focused.dataset.tab : undefined;
97
+ setShown(next);
98
+ if (tab) queueMicrotask(() => focusIn(next ? ".cloud-tab-bar__live" : ".k2b-tab-bar:not(.cloud-tab-bar__live)", tab));
99
+ });
79
100
  return (
80
- <Show when={JSON.stringify(items()) !== served}>
101
+ <Show when={shown()}>
81
102
  <Portal mount={row}>
82
- <TabBar variant="floating" class="cloud-tab-bar__live" label={t().apps} items={items()} />
103
+ <TabBar variant="floating" class="cloud-tab-bar__live" label={t().apps} items={live()} />
83
104
  </Portal>
84
105
  </Show>
85
106
  );
@@ -90,7 +111,12 @@ function RailNavigation(props: { currentPath: string; panelOpen?: boolean }) {
90
111
  const locale = useLocale();
91
112
  const t = () => platformMessages.resolve([locale()]).t;
92
113
  if (!data?.rail || !data.profile) return null;
93
- const profile = { name: data.profile.name, theme: data.theme, ...(data.profile.appHref ? { appHref: data.profile.appHref } : {}) };
114
+ const profile = {
115
+ userId: data.profile.userId,
116
+ name: data.profile.name,
117
+ theme: data.theme,
118
+ ...(data.profile.appHref ? { appHref: data.profile.appHref } : {}),
119
+ };
94
120
  createEffect(() => setAppLaunchpadContext(data.apps, data.legalLinks, profile));
95
121
  return (
96
122
  <nav class="layout-rail-navigation flex min-h-0 flex-1 flex-col items-center gap-1" aria-label={t().apps}>
@@ -107,6 +133,7 @@ function RailNavigation(props: { currentPath: string; panelOpen?: boolean }) {
107
133
  helpOpen={!!props.panelOpen && data.panel?.provider === "core"}
108
134
  />
109
135
  <ProfilePreferences
136
+ userId={data.profile.userId}
110
137
  appHref={data.profile.appHref}
111
138
  avatarSrc={data.profile.avatarSrc}
112
139
  initialTheme={data.theme}
@@ -143,7 +170,12 @@ function HeaderControls(props: {
143
170
  <AppLaunchpadButton
144
171
  profile={
145
172
  data.profile
146
- ? { name: data.profile.name, theme: data.theme, ...(data.profile.appHref ? { appHref: data.profile.appHref } : {}) }
173
+ ? {
174
+ userId: data.profile.userId,
175
+ name: data.profile.name,
176
+ theme: data.theme,
177
+ ...(data.profile.appHref ? { appHref: data.profile.appHref } : {}),
178
+ }
147
179
  : undefined
148
180
  }
149
181
  apps={data.apps}
@@ -167,7 +199,12 @@ function HeaderControls(props: {
167
199
  <AppLaunchpadButton
168
200
  profile={
169
201
  data.profile
170
- ? { name: data.profile.name, theme: data.theme, ...(data.profile.appHref ? { appHref: data.profile.appHref } : {}) }
202
+ ? {
203
+ userId: data.profile.userId,
204
+ name: data.profile.name,
205
+ theme: data.theme,
206
+ ...(data.profile.appHref ? { appHref: data.profile.appHref } : {}),
207
+ }
171
208
  : undefined
172
209
  }
173
210
  apps={data.apps}
@@ -14,6 +14,7 @@ import { isAppSessionActor } from "../server/actor";
14
14
  import { getLocale } from "../server/locale";
15
15
  import { getDateConfig } from "../server/time";
16
16
  import { dates, resolveHomePath } from "../shared";
17
+ import { publicCloudOrigin } from "../shared/app-url";
17
18
  import { readThemePreferenceFromCookieHeader } from "../shared/theme";
18
19
  import AnnouncementBanners from "./AnnouncementBanners";
19
20
  import { appAppearanceStyle, resolveCurrentApp } from "./app-appearance";
@@ -258,7 +259,9 @@ export default function Layout(props: LayoutProps) {
258
259
  const panel: ShellData["panel"] = providers.length
259
260
  ? { providers, provider: panelState.provider ?? providers[0]?.id, context: pageContext, consent: c.get("panelConsent") }
260
261
  : undefined;
262
+ const cloudOrigin = settings?.app?.url ? publicCloudOrigin(settings.app.url) : undefined;
261
263
  const shellData: ShellData = {
264
+ ...(cloudOrigin ? { cloudOrigin } : {}),
262
265
  ...(panel ? { panel } : {}),
263
266
  platform: keyboardPlatform(c.req.raw.headers.get("User-Agent") ?? ""),
264
267
  ...(viewHelp.length ? { help: viewHelp } : {}),
@@ -268,7 +271,7 @@ export default function Layout(props: LayoutProps) {
268
271
  ? {
269
272
  rail: c.get("railPreferences") ?? defaultRailPreferences(),
270
273
  home: resolveHomePath(settings?.app?.home_path),
271
- profile: { name: profileName, avatarSrc: profileAvatarSrc, appHref },
274
+ profile: { userId: user.id, name: profileName, avatarSrc: profileAvatarSrc, appHref },
272
275
  }
273
276
  : {}),
274
277
  legalLinks,
@@ -294,6 +297,7 @@ export default function Layout(props: LayoutProps) {
294
297
  apps: allApps,
295
298
  legalLinks,
296
299
  searchLinks,
300
+ cloudOrigin: shellData.cloudOrigin,
297
301
  home: shellData.home,
298
302
  profile: shellData.profile,
299
303
  announcements: user ? (announcements?.latestAnnouncementVersion ?? null) : null,
@@ -325,12 +329,10 @@ export default function Layout(props: LayoutProps) {
325
329
  <AppWorkspace.LayoutStateProvider state={workspaceLayout}>{props.children}</AppWorkspace.LayoutStateProvider>
326
330
  </main>
327
331
  </PageTrailProvider>
328
- {/* Focus pages carry shell data only for their panel providers. */}
329
- {panel && (
330
- <ShellDataProvider data={{ panel, platform: shellData.platform, theme, apps: [], legalLinks: [], searchLinks: {} }}>
331
- <PanelSlot panel={panel} state={{ ...panelState, open: false }} platform={shellData.platform} />
332
- </ShellDataProvider>
333
- )}
332
+ {/* Focus pages also expose the canonical origin to their islands. */}
333
+ <ShellDataProvider data={{ cloudOrigin, panel, platform: shellData.platform, theme, apps: [], legalLinks: [], searchLinks: {} }}>
334
+ {panel && <PanelSlot panel={panel} state={{ ...panelState, open: false }} platform={shellData.platform} />}
335
+ </ShellDataProvider>
334
336
  <div data-print-fallback="">
335
337
  <p>{t.printFallbackTitle}</p>
336
338
  <p>{props.printHint ?? t.printFallbackHint}</p>
@@ -5,10 +5,10 @@ import { createProfileActions } from "./profile-actions";
5
5
  import { ThemeCycleButton } from "./ThemeCycleButton";
6
6
 
7
7
  export function MobileProfileActions(props: {
8
- profile: { name: string; theme: CloudThemePreference; appHref?: string };
8
+ profile: { userId?: string; name: string; theme: CloudThemePreference; appHref?: string };
9
9
  beforeSelect: () => Promise<boolean>;
10
10
  }) {
11
- const { preferences, signingOut, signOut } = createProfileActions(props.profile.theme, props.profile.appHref);
11
+ const { preferences, signingOut, signOut } = createProfileActions(props.profile.theme, props.profile.appHref, props.profile.userId);
12
12
  const navigate = (href: string) => (event: MouseEvent) => {
13
13
  if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
14
14
  event.preventDefault();
@@ -3,6 +3,7 @@ import type { CloudThemePreference } from "../shared/theme";
3
3
  import { createProfileActions } from "./profile-actions";
4
4
 
5
5
  type ProfilePreferencesProps = {
6
+ userId?: string;
6
7
  /** Set while the installation offers the mobile app. */
7
8
  appHref?: string;
8
9
  avatarSrc?: string;
@@ -16,7 +17,7 @@ type ProfilePreferencesProps = {
16
17
  * bottom edge.
17
18
  */
18
19
  export default function ProfilePreferences(props: ProfilePreferencesProps) {
19
- const { preferences, items, signOut, signingOut } = createProfileActions(props.initialTheme, props.appHref);
20
+ const { preferences, items, signOut, signingOut } = createProfileActions(props.initialTheme, props.appHref, props.userId);
20
21
 
21
22
  const avatar = () => <Avatar name={props.name} src={props.avatarSrc} size="sm" />;
22
23
 
@@ -7,6 +7,7 @@ import { pwaBuild } from "../contracts/pwa-builds";
7
7
  import { getLocale } from "../server/locale";
8
8
  import { visiblePwaParts } from "./app-navigation";
9
9
  import type { LayoutContext } from "./layout-context";
10
+ import PwaPartCount from "./PwaPartCount.island";
10
11
  import PwaRuntime from "./PwaRuntime.island";
11
12
  import { pwaMessages } from "./pwa-messages";
12
13
  import { getDisplayRuntimeContext } from "./runtime";
@@ -62,12 +63,24 @@ export default function PwaLayout(props: PwaLayoutProps) {
62
63
  const ownBuild = pwaBuild(buildMetadata);
63
64
  const coreBuild = registeredPwaBuilds().core;
64
65
  const builds = { ...(owner && ownBuild ? { [owner]: ownBuild } : {}), ...(coreBuild ? { core: coreBuild } : {}) };
66
+ // A part's badge calls for attention in the sidebar and on its tab; without one, its summary's count stays quiet in
67
+ // the sidebar. One watcher reads every route of the page.
68
+ const countRoutes = [...new Set(parts.flatMap((part) => [part.pwa.badge, part.pwa.summary].filter((path) => path !== undefined)))];
69
+ const badge = (part: (typeof parts)[number]) =>
70
+ part.pwa.badge ? <PwaPartCount endpoint={part.pwa.badge} endpoints={countRoutes} attention /> : undefined;
65
71
  const tabs = parts.slice(0, TAB_PARTS);
66
72
  const currentTab = tabs.find((part) => path === part.pwa.href || path.startsWith(`${part.pwa.href}/`));
67
73
  // Start is titled with the installation's name; a part's first page carries the part's name.
68
74
  const items: TabBarItem[] = [
69
75
  { id: "start", label: t.start, title: cloud, icon: "ti ti-home", href: PWA_SCOPE, current: !currentTab },
70
- ...tabs.map((part) => ({ id: part.id, label: part.name, icon: part.icon, href: part.pwa.href, current: part === currentTab })),
76
+ ...tabs.map((part) => ({
77
+ id: part.id,
78
+ label: part.name,
79
+ icon: part.icon,
80
+ href: part.pwa.href,
81
+ current: part === currentTab,
82
+ badge: badge(part),
83
+ })),
71
84
  ];
72
85
  // The sidebar names the installation itself, so Start is titled Start there; Settings is a destination of its own.
73
86
  const settingsHref = `${PWA_SCOPE}settings`;
@@ -87,6 +100,8 @@ export default function PwaLayout(props: PwaLayoutProps) {
87
100
  icon: part.icon,
88
101
  href: part.pwa.href,
89
102
  current: part === currentPart,
103
+ badge: badge(part),
104
+ count: part.pwa.summary ? <PwaPartCount endpoint={part.pwa.summary} endpoints={countRoutes} /> : undefined,
90
105
  })),
91
106
  },
92
107
  ]}
@@ -0,0 +1,23 @@
1
+ import { AppWorkspace } from "@k2b/ui";
2
+ import { onCleanup, onMount, Show } from "solid-js";
3
+ import { appBadgeCount, watchAppBadges } from "./app-badges";
4
+
5
+ export type PwaPartCountProps = {
6
+ /** This part's `pwa.badge` or, without one, its `pwa.summary` route. */
7
+ endpoint: string;
8
+ /** Every count route of the page; the first count on a page reads them all, once a minute like the web's. */
9
+ endpoints: string[];
10
+ /** A badge calls for attention, such as unread messages; a summary's count stays quiet beside the label. */
11
+ attention?: boolean;
12
+ };
13
+
14
+ /** The count of a part in the Cloud app's sidebar and tab bar; nothing at zero. */
15
+ export default function PwaPartCount(props: PwaPartCountProps) {
16
+ onMount(() => onCleanup(watchAppBadges(props.endpoints)));
17
+ const count = () => appBadgeCount({ endpoint: props.endpoint });
18
+ return (
19
+ <Show when={props.attention} fallback={<Show when={count() > 0}>{count() > 99 ? "99+" : count()}</Show>}>
20
+ <AppWorkspace.SidebarItemStatus unread={count()} />
21
+ </Show>
22
+ );
23
+ }
@@ -0,0 +1,58 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+ import { Button, InlineGuidance, useLocale } from "@k2b/ui";
3
+ import { Show } from "solid-js";
4
+ import type { SettingsDraftOutcome } from "./settings-draft";
5
+
6
+ const messages = i18n.define({
7
+ baseLocale: "en",
8
+ messages: {
9
+ en: {
10
+ notSaved: "Not saved.",
11
+ notSavedKeep: "The previous values still apply. Your changes stay here.",
12
+ unknownOutcome: "The connection broke while saving.",
13
+ unknownOutcomeKeep: "Whether the new values apply is open. Cloud does not save again on its own.",
14
+ checkNow: "Check current state",
15
+ retry: "Try again",
16
+ },
17
+ de: {
18
+ notSaved: "Nicht gespeichert.",
19
+ notSavedKeep: "Es gelten weiter die alten Werte. Deine Änderungen bleiben hier stehen.",
20
+ unknownOutcome: "Die Verbindung brach beim Speichern ab.",
21
+ unknownOutcomeKeep: "Ob die neuen Werte gelten, ist offen. Cloud speichert nicht von selbst noch einmal.",
22
+ checkNow: "Stand jetzt prüfen",
23
+ retry: "Erneut versuchen",
24
+ },
25
+ },
26
+ });
27
+
28
+ export const checkSettingsSaveOutcomeMessages = () => messages.check();
29
+
30
+ /**
31
+ * What became of the last save when it did not succeed, above the form: not saved with the reason and a retry, or an
32
+ * unclear outcome with a way to check the current state instead of saving twice.
33
+ */
34
+ export default function SettingsSaveOutcome(props: { outcome: SettingsDraftOutcome | null; onRetry: () => void }) {
35
+ const locale = useLocale();
36
+ const t = () => messages.resolve([locale()]).t;
37
+ return (
38
+ <Show when={props.outcome}>
39
+ {(outcome) =>
40
+ outcome().kind === "failed" ? (
41
+ <InlineGuidance tone="danger" icon="ti ti-alert-triangle">
42
+ <b>{t().notSaved}</b> {outcome().message} {t().notSavedKeep}{" "}
43
+ <Button type="button" variant="ghost" size="xs" onClick={props.onRetry}>
44
+ {t().retry}
45
+ </Button>
46
+ </InlineGuidance>
47
+ ) : (
48
+ <InlineGuidance tone="warning" icon="ti ti-help-circle">
49
+ <b>{t().unknownOutcome}</b> {t().unknownOutcomeKeep}{" "}
50
+ <Button type="button" variant="ghost" size="xs" onClick={() => window.location.reload()}>
51
+ {t().checkNow}
52
+ </Button>
53
+ </InlineGuidance>
54
+ )
55
+ }
56
+ </Show>
57
+ );
58
+ }
@@ -0,0 +1,16 @@
1
+ import { SaveStatus } from "@k2b/ui";
2
+ import { settingsSaveControls } from "./settings-draft";
3
+
4
+ /** Status, Discard and Save in the head of a form level. Until the form registers, it shows "Saved" with both buttons disabled. */
5
+ export default function SettingsSaveStatus() {
6
+ const controls = settingsSaveControls;
7
+ return (
8
+ <SaveStatus
9
+ state={() => controls()?.state ?? "saved"}
10
+ savedAt={() => controls()?.savedAt}
11
+ invalidCount={() => controls()?.invalidCount ?? 1}
12
+ onSave={() => controls()?.save()}
13
+ onDiscard={() => controls()?.discard()}
14
+ />
15
+ );
16
+ }