@cosmicdrift/kumiko-bundled-features 0.165.2 → 0.165.3
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 +7 -7
- package/src/__tests__/access-invalidation-consumer.integration.test.ts +1 -1
- package/src/__tests__/access-invalidation-stream.integration.test.ts +80 -66
- package/src/admin-shell/changes.json +8 -0
- package/src/agent-tools/changes.json +1 -0
- package/src/audit/__tests__/audit.integration.test.ts +5 -2
- package/src/audit/changes.json +8 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +3 -0
- package/src/auth-email-password/changes.json +26 -0
- package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +3 -11
- package/src/auth-email-password/web/__tests__/fetch-mock.ts +14 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +3 -11
- package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +3 -11
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +3 -11
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +3 -11
- package/src/auth-foundation/changes.json +8 -0
- package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +6 -6
- package/src/auth-mfa/changes.json +38 -0
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +3 -11
- package/src/auth-mfa-user-data/changes.json +1 -0
- package/src/billing-foundation/changes.json +1 -0
- package/src/cap-counter/changes.json +1 -0
- package/src/channel-email/changes.json +1 -0
- package/src/channel-in-app/changes.json +1 -0
- package/src/channel-push/changes.json +1 -0
- package/src/compliance-profiles/changes.json +1 -0
- package/src/config/changes.json +9 -0
- package/src/crypto-shredding/changes.json +8 -0
- package/src/crypto-shredding/handlers/forget-subject.write.ts +13 -0
- package/src/custom-fields/changes.json +8 -0
- package/src/data-retention/changes.json +8 -0
- package/src/delivery/changes.json +8 -0
- package/src/document-ingest-foundation/changes.json +1 -0
- package/src/feature-toggles/changes.json +26 -0
- package/src/file-foundation/changes.json +1 -0
- package/src/file-provider-inmemory/changes.json +1 -0
- package/src/file-provider-s3/changes.json +1 -0
- package/src/file-provider-s3-env/changes.json +1 -0
- package/src/files/changes.json +1 -0
- package/src/files-provider-s3/changes.json +1 -0
- package/src/folders/changes.json +1 -0
- package/src/folders-user-data/changes.json +1 -0
- package/src/foundation-shared/changes.json +1 -0
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +3 -1
- package/src/inbound-mail-foundation/changes.json +8 -0
- package/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
- package/src/inbound-provider-imap/changes.json +1 -0
- package/src/inbound-provider-inmemory/changes.json +1 -0
- package/src/jobs/__tests__/jobs-catalog.integration.test.ts +159 -0
- package/src/jobs/__tests__/jobs-screens.boot.test.ts +1 -0
- package/src/jobs/__tests__/jobs-security.integration.test.ts +4 -2
- package/src/jobs/changes.json +8 -0
- package/src/jobs/constants.ts +2 -0
- package/src/jobs/feature.ts +3 -1
- package/src/jobs/handlers/catalog.query.ts +43 -0
- package/src/jobs/handlers/trigger.write.ts +26 -2
- package/src/jobs/i18n.ts +33 -0
- package/src/jobs/web/__tests__/job-runs-screen.test.tsx +223 -0
- package/src/jobs/web/job-runs-screen.tsx +218 -84
- package/src/ledger/changes.json +1 -0
- package/src/legal-pages/changes.json +1 -0
- package/src/mail-foundation/changes.json +1 -0
- package/src/mail-transport-inmemory/changes.json +1 -0
- package/src/mail-transport-smtp/changes.json +1 -0
- package/src/managed-pages/changes.json +20 -0
- package/src/page-render/changes.json +14 -0
- package/src/personal-access-tokens/changes.json +8 -0
- package/src/presets/changes.json +1 -0
- package/src/presets/dsgvo-self-service.ts +1 -1
- package/src/rate-limiting/changes.json +1 -0
- package/src/readiness/changes.json +1 -0
- package/src/renderer-foundation/changes.json +1 -0
- package/src/renderer-simple/changes.json +1 -0
- package/src/secrets/changes.json +1 -0
- package/src/seo/changes.json +20 -0
- package/src/sessions/changes.json +8 -0
- package/src/sessions/handlers/revoke-all-for-user.write.ts +9 -1
- package/src/shared/changes.json +1 -0
- package/src/step-dispatcher/changes.json +1 -0
- package/src/subscription-mollie/changes.json +1 -0
- package/src/subscription-stripe/changes.json +1 -0
- package/src/tags/changes.json +1 -0
- package/src/template-resolver/changes.json +1 -0
- package/src/tenant/changes.json +27 -0
- package/src/tenant-lifecycle/changes.json +1 -0
- package/src/tenant-settings/changes.json +1 -0
- package/src/text-content/changes.json +14 -0
- package/src/tier-engine/changes.json +1 -0
- package/src/user/changes.json +1 -0
- package/src/user/schema/user.ts +8 -5
- package/src/user-data-rights/__tests__/audit-log.integration.test.ts +7 -3
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/download.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/request-export.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +0 -2
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +0 -1
- package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +0 -1
- package/src/user-data-rights/changes.json +21 -0
- package/src/user-data-rights/feature.ts +7 -6
- package/src/user-data-rights/handlers/lift-restriction.write.ts +4 -20
- package/src/user-data-rights/handlers/restrict-account.write.ts +4 -20
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -0
- package/src/user-data-rights/lib/deny-if-target-outside-admin-tenant.ts +32 -0
- package/src/user-data-rights/run-forget-cleanup.ts +23 -0
- package/src/user-data-rights-defaults/changes.json +8 -0
- package/src/user-profile/changes.json +8 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// DOM test for JobRunsScreen trigger panel (#1602).
|
|
3
|
+
// Provider wrapper is local — renderer-web → bundled-features forbids importing
|
|
4
|
+
// renderer-web test-utils from here.
|
|
5
|
+
|
|
6
|
+
// CI runs this file in its own `bun test` process via `bun run test:dom:isolated`
|
|
7
|
+
// (excluded from shared bunfig.dom.toml) — same #457 happy-dom accumulation class
|
|
8
|
+
// as privacy-center / deletion-screens.
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
import { createStore, type Dispatcher, type DispatcherStatus } from "@cosmicdrift/kumiko-headless";
|
|
12
|
+
import {
|
|
13
|
+
createStaticLocaleResolver,
|
|
14
|
+
DispatcherProvider,
|
|
15
|
+
kumikoDefaultTranslations,
|
|
16
|
+
type LiveEventSubscriber,
|
|
17
|
+
LiveEventsProvider,
|
|
18
|
+
LocaleProvider,
|
|
19
|
+
type NavApi,
|
|
20
|
+
NavProvider,
|
|
21
|
+
PrimitivesProvider,
|
|
22
|
+
TokensProvider,
|
|
23
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
24
|
+
import { defaultPrimitives, defaultTokens } from "@cosmicdrift/kumiko-renderer-web";
|
|
25
|
+
import { fireEvent, render, waitFor } from "@testing-library/react";
|
|
26
|
+
import userEvent from "@testing-library/user-event";
|
|
27
|
+
import type { ReactNode } from "react";
|
|
28
|
+
import { JobHandlers, JobQueries } from "../../constants";
|
|
29
|
+
import { defaultTranslations } from "../i18n";
|
|
30
|
+
import { JobRunsScreen } from "../job-runs-screen";
|
|
31
|
+
|
|
32
|
+
if (typeof globalThis.ResizeObserver === "undefined") {
|
|
33
|
+
globalThis.ResizeObserver = class {
|
|
34
|
+
observe(): void {}
|
|
35
|
+
unobserve(): void {}
|
|
36
|
+
disconnect(): void {}
|
|
37
|
+
} as unknown as typeof ResizeObserver;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const stubLiveEvents: LiveEventSubscriber = () => () => {};
|
|
41
|
+
const stubTokens = {
|
|
42
|
+
tokens: defaultTokens,
|
|
43
|
+
mode: "light" as const,
|
|
44
|
+
setMode: () => {},
|
|
45
|
+
toggleMode: () => {},
|
|
46
|
+
};
|
|
47
|
+
const stubResolver = createStaticLocaleResolver();
|
|
48
|
+
const stubNav: NavApi = {
|
|
49
|
+
route: undefined,
|
|
50
|
+
navigate: () => {},
|
|
51
|
+
replace: () => {},
|
|
52
|
+
hrefFor: (target) =>
|
|
53
|
+
target.entityId !== undefined
|
|
54
|
+
? `/${target.screenId}/${target.entityId}`
|
|
55
|
+
: `/${target.screenId}`,
|
|
56
|
+
searchParams: {},
|
|
57
|
+
setSearchParams: () => {},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type CatalogRow = {
|
|
61
|
+
readonly jobName: string;
|
|
62
|
+
readonly perTenant: boolean;
|
|
63
|
+
readonly payloadSchema: Record<string, unknown> | null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type Fixture = {
|
|
67
|
+
readonly catalogRows?: readonly CatalogRow[];
|
|
68
|
+
readonly listRows?: readonly Record<string, unknown>[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function makeDispatcher(
|
|
72
|
+
fixture: Fixture,
|
|
73
|
+
writes: Array<{ type: string; payload: unknown }>,
|
|
74
|
+
queries: Array<{ type: string; payload: unknown }>,
|
|
75
|
+
): Dispatcher {
|
|
76
|
+
const statusStore = createStore<DispatcherStatus>("online");
|
|
77
|
+
const query = (async (type: string, payload: unknown) => {
|
|
78
|
+
queries.push({ type, payload });
|
|
79
|
+
if (type === JobQueries.catalog) {
|
|
80
|
+
return { isSuccess: true, data: { rows: fixture.catalogRows ?? [] } };
|
|
81
|
+
}
|
|
82
|
+
if (type === JobQueries.list) {
|
|
83
|
+
return { isSuccess: true, data: { rows: fixture.listRows ?? [] } };
|
|
84
|
+
}
|
|
85
|
+
return { isSuccess: true, data: null };
|
|
86
|
+
}) as unknown as Dispatcher["query"];
|
|
87
|
+
const write = (async (type: string, payload: unknown) => {
|
|
88
|
+
writes.push({ type, payload });
|
|
89
|
+
return {
|
|
90
|
+
isSuccess: true,
|
|
91
|
+
data: { jobName: (payload as { jobName: string }).jobName, bullJobId: "bull-1" },
|
|
92
|
+
};
|
|
93
|
+
}) as unknown as Dispatcher["write"];
|
|
94
|
+
return {
|
|
95
|
+
write,
|
|
96
|
+
query,
|
|
97
|
+
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
98
|
+
statusStore,
|
|
99
|
+
async *stream() {},
|
|
100
|
+
pendingWrites: () => [],
|
|
101
|
+
pendingFiles: () => [],
|
|
102
|
+
} as unknown as Dispatcher; // @cast-boundary test-stub
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderScreen(fixture: Fixture = {}): {
|
|
106
|
+
view: ReturnType<typeof render>;
|
|
107
|
+
writes: Array<{ type: string; payload: unknown }>;
|
|
108
|
+
queries: Array<{ type: string; payload: unknown }>;
|
|
109
|
+
} {
|
|
110
|
+
const writes: Array<{ type: string; payload: unknown }> = [];
|
|
111
|
+
const queries: Array<{ type: string; payload: unknown }> = [];
|
|
112
|
+
const wrapper = ({ children }: { readonly children: ReactNode }): ReactNode => (
|
|
113
|
+
<TokensProvider value={stubTokens}>
|
|
114
|
+
<LocaleProvider
|
|
115
|
+
resolver={stubResolver}
|
|
116
|
+
fallbackBundles={[defaultTranslations, kumikoDefaultTranslations]}
|
|
117
|
+
>
|
|
118
|
+
<PrimitivesProvider value={defaultPrimitives}>
|
|
119
|
+
<NavProvider value={stubNav}>
|
|
120
|
+
<LiveEventsProvider value={stubLiveEvents}>
|
|
121
|
+
<DispatcherProvider dispatcher={makeDispatcher(fixture, writes, queries)}>
|
|
122
|
+
{children}
|
|
123
|
+
</DispatcherProvider>
|
|
124
|
+
</LiveEventsProvider>
|
|
125
|
+
</NavProvider>
|
|
126
|
+
</PrimitivesProvider>
|
|
127
|
+
</LocaleProvider>
|
|
128
|
+
</TokensProvider>
|
|
129
|
+
);
|
|
130
|
+
return { view: render(<JobRunsScreen />, { wrapper }), writes, queries };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const REINDEX: CatalogRow = {
|
|
134
|
+
jobName: "jobs:job:reindex-entity",
|
|
135
|
+
perTenant: true,
|
|
136
|
+
payloadSchema: {
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: { entity: { type: "string" } },
|
|
139
|
+
required: ["entity"],
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
describe("JobRunsScreen — manual trigger panel", () => {
|
|
144
|
+
test("loads catalog + list via QNs and renders translated trigger UI", async () => {
|
|
145
|
+
const { view, queries } = renderScreen({ catalogRows: [REINDEX] });
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
if (view.queryByTestId("job-runs-screen") === null) throw new Error("not mounted");
|
|
148
|
+
});
|
|
149
|
+
expect(view.getByTestId("job-trigger-panel")).toBeTruthy();
|
|
150
|
+
expect(view.getByTestId("job-trigger-form")).toBeTruthy();
|
|
151
|
+
expect(view.container.textContent).not.toContain("jobs.trigger.");
|
|
152
|
+
expect(view.container.textContent).not.toContain("jobs.runs.");
|
|
153
|
+
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
const catalogHit = queries.some((q) => q.type === JobQueries.catalog);
|
|
156
|
+
const listHit = queries.some((q) => q.type === JobQueries.list);
|
|
157
|
+
if (!catalogHit || !listHit) throw new Error("expected catalog + list queries");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("empty catalog shows empty copy, no form", async () => {
|
|
162
|
+
const { view } = renderScreen({ catalogRows: [] });
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
if (view.queryByTestId("job-trigger-empty") === null) throw new Error("empty missing");
|
|
165
|
+
});
|
|
166
|
+
expect(view.queryByTestId("job-trigger-form")).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("select job + JSON payload → jobs:write:trigger", async () => {
|
|
170
|
+
const user = userEvent.setup();
|
|
171
|
+
const { view, writes } = renderScreen({ catalogRows: [REINDEX] });
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
if (view.queryByTestId("job-trigger-form") === null) throw new Error("form missing");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await user.click(view.getByTestId("combobox-job-trigger-name"));
|
|
177
|
+
const option = await view.findByText("jobs:job:reindex-entity");
|
|
178
|
+
await user.click(option);
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
if (view.queryByTestId("job-trigger-per-tenant") === null) {
|
|
182
|
+
throw new Error("perTenant hint missing");
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
expect(view.getByTestId("job-trigger-schema-hint").textContent).toContain("entity");
|
|
186
|
+
|
|
187
|
+
const payload = view.container.querySelector<HTMLTextAreaElement>("#job-trigger-payload");
|
|
188
|
+
if (!payload) throw new Error("payload textarea missing");
|
|
189
|
+
fireEvent.change(payload, { target: { value: '{"entity":"credit"}' } });
|
|
190
|
+
|
|
191
|
+
await user.click(view.getByTestId("job-trigger-submit"));
|
|
192
|
+
|
|
193
|
+
await waitFor(() => {
|
|
194
|
+
if (writes.length === 0) throw new Error("no write yet");
|
|
195
|
+
});
|
|
196
|
+
expect(writes[0]).toEqual({
|
|
197
|
+
type: JobHandlers.trigger,
|
|
198
|
+
payload: { jobName: "jobs:job:reindex-entity", payload: { entity: "credit" } },
|
|
199
|
+
});
|
|
200
|
+
expect(view.getByTestId("job-trigger-success")).toBeTruthy();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("invalid JSON payload → client error, no write", async () => {
|
|
204
|
+
const user = userEvent.setup();
|
|
205
|
+
const { view, writes } = renderScreen({ catalogRows: [REINDEX] });
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
if (view.queryByTestId("job-trigger-form") === null) throw new Error("form missing");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await user.click(view.getByTestId("combobox-job-trigger-name"));
|
|
211
|
+
await user.click(await view.findByText("jobs:job:reindex-entity"));
|
|
212
|
+
|
|
213
|
+
const payload = view.container.querySelector<HTMLTextAreaElement>("#job-trigger-payload");
|
|
214
|
+
if (!payload) throw new Error("payload textarea missing");
|
|
215
|
+
fireEvent.change(payload, { target: { value: "{not-json" } });
|
|
216
|
+
await user.click(view.getByTestId("job-trigger-submit"));
|
|
217
|
+
|
|
218
|
+
await waitFor(() => {
|
|
219
|
+
if (view.queryByTestId("job-trigger-error") === null) throw new Error("error missing");
|
|
220
|
+
});
|
|
221
|
+
expect(writes).toEqual([]);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// @runtime client
|
|
2
|
-
// SystemAdmin job-run list
|
|
2
|
+
// SystemAdmin job-run list + manual trigger panel.
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
type DataTableSort,
|
|
6
6
|
formatWhen,
|
|
7
7
|
sortByAccessor,
|
|
8
|
-
|
|
8
|
+
useMutation,
|
|
9
9
|
useNav,
|
|
10
10
|
usePrimitives,
|
|
11
|
+
useQuery,
|
|
11
12
|
useTranslation,
|
|
12
13
|
} from "@cosmicdrift/kumiko-renderer";
|
|
13
|
-
import { type ReactNode,
|
|
14
|
-
import { JOB_RUN_DETAIL_SCREEN_ID, JobQueries } from "../constants";
|
|
14
|
+
import { type ReactNode, useMemo, useState } from "react";
|
|
15
|
+
import { JOB_RUN_DETAIL_SCREEN_ID, JobHandlers, JobQueries } from "../constants";
|
|
15
16
|
|
|
16
17
|
type JobRunRow = {
|
|
17
18
|
readonly id: string;
|
|
@@ -25,10 +26,15 @@ type JobRunRow = {
|
|
|
25
26
|
|
|
26
27
|
type ListResponse = { readonly rows: readonly JobRunRow[] };
|
|
27
28
|
|
|
28
|
-
type
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
type CatalogEntry = {
|
|
30
|
+
readonly jobName: string;
|
|
31
|
+
readonly perTenant: boolean;
|
|
32
|
+
readonly payloadSchema: Record<string, unknown> | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type CatalogResponse = { readonly rows: readonly CatalogEntry[] };
|
|
36
|
+
|
|
37
|
+
type TriggerResult = { readonly jobName: string; readonly bullJobId: string };
|
|
32
38
|
|
|
33
39
|
const STATUS_FILTER_OPTIONS = [
|
|
34
40
|
{ value: "", labelKey: "jobs.runs.filter.all" },
|
|
@@ -40,100 +46,228 @@ const STATUS_FILTER_OPTIONS = [
|
|
|
40
46
|
|
|
41
47
|
export function JobRunsScreen(): ReactNode {
|
|
42
48
|
const t = useTranslation();
|
|
43
|
-
const { Banner, DataTable, Field, Input, Text } = usePrimitives();
|
|
44
|
-
const dispatcher = useDispatcher();
|
|
49
|
+
const { Banner, Button, DataTable, Field, Form, Heading, Input, Text } = usePrimitives();
|
|
45
50
|
const nav = useNav();
|
|
46
|
-
const [state, setState] = useState<State>({ kind: "loading" });
|
|
47
51
|
const [statusFilter, setStatusFilter] = useState("");
|
|
48
52
|
const [sort, setSort] = useState<DataTableSort | null>(null);
|
|
53
|
+
const [jobName, setJobName] = useState("");
|
|
54
|
+
const [payloadText, setPayloadText] = useState("{}");
|
|
55
|
+
const [clientError, setClientError] = useState<string | null>(null);
|
|
56
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
57
|
+
|
|
58
|
+
const listPayload = useMemo(
|
|
59
|
+
() => ({
|
|
60
|
+
limit: 50,
|
|
61
|
+
...(statusFilter !== "" && {
|
|
62
|
+
status: statusFilter as "queued" | "running" | "completed" | "failed",
|
|
63
|
+
}),
|
|
64
|
+
}),
|
|
65
|
+
[statusFilter],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const listQuery = useQuery<ListResponse | null>(JobQueries.list, listPayload);
|
|
69
|
+
const catalogQuery = useQuery<CatalogResponse | null>(JobQueries.catalog, {});
|
|
70
|
+
const triggerMutation = useMutation<TriggerResult>(JobHandlers.trigger);
|
|
49
71
|
|
|
50
72
|
const filterOptions = STATUS_FILTER_OPTIONS.map((opt) => ({
|
|
51
73
|
value: opt.value,
|
|
52
74
|
label: t(opt.labelKey),
|
|
53
75
|
}));
|
|
54
76
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
const catalogRows = catalogQuery.data?.rows ?? [];
|
|
78
|
+
const selected = catalogRows.find((row) => row.jobName === jobName) ?? null;
|
|
79
|
+
const jobOptions = catalogRows.map((row) => ({
|
|
80
|
+
value: row.jobName,
|
|
81
|
+
label: row.jobName,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const schemaHint =
|
|
85
|
+
selected?.payloadSchema !== undefined && selected.payloadSchema !== null
|
|
86
|
+
? JSON.stringify(selected.payloadSchema, null, 2)
|
|
87
|
+
: null;
|
|
88
|
+
|
|
89
|
+
const onTrigger = async (): Promise<void> => {
|
|
90
|
+
setClientError(null);
|
|
91
|
+
setSuccessMessage(null);
|
|
92
|
+
if (jobName === "") return;
|
|
93
|
+
|
|
94
|
+
let payload: Record<string, unknown> = {};
|
|
95
|
+
const trimmed = payloadText.trim();
|
|
96
|
+
if (trimmed !== "" && trimmed !== "{}") {
|
|
97
|
+
try {
|
|
98
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
99
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
100
|
+
setClientError(t("jobs.trigger.payload.invalidJson"));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
payload = parsed as Record<string, unknown>;
|
|
104
|
+
} catch {
|
|
105
|
+
setClientError(t("jobs.trigger.payload.invalidJson"));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const res = await triggerMutation.mutate({
|
|
111
|
+
jobName,
|
|
112
|
+
...(Object.keys(payload).length > 0 ? { payload } : {}),
|
|
62
113
|
});
|
|
63
114
|
if (!res.isSuccess) {
|
|
64
|
-
|
|
115
|
+
setClientError(res.error.message);
|
|
65
116
|
return;
|
|
66
117
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
void refresh();
|
|
72
|
-
}, [refresh]);
|
|
73
|
-
|
|
74
|
-
if (state.kind === "loading") {
|
|
75
|
-
return (
|
|
76
|
-
<div className="p-6" data-testid="job-runs-screen">
|
|
77
|
-
<Text variant="small">{t("jobs.runs.loading")}</Text>
|
|
78
|
-
</div>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (state.kind === "error") {
|
|
83
|
-
return (
|
|
84
|
-
<div className="p-6" data-testid="job-runs-screen">
|
|
85
|
-
<Banner variant="error">{state.message}</Banner>
|
|
86
|
-
</div>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
118
|
+
setSuccessMessage(t("jobs.trigger.success"));
|
|
119
|
+
void listQuery.refetch();
|
|
120
|
+
};
|
|
89
121
|
|
|
90
122
|
const openDetail = (id: string): void =>
|
|
91
123
|
nav.navigate({ screenId: JOB_RUN_DETAIL_SCREEN_ID, entityId: id });
|
|
92
124
|
|
|
125
|
+
const rows = listQuery.data?.rows ?? [];
|
|
126
|
+
const listLoading = listQuery.loading === true && listQuery.data === null;
|
|
127
|
+
const submitting = triggerMutation.pending;
|
|
128
|
+
|
|
93
129
|
return (
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
<div className="flex flex-col gap-6 p-6" data-testid="job-runs-screen">
|
|
131
|
+
<section className="flex flex-col gap-3" data-testid="job-trigger-panel">
|
|
132
|
+
<Heading variant="section">{t("jobs.trigger.title")}</Heading>
|
|
133
|
+
<Text variant="small">{t("jobs.trigger.explainer")}</Text>
|
|
134
|
+
|
|
135
|
+
{catalogQuery.error !== null && (
|
|
136
|
+
<Banner variant="error" testId="job-trigger-catalog-error">
|
|
137
|
+
{catalogQuery.error.message}
|
|
138
|
+
</Banner>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{catalogQuery.error === null &&
|
|
142
|
+
catalogRows.length === 0 &&
|
|
143
|
+
catalogQuery.loading !== true ? (
|
|
144
|
+
<Text variant="small" testId="job-trigger-empty">
|
|
145
|
+
{t("jobs.trigger.empty")}
|
|
146
|
+
</Text>
|
|
147
|
+
) : catalogQuery.error !== null ? null : (
|
|
148
|
+
<Form
|
|
149
|
+
testId="job-trigger-form"
|
|
150
|
+
onSubmit={(e) => {
|
|
151
|
+
e?.preventDefault();
|
|
152
|
+
void onTrigger();
|
|
153
|
+
}}
|
|
154
|
+
actions={
|
|
155
|
+
<Button
|
|
156
|
+
type="submit"
|
|
157
|
+
disabled={jobName === "" || submitting}
|
|
158
|
+
loading={submitting}
|
|
159
|
+
testId="job-trigger-submit"
|
|
160
|
+
>
|
|
161
|
+
{submitting ? t("jobs.trigger.submitting") : t("jobs.trigger.submit")}
|
|
162
|
+
</Button>
|
|
163
|
+
}
|
|
164
|
+
>
|
|
165
|
+
<Field id="job-trigger-name" label={t("jobs.trigger.job")} required>
|
|
166
|
+
<Input
|
|
167
|
+
kind="select"
|
|
168
|
+
id="job-trigger-name"
|
|
169
|
+
name="job-trigger-name"
|
|
170
|
+
value={jobName}
|
|
171
|
+
onChange={setJobName}
|
|
172
|
+
options={jobOptions}
|
|
173
|
+
/>
|
|
174
|
+
</Field>
|
|
175
|
+
|
|
176
|
+
{selected?.perTenant === true && (
|
|
177
|
+
<Text variant="small" testId="job-trigger-per-tenant">
|
|
178
|
+
{t("jobs.trigger.perTenant")}
|
|
179
|
+
</Text>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{schemaHint !== null && (
|
|
183
|
+
<Text variant="code" testId="job-trigger-schema-hint">
|
|
184
|
+
{t("jobs.trigger.payload.hint")}: {schemaHint}
|
|
185
|
+
</Text>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
<Field id="job-trigger-payload" label={t("jobs.trigger.payload")}>
|
|
189
|
+
<Input
|
|
190
|
+
kind="textarea"
|
|
191
|
+
id="job-trigger-payload"
|
|
192
|
+
name="job-trigger-payload"
|
|
193
|
+
value={payloadText}
|
|
194
|
+
onChange={setPayloadText}
|
|
195
|
+
/>
|
|
196
|
+
</Field>
|
|
197
|
+
|
|
198
|
+
{clientError !== null && (
|
|
199
|
+
<Banner variant="error" testId="job-trigger-error">
|
|
200
|
+
{clientError}
|
|
201
|
+
</Banner>
|
|
202
|
+
)}
|
|
203
|
+
{successMessage !== null && (
|
|
204
|
+
<Banner variant="info" testId="job-trigger-success">
|
|
205
|
+
{successMessage}
|
|
206
|
+
</Banner>
|
|
207
|
+
)}
|
|
208
|
+
</Form>
|
|
209
|
+
)}
|
|
210
|
+
</section>
|
|
211
|
+
|
|
212
|
+
{listQuery.error !== null && (
|
|
213
|
+
<Banner variant="error" testId="job-runs-list-error">
|
|
214
|
+
{listQuery.error.message}
|
|
215
|
+
</Banner>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{listLoading ? (
|
|
219
|
+
<Text variant="small">{t("jobs.runs.loading")}</Text>
|
|
220
|
+
) : listQuery.error !== null ? null : (
|
|
221
|
+
<DataTable
|
|
222
|
+
testId="job-runs-table"
|
|
223
|
+
columns={[
|
|
224
|
+
{ field: "job", label: t("jobs.runs.col.job"), type: "string", sortable: true },
|
|
225
|
+
{ field: "status", label: t("jobs.runs.col.status"), type: "string", sortable: true },
|
|
226
|
+
{ field: "started", label: t("jobs.runs.col.started"), type: "string", sortable: true },
|
|
227
|
+
{
|
|
228
|
+
field: "duration",
|
|
229
|
+
label: t("jobs.runs.col.duration"),
|
|
230
|
+
type: "string",
|
|
231
|
+
sortable: false,
|
|
232
|
+
},
|
|
233
|
+
]}
|
|
234
|
+
sort={sort}
|
|
235
|
+
onSortChange={setSort}
|
|
236
|
+
rows={sortByAccessor(rows, sort, SORT_ACCESSORS).map((row) => ({
|
|
237
|
+
id: row.id,
|
|
238
|
+
values: {
|
|
239
|
+
job: row.jobName,
|
|
240
|
+
status: row.status,
|
|
241
|
+
started: formatWhen(row.startedAt),
|
|
242
|
+
duration: row.duration ?? "—",
|
|
243
|
+
},
|
|
244
|
+
}))}
|
|
245
|
+
onRowClick={(row) => openDetail(row.id)}
|
|
246
|
+
toolbarStart={
|
|
247
|
+
<Field id="job-runs-status-filter" label={t("jobs.runs.filter.status")}>
|
|
248
|
+
<Input
|
|
249
|
+
kind="select"
|
|
250
|
+
id="job-runs-status-filter"
|
|
251
|
+
name="job-runs-status-filter"
|
|
252
|
+
value={statusFilter}
|
|
253
|
+
onChange={setStatusFilter}
|
|
254
|
+
options={filterOptions}
|
|
255
|
+
/>
|
|
256
|
+
</Field>
|
|
257
|
+
}
|
|
258
|
+
rowActions={[
|
|
259
|
+
{
|
|
260
|
+
id: "open",
|
|
261
|
+
label: t("jobs.runs.open"),
|
|
262
|
+
style: "secondary",
|
|
263
|
+
onTrigger: (row) => openDetail(row.id),
|
|
264
|
+
},
|
|
265
|
+
]}
|
|
266
|
+
rowActionMode="inline"
|
|
267
|
+
emptyState={<Text variant="small">{t("jobs.runs.empty")}</Text>}
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
137
271
|
);
|
|
138
272
|
}
|
|
139
273
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.147.3",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "`managed-pages`' `GET {basePath}/:slug` render route and `seo`'s `sitemap.xml`/`llms.txt`…",
|
|
6
|
+
"detail": "`managed-pages`' `GET {basePath}/:slug` render route and `seo`'s `sitemap.xml`/`llms.txt` routes now use the `systemQuery` in-process dispatch (introduced for `legal-pages` in the `resolverTrust` fix) instead of an internal `app.fetch` self-request with a forged `X-Tenant` header. That self-fetch carried no `Host` header (Host is only implied by the request URL, never stored in `Headers`), so a host-based `anonymousAccess.tenantResolver` reading only the header resolved `null` for the inner request. Under `resolverTrust: \"authoritative\"` there is no client-tenant fallback for a `null` resolution, so the route failed with `tenant_required` (503 \"page unavailable\" / empty sitemap entries) even for a legitimate, correctly-routed visitor. `systemQuery` forces the already host-resolved tenant in-process — no header round-trip, nothing that can go missing. Consumers running `managed-pages` or `seo` with `resolverTrust: \"authoritative\"` should upgrade — the render/discovery routes were not reliably servable under that mode before this fix. - @cosmicdrift/kumiko-framework@0.147.3 - @cosmicdrift/kumiko-dispatcher-live@0.147.3 - @cosmicdrift/kumiko-headless@0.147.3 - @cosmicdrift/kumiko-renderer@0.147.3 - @cosmicdrift/kumiko-renderer-web@0.147.3"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.123.1",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "managed-pages: add a `./managed-pages/web` client export (`managedPagesClient()`) so apps…",
|
|
12
|
+
"detail": "managed-pages: add a `./managed-pages/web` client export (`managedPagesClient()`) so apps can register the feature's admin-screen translations into the browser i18n store. Previously the server-side `r.translations` bundle never reached the client, so configEdit/entityEdit labels (branding, page CMS) rendered as raw i18n keys in the admin UI. The client bundle is pivoted from the same `MANAGED_PAGES_I18N` source (no key duplication). renderer-web: extract a shared `FormScreenShell` primitive — the canonical centered `max-w-3xl` form/settings column that `DefaultForm` (configEdit/entityEdit) already used. Exporting it lets custom settings screens share the exact same width + centering instead of each author re-inventing the wrapper. `user-data-rights`' privacy-center screen adopts it. - @cosmicdrift/kumiko-renderer-web@0.123.1 - @cosmicdrift/kumiko-framework@0.123.1 - @cosmicdrift/kumiko-dispatcher-live@0.123.1 - @cosmicdrift/kumiko-headless@0.123.1 - @cosmicdrift/kumiko-renderer@0.123.1"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.122.5",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "managed-pages: ship de/en translations for its admin screens (branding settings + page CM…",
|
|
18
|
+
"detail": "managed-pages: ship de/en translations for its admin screens (branding settings + page CMS) via `r.translations`, so field labels, section headers and screen titles no longer render as raw i18n keys. Any app mounting `managed-pages` now boots with a complete, translated admin surface. Also adds `tag` and `key` to the nav-icon allowlist (`NAV_ICONS`) so nav entries using those keys render a Lucide icon instead of the grey dot fallback. - @cosmicdrift/kumiko-renderer-web@0.122.5 - @cosmicdrift/kumiko-framework@0.122.5 - @cosmicdrift/kumiko-dispatcher-live@0.122.5 - @cosmicdrift/kumiko-headless@0.122.5 - @cosmicdrift/kumiko-renderer@0.122.5"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.123.3",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "page-render: neuer `./page-render/web`-Subpath für client-safe Exports (renderSafeMarkdow…",
|
|
6
|
+
"detail": "page-render: neuer `./page-render/web`-Subpath für client-safe Exports (renderSafeMarkdown, sanitizeTenantCss, wrapInLayout, branding-Helpers, securePageHeaders). Der bestehende `./page-render`-Barrel re-exportierte auch `cachedSecurePageResponse`, das transitiv `@cosmicdrift/kumiko-framework/api` (postgres/ioredis) zieht — jeder Import aus dem Barrel in Client-Code ließ den Browser-Bundle mit \"Bundle failed\" crashen, ohne brauchbare Fehlermeldung. - @cosmicdrift/kumiko-framework@0.123.3 - @cosmicdrift/kumiko-dispatcher-live@0.123.3 - @cosmicdrift/kumiko-headless@0.123.3 - @cosmicdrift/kumiko-renderer@0.123.3 - @cosmicdrift/kumiko-renderer-web@0.123.3"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.123.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "headless: new `html` tagged template + `raw()`/`RawHtml` — auto-escapes every interpolati…",
|
|
12
|
+
"detail": "headless: new `html` tagged template + `raw()`/`RawHtml` — auto-escapes every interpolation, `raw()` marks prerendered markup, nested `html` fragments compose without double-escaping. Structural companion to the new HTML-escape guard (infra#201). Hardening from the guard's first run: apex JSON-LD `<script>` block serializes `<` as `<` (no `</script>` breakout), dev-server `injectSchema` does the same for `window.__KUMIKO_SCHEMA__`; apex/page-render prerendered fragments renamed to the `*Html` convention. - @cosmicdrift/kumiko-headless@0.123.0 - @cosmicdrift/kumiko-dispatcher-live@0.123.0 - @cosmicdrift/kumiko-renderer@0.123.0 - @cosmicdrift/kumiko-renderer-web@0.123.0 - @cosmicdrift/kumiko-framework@0.123.0"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.117.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "personal-access-tokens: add `toggleable` option so the whole feature can be tier-gated vi…",
|
|
6
|
+
"detail": "personal-access-tokens: add `toggleable` option so the whole feature can be tier-gated via the tier-engine (mirrors ledger/tags). Pass `{ toggleable: { default: false } }` for fail-closed gating — PAT is then off until a tier lists `\"personal-access-tokens\"` in its features. Omitting the option keeps PAT always-on (no behaviour change for existing consumers)."
|
|
7
|
+
}
|
|
8
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -13,7 +13,7 @@ export type DsgvoSelfServiceOptions = {
|
|
|
13
13
|
|
|
14
14
|
// DSGVO- + Account-Self-Service-Kette, die jede Kumiko-SaaS-App mountet
|
|
15
15
|
// (Privacy-Center, Account-Löschung Art. 17, Export Art. 20, Sessions).
|
|
16
|
-
//
|
|
16
|
+
// Order is load-bearing (Require-Order): user-data-rights braucht
|
|
17
17
|
// data-retention + compliance-profiles + sessions, user-profile braucht
|
|
18
18
|
// user-data-rights. Genau diese Order stand bisher in jeder App handkopiert
|
|
19
19
|
// mit Erklär-Kommentar. text-content + legal-pages bleiben bewusst draußen —
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|