@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.
- package/package.json +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- 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
|
-
|
|
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=
|
|
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
|
|
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
|
];
|
package/src/ssr/app-badges.ts
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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((
|
|
128
|
+
void result.then((answer) => {
|
|
112
129
|
if (stopped.signal.aborted) return;
|
|
113
|
-
if (
|
|
114
|
-
|
|
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
|
-
|
|
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-
|
|
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",
|
package/src/ssr/pwa-messages.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
):
|
|
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
|
+
};
|
package/src/ssr/shell-data.tsx
CHANGED
|
@@ -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[];
|