@k2b/cloud 0.44.0 → 0.46.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 +9 -2
- package/src/_internal/app-queries.ts +73 -0
- package/src/_internal/capabilities.ts +137 -9
- package/src/_internal/define-app.ts +2 -0
- package/src/_internal/registry-validation.ts +17 -4
- package/src/_internal/registry.ts +32 -4
- package/src/_internal/resource-principals.ts +36 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +11 -2
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/ai/capabilities.ts +4 -2
- package/src/ai/chat/map-messages.ts +270 -0
- package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
- package/src/ai/chat/map-notebook.tsx +301 -0
- package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
- package/src/ai/chat/map-widget.css +107 -0
- package/src/ai/chat/map-widget.tsx +1279 -0
- package/src/ai/chat/message-utils.ts +1 -0
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -1
- package/src/ai/chat/turn-view.tsx +17 -1
- package/src/ai/default-tools.ts +3 -0
- package/src/ai/dictation-runtime.ts +6 -2
- package/src/ai/executor.ts +22 -4
- package/src/ai/index.ts +1 -0
- package/src/ai/map-contracts.ts +209 -0
- package/src/ai/map-execution.ts +14 -0
- package/src/ai/map-notebook-save.ts +276 -0
- package/src/ai/map-notebook.ts +32 -0
- package/src/ai/map-opening-hours.ts +40 -0
- package/src/ai/map-routes.ts +84 -0
- package/src/ai/map-service.ts +288 -0
- package/src/ai/map-store.ts +137 -0
- package/src/ai/map-tool.ts +73 -0
- package/src/ai/migrate.ts +3 -0
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +56 -46
- package/src/ai/projects.ts +100 -79
- package/src/ai/protocol.ts +3 -1
- package/src/ai/recent-resources.ts +3 -3
- package/src/ai/routes.ts +32 -30
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/ai/store.ts +19 -6
- package/src/ai/tools.ts +14 -0
- package/src/ai/types.ts +3 -0
- package/src/api/accounts-entities.ts +6 -3
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/auth.ts +9 -2
- package/src/api/capabilities.ts +90 -12
- package/src/api/mcp.ts +4 -2
- package/src/browser/notifications.ts +25 -8
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/discussion-providers.ts +99 -0
- package/src/capabilities/executions.ts +2 -2
- package/src/capabilities/migrate.ts +10 -1
- package/src/capabilities/resource-readers.ts +58 -0
- package/src/capabilities/server-invoke.ts +23 -13
- package/src/capabilities/server.ts +17 -6
- package/src/capabilities/testing.ts +13 -0
- package/src/contracts/capabilities.ts +137 -4
- package/src/contracts/discussions.ts +91 -0
- package/src/contracts/geo-document.ts +60 -0
- package/src/contracts/geo-snapshot.ts +86 -0
- package/src/contracts/geo.ts +14 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/resource-types.ts +21 -0
- package/src/contracts/shared.ts +2 -1
- package/src/geo/AddressAutocomplete.tsx +213 -0
- package/src/geo/messages.ts +29 -0
- package/src/geo/navigation.ts +25 -0
- package/src/geo/ui.ts +2 -0
- package/src/server/api/respond.ts +10 -2
- package/src/server/index.ts +1 -0
- package/src/server/services/access.ts +23 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +56 -17
- package/src/services/accounts/entities.ts +36 -16
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +26 -17
- package/src/services/accounts/identities.ts +8 -1
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +141 -27
- package/src/services/accounts/machine-group-management.ts +94 -0
- package/src/services/accounts/users.ts +38 -8
- package/src/services/geo/cache.ts +2 -1
- package/src/services/geo/geoapify.ts +3 -0
- package/src/services/identity/invocation-token.ts +110 -0
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +1 -0
- package/src/services/pdf/geo-snapshot.ts +107 -0
- package/src/services/pdf/index.ts +2 -0
- package/src/services/session/index.ts +12 -3
- package/src/shared/email-html.ts +1 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +428 -77
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +2 -2
- package/src/ssr/AdminLayout.tsx +1 -1
- package/src/ssr/AdminSidebar.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +19 -3
- package/src/ssr/Layout.tsx +1 -1
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-navigation.ts +1 -11
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/shell-data.tsx +1 -1
- package/src/styles/global.css +2 -0
- package/src/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +2 -1
|
@@ -2,16 +2,18 @@ import { query } from "@k2b/stdlib/solid";
|
|
|
2
2
|
import { Button, InlineGuidance, useLocale } from "@k2b/ui";
|
|
3
3
|
import { createSignal, createUniqueId, For, Show } from "solid-js";
|
|
4
4
|
import { CloudAvatar } from "../account/Avatar";
|
|
5
|
-
import type {
|
|
5
|
+
import type { EntityListItem, PaginationResponse } from "../contracts/shared";
|
|
6
6
|
import { accessMessages } from "./messages";
|
|
7
|
+
import { serviceAccountKindDisplay } from "./service-account-kind";
|
|
7
8
|
|
|
8
9
|
/** Members per request. Further pages load only when the viewer asks for them. */
|
|
9
10
|
const PAGE_SIZE = 20;
|
|
11
|
+
type GroupMember = Exclude<EntityListItem, { kind: "group" }>;
|
|
10
12
|
|
|
11
13
|
type MembersPage =
|
|
12
14
|
/** The directory withholds group members from this viewer (guest accounts). */
|
|
13
15
|
| { visible: false; directoryGroup: boolean }
|
|
14
|
-
| { visible: true; directoryGroup: boolean; members:
|
|
16
|
+
| { visible: true; directoryGroup: boolean; members: GroupMember[]; total: number; page: number; hasNext: boolean };
|
|
15
17
|
|
|
16
18
|
/** Reads the Accounts directory with the viewer's own visibility: `null` when it is withheld. */
|
|
17
19
|
const listEntities = async (params: Record<string, string>, signal: AbortSignal) => {
|
|
@@ -38,7 +40,7 @@ export const createGroupCoverage = (groupId: string) => {
|
|
|
38
40
|
const page = cursor ?? 1;
|
|
39
41
|
const [list, group] = await Promise.all([
|
|
40
42
|
listEntities(
|
|
41
|
-
{ kinds: "user", member_of_group_id: id, recursive: "true", page: String(page), per_page: String(PAGE_SIZE) },
|
|
43
|
+
{ kinds: "user,service_account", member_of_group_id: id, recursive: "true", page: String(page), per_page: String(PAGE_SIZE) },
|
|
42
44
|
abortSignal,
|
|
43
45
|
),
|
|
44
46
|
// Only the first page needs the group itself, for the directory hint.
|
|
@@ -49,7 +51,7 @@ export const createGroupCoverage = (groupId: string) => {
|
|
|
49
51
|
return {
|
|
50
52
|
visible: true,
|
|
51
53
|
directoryGroup,
|
|
52
|
-
members: list.items.
|
|
54
|
+
members: list.items.filter((item) => item.kind !== "group"),
|
|
53
55
|
total: list.pagination.total,
|
|
54
56
|
page,
|
|
55
57
|
hasNext: list.pagination.has_next,
|
|
@@ -146,7 +148,20 @@ export function GroupCoveragePanel(props: { coverage: GroupCoverage; groupName:
|
|
|
146
148
|
<Show when={shown().length > 0}>
|
|
147
149
|
<ul ref={list} tabindex="-1" aria-label={t().groupMembersOf({ name: props.groupName })} class="flex flex-col gap-1 outline-none">
|
|
148
150
|
<For each={shown()}>
|
|
149
|
-
{(
|
|
151
|
+
{(member) => {
|
|
152
|
+
if (member.kind === "service_account") {
|
|
153
|
+
const kind = () => serviceAccountKindDisplay(member.serviceAccount.kind, t());
|
|
154
|
+
return (
|
|
155
|
+
<li class="flex min-w-0 items-center gap-2 text-sm">
|
|
156
|
+
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-secondary dark:bg-zinc-700">
|
|
157
|
+
<i class={`ti ${kind().icon}`} aria-hidden="true" />
|
|
158
|
+
</span>
|
|
159
|
+
<span class="truncate">{member.serviceAccount.name}</span>
|
|
160
|
+
<span class="shrink-0 text-xs text-muted">{kind().label}</span>
|
|
161
|
+
</li>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
const user = member.user;
|
|
150
165
|
const name = user.displayName || user.mail || user.uid;
|
|
151
166
|
return (
|
|
152
167
|
<li class="flex min-w-0 items-center gap-2 text-sm">
|
|
@@ -449,6 +449,8 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
449
449
|
allowPublic={allowPublic()}
|
|
450
450
|
allowAuthenticated={allowAuthenticated()}
|
|
451
451
|
allowServiceAccounts={props.allowServiceAccounts}
|
|
452
|
+
serviceAccountKinds={props.serviceAccountKinds}
|
|
453
|
+
serviceAccountStatus={props.serviceAccountStatus ?? "active"}
|
|
452
454
|
placeholder={props.placeholder}
|
|
453
455
|
signedIn={props.signedIn}
|
|
454
456
|
disabled={saving()}
|
|
@@ -651,7 +653,7 @@ function AccessEntryRow(props: {
|
|
|
651
653
|
>
|
|
652
654
|
<Show when={!props.expanded} fallback={<PrincipalAvatar entry={props.entry} label={displayName()} />}>
|
|
653
655
|
<Show
|
|
654
|
-
when={props.entry.principal.type === "user"}
|
|
656
|
+
when={props.entry.principal.type === "user" || props.entry.principal.type === "service_account"}
|
|
655
657
|
fallback={
|
|
656
658
|
<div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-secondary dark:bg-zinc-700">
|
|
657
659
|
<i class={`ti ${getPrincipalIcon(props.entry, t())} text-sm`} />
|
|
@@ -660,8 +662,15 @@ function AccessEntryRow(props: {
|
|
|
660
662
|
>
|
|
661
663
|
<CloudAvatar
|
|
662
664
|
username={displayName()}
|
|
663
|
-
userId={
|
|
665
|
+
userId={
|
|
666
|
+
props.entry.principal.type === "user"
|
|
667
|
+
? props.entry.principal.userId
|
|
668
|
+
: props.entry.principal.type === "service_account"
|
|
669
|
+
? props.entry.principal.serviceAccountId
|
|
670
|
+
: undefined
|
|
671
|
+
}
|
|
664
672
|
avatarHash={props.entry.avatarHash}
|
|
673
|
+
kind={props.entry.principal.type === "service_account" ? "service_account" : "user"}
|
|
665
674
|
size="xs"
|
|
666
675
|
class="h-7 w-7"
|
|
667
676
|
/>
|
|
@@ -2,9 +2,9 @@ import { Show } from "solid-js";
|
|
|
2
2
|
import { CloudAvatar } from "../account/Avatar";
|
|
3
3
|
import type { AccessEntry } from "../contracts/shared";
|
|
4
4
|
|
|
5
|
-
// One hue per principal kind from the avatar palette, so a group
|
|
6
|
-
//
|
|
7
|
-
const hues = { group: "#22c55e",
|
|
5
|
+
// One hue per principal kind from the avatar palette, so a group and all signed-in people read apart at a glance.
|
|
6
|
+
// People use a neutral background; a service account shows its robot or photo, never a bare icon.
|
|
7
|
+
const hues = { group: "#22c55e", authenticated: "#3b82f6" } as const;
|
|
8
8
|
|
|
9
9
|
/** The 32px avatar of an access entry in the expanded editor and on the denial page. */
|
|
10
10
|
export default function PrincipalAvatar(props: {
|
|
@@ -15,8 +15,6 @@ export default function PrincipalAvatar(props: {
|
|
|
15
15
|
switch (props.entry.principal.type) {
|
|
16
16
|
case "group":
|
|
17
17
|
return "ti-users-group";
|
|
18
|
-
case "service_account":
|
|
19
|
-
return "ti-robot";
|
|
20
18
|
case "authenticated":
|
|
21
19
|
return "ti-world";
|
|
22
20
|
default:
|
|
@@ -25,27 +23,38 @@ export default function PrincipalAvatar(props: {
|
|
|
25
23
|
};
|
|
26
24
|
const hue = () => {
|
|
27
25
|
const type = props.entry.principal.type;
|
|
28
|
-
return type === "group" || type === "
|
|
26
|
+
return type === "group" || type === "authenticated" ? hues[type] : undefined;
|
|
29
27
|
};
|
|
30
28
|
return (
|
|
31
29
|
<Show
|
|
32
30
|
when={props.entry.principal.type === "user" ? props.entry.principal : undefined}
|
|
33
31
|
fallback={
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
props.entry.principal.type === "service_account" ? (
|
|
33
|
+
<CloudAvatar
|
|
34
|
+
username={props.label}
|
|
35
|
+
userId={props.entry.principal.serviceAccountId}
|
|
36
|
+
avatarHash={props.entry.avatarHash}
|
|
37
|
+
kind="service_account"
|
|
38
|
+
size="sm"
|
|
39
|
+
class="h-8 w-8"
|
|
40
|
+
/>
|
|
41
|
+
) : (
|
|
42
|
+
<span
|
|
43
|
+
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--ui-surface-muted)] text-secondary"
|
|
44
|
+
// The avatar formula: a light share of the hue on the surface, a dark share on the text, in both themes.
|
|
45
|
+
style={
|
|
46
|
+
hue()
|
|
47
|
+
? {
|
|
48
|
+
background: `color-mix(in srgb, ${hue()} 18%, var(--k2b-surface))`,
|
|
49
|
+
color: `color-mix(in srgb, ${hue()} 55%, var(--k2b-text))`,
|
|
50
|
+
}
|
|
51
|
+
: undefined
|
|
52
|
+
}
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
>
|
|
55
|
+
<i class={`ti ${icon()} text-xs`} />
|
|
56
|
+
</span>
|
|
57
|
+
)
|
|
49
58
|
}
|
|
50
59
|
>
|
|
51
60
|
{(user) => (
|
package/src/access/messages.ts
CHANGED
|
@@ -47,8 +47,8 @@ export const accessMessages = i18n.define({
|
|
|
47
47
|
count === 0
|
|
48
48
|
? "No one receives access through this group right now."
|
|
49
49
|
: count === 1
|
|
50
|
-
? "1
|
|
51
|
-
: `${count.toLocaleString("en")}
|
|
50
|
+
? "1 account receives access through this group right now."
|
|
51
|
+
: `${count.toLocaleString("en")} accounts receive access through this group right now.`,
|
|
52
52
|
groupMembersLoading: "Loading members…",
|
|
53
53
|
groupMembersHidden: "Your account can't see who belongs to this group.",
|
|
54
54
|
groupMembersFailed: "Members couldn't be loaded.",
|
|
@@ -137,8 +137,8 @@ export const accessMessages = i18n.define({
|
|
|
137
137
|
count === 0
|
|
138
138
|
? "Über diese Gruppe erhält gerade niemand Zugriff."
|
|
139
139
|
: count === 1
|
|
140
|
-
? "Über diese Gruppe erhält gerade 1
|
|
141
|
-
: `Über diese Gruppe erhalten gerade ${count.toLocaleString("de")}
|
|
140
|
+
? "Über diese Gruppe erhält gerade 1 Konto Zugriff."
|
|
141
|
+
: `Über diese Gruppe erhalten gerade ${count.toLocaleString("de")} Konten Zugriff.`,
|
|
142
142
|
groupMembersLoading: "Mitglieder werden geladen…",
|
|
143
143
|
groupMembersHidden: "Dein Konto kann nicht sehen, wer zu dieser Gruppe gehört.",
|
|
144
144
|
groupMembersFailed: "Die Mitglieder konnten nicht geladen werden.",
|
|
@@ -5,6 +5,7 @@ import { createSignal, For, Show } from "solid-js";
|
|
|
5
5
|
import { accessMessages } from "../access/messages";
|
|
6
6
|
import { principalLabel } from "../access/principal-label";
|
|
7
7
|
import { serviceAccountKindDisplay } from "../access/service-account-kind";
|
|
8
|
+
import type { ServiceAccountKind, ServiceAccountStatus } from "../contracts/shared";
|
|
8
9
|
import { groupDisplayName } from "../shared/account-display";
|
|
9
10
|
import { CloudAvatar } from "./Avatar";
|
|
10
11
|
|
|
@@ -52,6 +53,10 @@ type EntitySearchProps = {
|
|
|
52
53
|
includeGroups?: boolean;
|
|
53
54
|
/** Surface service accounts in the result list. Off by default. */
|
|
54
55
|
includeServiceAccounts?: boolean;
|
|
56
|
+
/** Restrict service-account kinds before pagination. Omitted or empty allows all kinds. */
|
|
57
|
+
serviceAccountKinds?: ServiceAccountKind[];
|
|
58
|
+
/** Restrict service-account status before pagination. Omitted allows both statuses. */
|
|
59
|
+
serviceAccountStatus?: ServiceAccountStatus;
|
|
55
60
|
/** Inject a synthetic "All authenticated users" row at the top. */
|
|
56
61
|
includeAuthenticated?: boolean;
|
|
57
62
|
/** Inject a synthetic "Public" row at the top. */
|
|
@@ -171,6 +176,12 @@ const EntitySearch = (props: EntitySearchProps) => {
|
|
|
171
176
|
if (props.excludeServiceAccountIds?.length) {
|
|
172
177
|
url.searchParams.set("exclude_service_account_ids", props.excludeServiceAccountIds.join(","));
|
|
173
178
|
}
|
|
179
|
+
if (props.includeServiceAccounts && props.serviceAccountKinds?.length) {
|
|
180
|
+
url.searchParams.set("service_account_kinds", props.serviceAccountKinds.join(","));
|
|
181
|
+
}
|
|
182
|
+
if (props.includeServiceAccounts && props.serviceAccountStatus) {
|
|
183
|
+
url.searchParams.set("service_account_status", props.serviceAccountStatus);
|
|
184
|
+
}
|
|
174
185
|
if (props.onlyMembersOf?.length) {
|
|
175
186
|
url.searchParams.set("user_member_of_group_ids", props.onlyMembersOf.join(","));
|
|
176
187
|
}
|
|
@@ -296,6 +307,7 @@ const EntitySearch = (props: EntitySearchProps) => {
|
|
|
296
307
|
{(serviceAccount) => (
|
|
297
308
|
<ResultRow
|
|
298
309
|
icon={serviceAccountKind(serviceAccount).icon}
|
|
310
|
+
avatar={<CloudAvatar username={serviceAccount.name} userId={serviceAccount.id} kind="service_account" size="sm" />}
|
|
299
311
|
title={serviceAccount.name}
|
|
300
312
|
subtitle={
|
|
301
313
|
// Resource-bound accounts often share a name; their binding tells them apart.
|
package/src/ai/capabilities.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { Tool, ToolContext, ToolResolver } from "@k2b/nessi";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { resolveCapabilityActionWording, resolveCapabilityOperationTitle } from "../_internal/capabilities";
|
|
4
|
+
import { isPersonVisibleCapabilityQuery, resolveCapabilityActionWording, resolveCapabilityOperationTitle } from "../_internal/capabilities";
|
|
5
5
|
import { capabilityReviewView } from "../_internal/capability-reviews";
|
|
6
6
|
import {
|
|
7
7
|
CAPABILITY_APPROVAL_REASON_FIELD,
|
|
@@ -164,7 +164,9 @@ export const buildAiCapabilityCatalog = (apps: CapabilityRegistryEntry[], locale
|
|
|
164
164
|
: app.appName;
|
|
165
165
|
return [
|
|
166
166
|
...app.manifest.actions.map((operation) => ({ app, appName, operation, kind: "action" as const })),
|
|
167
|
-
...app.manifest.queries
|
|
167
|
+
...app.manifest.queries
|
|
168
|
+
.filter(isPersonVisibleCapabilityQuery)
|
|
169
|
+
.map((operation) => ({ app, appName, operation, kind: "query" as const })),
|
|
168
170
|
];
|
|
169
171
|
})
|
|
170
172
|
.map(
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
|
|
3
|
+
const messages = i18n.define({
|
|
4
|
+
baseLocale: "en",
|
|
5
|
+
messages: {
|
|
6
|
+
en: {
|
|
7
|
+
places: "Places",
|
|
8
|
+
museum: "Museum",
|
|
9
|
+
changeStop: "Change stop",
|
|
10
|
+
without: "avoiding",
|
|
11
|
+
route: "Route",
|
|
12
|
+
searched: "Places searched",
|
|
13
|
+
calculated: "Route calculated",
|
|
14
|
+
loading: "Loading places and route …",
|
|
15
|
+
here: "Route here",
|
|
16
|
+
nearby: "More nearby",
|
|
17
|
+
save: "Save as Notebook",
|
|
18
|
+
locate: "I am here",
|
|
19
|
+
unknown: "Opening hours unknown",
|
|
20
|
+
source: "Place and hours",
|
|
21
|
+
description: "Description",
|
|
22
|
+
all: "All",
|
|
23
|
+
less: "Show fewer",
|
|
24
|
+
jump: "Jump to map",
|
|
25
|
+
noPlaces: "No places found.",
|
|
26
|
+
expand: "Search within 10 km",
|
|
27
|
+
retry: "Try again",
|
|
28
|
+
failed: "Places or route could not be loaded. Your question and answer are kept.",
|
|
29
|
+
off: "Places and maps are not configured.",
|
|
30
|
+
configure: "Configure places and maps",
|
|
31
|
+
budget: "Map allowance reached",
|
|
32
|
+
person: "Daily allowance",
|
|
33
|
+
cloud: "Monthly budget",
|
|
34
|
+
reset: "Available again",
|
|
35
|
+
limits: "Change allowances",
|
|
36
|
+
insert: "Insert stop",
|
|
37
|
+
optimize: "Optimize stop order",
|
|
38
|
+
options: "Options",
|
|
39
|
+
along: "Eat along the route",
|
|
40
|
+
nav: "Open navigation app",
|
|
41
|
+
discard: "Discard",
|
|
42
|
+
cancel: "Cancel",
|
|
43
|
+
preview: "Preview for",
|
|
44
|
+
undo: "Undo",
|
|
45
|
+
before: "Before",
|
|
46
|
+
after: "After",
|
|
47
|
+
distance: "Distance",
|
|
48
|
+
duration: "Travel time",
|
|
49
|
+
choose: "Which place did you mean?",
|
|
50
|
+
useStop: "Use this stop",
|
|
51
|
+
stopSearch: "Find a place near the route",
|
|
52
|
+
search: "Search",
|
|
53
|
+
category: "Category",
|
|
54
|
+
placeName: "Place name (optional)",
|
|
55
|
+
placeNamePlaceholder: "e.g. waterfall",
|
|
56
|
+
waterfall: "Attractions",
|
|
57
|
+
cafe: "Cafés",
|
|
58
|
+
restaurant: "Restaurants",
|
|
59
|
+
whole: "Whole tour",
|
|
60
|
+
mode: "Travel mode",
|
|
61
|
+
car: "Car",
|
|
62
|
+
bike: "Bike",
|
|
63
|
+
walk: "Walk",
|
|
64
|
+
avoid: "Avoid",
|
|
65
|
+
highways: "Highways",
|
|
66
|
+
tolls: "Tolls",
|
|
67
|
+
ferries: "Ferries",
|
|
68
|
+
scope: "Apply to",
|
|
69
|
+
calculate: "Calculate preview",
|
|
70
|
+
prefs: "Geoapify avoids where possible; this is not guaranteed.",
|
|
71
|
+
from: "From km",
|
|
72
|
+
to: "To km",
|
|
73
|
+
corridor: "Distance from route",
|
|
74
|
+
noRoute: "No usable route. Change a stop or the options.",
|
|
75
|
+
noLocation: "Location denied · allow it in your browser",
|
|
76
|
+
locationFailed: "Location unavailable. Enter a starting place.",
|
|
77
|
+
start: "Starting place",
|
|
78
|
+
go: "Plan route",
|
|
79
|
+
conflict: "This map has changed. Load its current state and choose your change again.",
|
|
80
|
+
reload: "Load current state",
|
|
81
|
+
pending: "Saving …",
|
|
82
|
+
changed: "Changed",
|
|
83
|
+
stops: "Stops",
|
|
84
|
+
noTraffic: "Times without traffic, calculated",
|
|
85
|
+
resumeRoute: "Continue calculation",
|
|
86
|
+
routeLimit:
|
|
87
|
+
"This route needs more credits than the configured limit. Use fewer stops, select address suggestions, or ask an administrator to raise the budget.",
|
|
88
|
+
budgetNumbers: ({ required, available, missing }: { required: string; available: string; missing: string }) =>
|
|
89
|
+
`Required: ${required} credits. Available: ${available}. Missing: ${missing}.`,
|
|
90
|
+
recordedCosts: ({ settled, unknown }: { settled: string; unknown: string }) =>
|
|
91
|
+
`Last attempt: ${settled} credits charged. Reserved with an unknown outcome: ${unknown}.`,
|
|
92
|
+
noInvoiceCap: "Cloud reserves estimated costs; Geoapify does not guarantee a cap on its side.",
|
|
93
|
+
cost: "The route cost more than estimated. Calculation stopped. Continuing can incur further costs.",
|
|
94
|
+
notebook: "Choose notebook",
|
|
95
|
+
inspectNotebook: "Inspect notebook",
|
|
96
|
+
noNotebooks: "No writable notebook found.",
|
|
97
|
+
saved: "Saved in Notebook",
|
|
98
|
+
previousSave: "This map has an earlier save receipt. Open the notebook to find the note before starting another save.",
|
|
99
|
+
resumeSave: "Resume save",
|
|
100
|
+
reconcileSave: "Check save outcome",
|
|
101
|
+
partialNote: "Open partial note",
|
|
102
|
+
saveProgress: "Map save progress",
|
|
103
|
+
saveLoading: "Preparing save …",
|
|
104
|
+
saveCancelled: "Saving stopped. Any saved part is kept. You can resume here.",
|
|
105
|
+
savePaused: "This save is unfinished. Resume to check the note and continue.",
|
|
106
|
+
saveStorage: "Save progress could not be stored in this browser. Allow local storage and try again before continuing.",
|
|
107
|
+
saveLockUnavailable: "This browser cannot protect the save across tabs. Use a browser with Web Locks support.",
|
|
108
|
+
saveLocked: "This map is being saved in another tab. Wait for it to finish, then check again.",
|
|
109
|
+
saveOwnerChanged: "The signed-in account has changed. Reload the page before saving.",
|
|
110
|
+
saveFailed: "The map could not be saved. Try again or open the notebook to check it.",
|
|
111
|
+
saveSourceChanged: "The map source has changed. Reload the map. Open any partial note before starting another save.",
|
|
112
|
+
saveContentChanged: "The note has changed. Open it to review your changes; this save cannot continue.",
|
|
113
|
+
saveInvalid: "The saved progress or map source cannot be used. Open the notebook to recover any saved part.",
|
|
114
|
+
saveTooLarge: "The map title cannot fit in a save request. Shorten it before saving.",
|
|
115
|
+
saveAppendUnknown: "The last part may still be saving. Check the outcome again later. It will not be sent twice.",
|
|
116
|
+
unknownSave: "The note may have been created. Open the notebook to recover it. This save will not be sent again.",
|
|
117
|
+
},
|
|
118
|
+
de: {
|
|
119
|
+
places: "Orte",
|
|
120
|
+
museum: "Museum",
|
|
121
|
+
changeStop: "Stopp ändern",
|
|
122
|
+
without: "ohne",
|
|
123
|
+
route: "Route",
|
|
124
|
+
searched: "Orte gesucht",
|
|
125
|
+
calculated: "Route berechnet",
|
|
126
|
+
loading: "Orte und Route werden geladen …",
|
|
127
|
+
here: "Route hierher",
|
|
128
|
+
nearby: "Mehr in der Nähe",
|
|
129
|
+
save: "Als Notebook speichern",
|
|
130
|
+
locate: "Ich bin hier",
|
|
131
|
+
unknown: "Öffnungszeiten unbekannt",
|
|
132
|
+
source: "Ort und Zeiten",
|
|
133
|
+
description: "Beschreibung",
|
|
134
|
+
all: "Alle",
|
|
135
|
+
less: "Weniger zeigen",
|
|
136
|
+
jump: "Zur Karte",
|
|
137
|
+
noPlaces: "Keine Orte gefunden.",
|
|
138
|
+
expand: "Umkreis 10 km",
|
|
139
|
+
retry: "Erneut versuchen",
|
|
140
|
+
failed: "Die Orte oder Route konnten nicht geladen werden. Deine Frage und die Antwort bleiben stehen.",
|
|
141
|
+
off: "Orte und Karten sind nicht eingerichtet.",
|
|
142
|
+
configure: "Orte und Karten einrichten",
|
|
143
|
+
budget: "Kartengrenze erreicht",
|
|
144
|
+
person: "Tagesgrenze",
|
|
145
|
+
cloud: "Monatsbudget",
|
|
146
|
+
reset: "Wieder verfügbar",
|
|
147
|
+
limits: "Grenzen ändern",
|
|
148
|
+
insert: "Stopp einfügen",
|
|
149
|
+
optimize: "Reihenfolge optimieren",
|
|
150
|
+
options: "Optionen",
|
|
151
|
+
along: "Unterwegs essen",
|
|
152
|
+
nav: "In Navi-App öffnen",
|
|
153
|
+
discard: "Verwerfen",
|
|
154
|
+
cancel: "Abbrechen",
|
|
155
|
+
preview: "Vorschau für",
|
|
156
|
+
undo: "Rückgängig",
|
|
157
|
+
before: "Bisher",
|
|
158
|
+
after: "Neu",
|
|
159
|
+
distance: "Strecke",
|
|
160
|
+
duration: "Fahrzeit",
|
|
161
|
+
choose: "Welcher Ort ist gemeint?",
|
|
162
|
+
useStop: "Als Stopp übernehmen",
|
|
163
|
+
stopSearch: "Ort nahe der Strecke suchen",
|
|
164
|
+
search: "Suchen",
|
|
165
|
+
category: "Kategorie",
|
|
166
|
+
placeName: "Ortsname (optional)",
|
|
167
|
+
placeNamePlaceholder: "z. B. Wasserfall",
|
|
168
|
+
waterfall: "Sehenswürdigkeiten",
|
|
169
|
+
cafe: "Cafés",
|
|
170
|
+
restaurant: "Restaurants",
|
|
171
|
+
whole: "Ganze Tour",
|
|
172
|
+
mode: "Verkehrsmittel",
|
|
173
|
+
car: "Auto",
|
|
174
|
+
bike: "Rad",
|
|
175
|
+
walk: "Zu Fuß",
|
|
176
|
+
avoid: "Meiden",
|
|
177
|
+
highways: "Autobahn",
|
|
178
|
+
tolls: "Maut",
|
|
179
|
+
ferries: "Fähren",
|
|
180
|
+
scope: "Gilt für",
|
|
181
|
+
calculate: "Vorschau berechnen",
|
|
182
|
+
prefs: "Geoapify meidet so gut es geht; garantiert ist es nicht.",
|
|
183
|
+
from: "Von km",
|
|
184
|
+
to: "Bis km",
|
|
185
|
+
corridor: "Abstand zur Strecke",
|
|
186
|
+
noRoute: "Keine befahrbare Strecke. Ändere einen Stopp oder die Optionen.",
|
|
187
|
+
noLocation: "Standort nicht erlaubt · im Browser freigeben",
|
|
188
|
+
locationFailed: "Standort nicht verfügbar. Gib einen Startort ein.",
|
|
189
|
+
start: "Startort",
|
|
190
|
+
go: "Route planen",
|
|
191
|
+
conflict: "Diese Karte wurde inzwischen geändert. Aktuellen Stand laden und die Änderung erneut wählen.",
|
|
192
|
+
reload: "Aktuellen Stand laden",
|
|
193
|
+
pending: "Wird gespeichert …",
|
|
194
|
+
changed: "Geändert",
|
|
195
|
+
stops: "Stopps",
|
|
196
|
+
noTraffic: "Zeiten ohne Verkehr, gerechnet",
|
|
197
|
+
resumeRoute: "Berechnung fortsetzen",
|
|
198
|
+
routeLimit:
|
|
199
|
+
"Diese Route benötigt mehr Credits als die eingestellte Grenze. Wähle weniger Stopps oder Adressvorschläge, oder lass das Budget durch einen Administrator erhöhen.",
|
|
200
|
+
budgetNumbers: ({ required, available, missing }) => `Benötigt: ${required} Credits. Verfügbar: ${available}. Fehlend: ${missing}.`,
|
|
201
|
+
recordedCosts: ({ settled, unknown }) => `Letzter Versuch: ${settled} Credits gebucht. Mit unklarem Ergebnis reserviert: ${unknown}.`,
|
|
202
|
+
noInvoiceCap: "Cloud reserviert geschätzte Kosten; Geoapify garantiert auf seiner Seite keine Obergrenze.",
|
|
203
|
+
cost: "Die Route war teurer als geschätzt. Die Berechnung wurde gestoppt. Beim Fortsetzen können weitere Kosten entstehen.",
|
|
204
|
+
notebook: "Notebook auswählen",
|
|
205
|
+
inspectNotebook: "Notebook prüfen",
|
|
206
|
+
noNotebooks: "Kein beschreibbares Notebook gefunden.",
|
|
207
|
+
saved: "Im Notebook gespeichert",
|
|
208
|
+
previousSave:
|
|
209
|
+
"Für diese Karte liegt ein früherer Speicherbeleg vor. Öffne das Notebook, um die Notiz zu finden, bevor du erneut speicherst.",
|
|
210
|
+
resumeSave: "Speichern fortsetzen",
|
|
211
|
+
reconcileSave: "Speicherstand prüfen",
|
|
212
|
+
partialNote: "Teilweise gespeicherte Notiz öffnen",
|
|
213
|
+
saveProgress: "Fortschritt beim Speichern der Karte",
|
|
214
|
+
saveLoading: "Speichern wird vorbereitet …",
|
|
215
|
+
saveCancelled: "Speichern gestoppt. Bereits gespeicherte Teile bleiben erhalten. Du kannst hier fortsetzen.",
|
|
216
|
+
savePaused: "Das Speichern ist noch nicht abgeschlossen. Beim Fortsetzen wird die Notiz geprüft.",
|
|
217
|
+
saveStorage:
|
|
218
|
+
"Der Fortschritt konnte in diesem Browser nicht gespeichert werden. Erlaube lokalen Speicher und versuche es erneut, bevor du fortsetzt.",
|
|
219
|
+
saveLockUnavailable:
|
|
220
|
+
"Dieser Browser kann das Speichern nicht über mehrere Tabs absichern. Verwende einen Browser mit Web-Locks-Unterstützung.",
|
|
221
|
+
saveLocked: "Diese Karte wird in einem anderen Tab gespeichert. Warte, bis er fertig ist, und prüfe erneut.",
|
|
222
|
+
saveOwnerChanged: "Das angemeldete Konto hat sich geändert. Lade die Seite vor dem Speichern neu.",
|
|
223
|
+
saveFailed: "Die Karte konnte nicht gespeichert werden. Versuche es erneut oder prüfe das Notebook.",
|
|
224
|
+
saveSourceChanged:
|
|
225
|
+
"Die Kartenquelle hat sich geändert. Lade die Karte neu. Öffne eine teilweise gespeicherte Notiz, bevor du erneut speicherst.",
|
|
226
|
+
saveContentChanged:
|
|
227
|
+
"Die Notiz hat sich geändert. Öffne sie, um deine Änderungen zu prüfen. Dieser Speichervorgang kann nicht fortgesetzt werden.",
|
|
228
|
+
saveInvalid:
|
|
229
|
+
"Der gespeicherte Fortschritt oder die Kartenquelle ist nicht verwendbar. Öffne das Notebook, um bereits gespeicherte Teile zu finden.",
|
|
230
|
+
saveTooLarge: "Der Kartentitel passt nicht in eine Speicheranfrage. Kürze ihn vor dem Speichern.",
|
|
231
|
+
saveAppendUnknown:
|
|
232
|
+
"Der letzte Teil wird möglicherweise noch gespeichert. Prüfe den Stand später erneut. Er wird nicht doppelt gesendet.",
|
|
233
|
+
unknownSave:
|
|
234
|
+
"Die Notiz wurde möglicherweise erstellt. Öffne das Notebook, um sie zu finden. Dieser Speichervorgang wird nicht erneut gesendet.",
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
export const mapMessages = (locale: string) => messages.resolve([locale]).t;
|
|
240
|
+
|
|
241
|
+
/** Helper codes are internal; owning capabilities may supply a final human message. */
|
|
242
|
+
export const mapSaveMessage = (locale: string, code?: string, message?: string): string => {
|
|
243
|
+
const t = mapMessages(locale);
|
|
244
|
+
switch (code) {
|
|
245
|
+
case "source_changed":
|
|
246
|
+
return t.saveSourceChanged;
|
|
247
|
+
case "checkpoint_failed":
|
|
248
|
+
case "persistence_failed":
|
|
249
|
+
return t.saveStorage;
|
|
250
|
+
case "content_changed":
|
|
251
|
+
case "note_moved":
|
|
252
|
+
case "unexpected_receipt":
|
|
253
|
+
return t.saveContentChanged;
|
|
254
|
+
case "invalid_checkpoint":
|
|
255
|
+
case "invalid_title":
|
|
256
|
+
return t.saveInvalid;
|
|
257
|
+
case "request_too_large":
|
|
258
|
+
return t.saveTooLarge;
|
|
259
|
+
case "create_outcome_unknown":
|
|
260
|
+
return t.unknownSave;
|
|
261
|
+
case "append_outcome_unknown":
|
|
262
|
+
return t.saveAppendUnknown;
|
|
263
|
+
case "cancelled":
|
|
264
|
+
return t.saveCancelled;
|
|
265
|
+
case "save_failed":
|
|
266
|
+
return t.saveFailed;
|
|
267
|
+
default:
|
|
268
|
+
return message || t.savePaused;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Focused save fixture: real controls and persistence without starting the map renderer. */
|
|
2
|
+
import { Button, LocaleProvider, useLocale } from "@k2b/ui";
|
|
3
|
+
import { createSignal, Show } from "solid-js";
|
|
4
|
+
import { render } from "solid-js/web";
|
|
5
|
+
import type { AiMap } from "../map-contracts";
|
|
6
|
+
import { mapMessages } from "./map-messages";
|
|
7
|
+
import { MapNotebookSave } from "./map-notebook";
|
|
8
|
+
|
|
9
|
+
const snapshot: { map: AiMap } = JSON.parse(document.getElementById("snapshot")!.textContent!);
|
|
10
|
+
function SaveFixture() {
|
|
11
|
+
const [saving, setSaving] = createSignal(false);
|
|
12
|
+
const locale = useLocale();
|
|
13
|
+
return (
|
|
14
|
+
<section class="ai-map">
|
|
15
|
+
<Show when={saving()} fallback={<Button onClick={() => setSaving(true)}>{mapMessages(locale()).save}</Button>}>
|
|
16
|
+
<MapNotebookSave map={snapshot.map} onCancel={() => setSaving(false)} />
|
|
17
|
+
</Show>
|
|
18
|
+
</section>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
render(
|
|
22
|
+
() => (
|
|
23
|
+
<LocaleProvider locale={document.documentElement.lang}>
|
|
24
|
+
<SaveFixture />
|
|
25
|
+
</LocaleProvider>
|
|
26
|
+
),
|
|
27
|
+
document.getElementById("fixture-mount")!,
|
|
28
|
+
);
|