@cosmicdrift/kumiko-bundled-features 0.130.1 → 0.130.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.130.1",
3
+ "version": "0.130.2",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -107,11 +107,11 @@
107
107
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
108
108
  },
109
109
  "dependencies": {
110
- "@cosmicdrift/kumiko-dispatcher-live": "0.130.1",
111
- "@cosmicdrift/kumiko-framework": "0.130.1",
112
- "@cosmicdrift/kumiko-headless": "0.130.1",
113
- "@cosmicdrift/kumiko-renderer": "0.130.1",
114
- "@cosmicdrift/kumiko-renderer-web": "0.130.1",
110
+ "@cosmicdrift/kumiko-dispatcher-live": "0.130.2",
111
+ "@cosmicdrift/kumiko-framework": "0.130.2",
112
+ "@cosmicdrift/kumiko-headless": "0.130.2",
113
+ "@cosmicdrift/kumiko-renderer": "0.130.2",
114
+ "@cosmicdrift/kumiko-renderer-web": "0.130.2",
115
115
  "@mollie/api-client": "^4.5.0",
116
116
  "@node-rs/argon2": "^2.0.2",
117
117
  "@types/nodemailer": "^8.0.0",
@@ -5,6 +5,7 @@ import { createConfigFeature } from "../../config/feature";
5
5
  import { createJobsFeature } from "../../jobs/feature";
6
6
  import { createTenantFeature } from "../../tenant/feature";
7
7
  import { tierEngineFeature } from "../../tier-engine/feature";
8
+ import { createUserFeature } from "../../user/feature";
8
9
  import {
9
10
  ADMIN_SHELL_FEATURE,
10
11
  DEFAULT_PLATFORM_WORKSPACE_ID,
@@ -14,6 +15,7 @@ import { createAdminShellFeature } from "../feature";
14
15
 
15
16
  const features = [
16
17
  createConfigFeature(),
18
+ createUserFeature(),
17
19
  createTenantFeature(),
18
20
  createAuditFeature(),
19
21
  createJobsFeature(),
@@ -22,10 +24,22 @@ const features = [
22
24
  ];
23
25
 
24
26
  describe("admin-shell boot + workspace composition", () => {
25
- test("validateBoot with tenant, audit, jobs, tier-engine", () => {
27
+ test("validateBoot with user, tenant, audit, jobs, tier-engine", () => {
26
28
  expect(() => validateBoot(features)).not.toThrow();
27
29
  });
28
30
 
31
+ test("validateBoot fails without user (platform-overview user-count requires it)", () => {
32
+ const withoutUser = [
33
+ createConfigFeature(),
34
+ createTenantFeature(),
35
+ createAuditFeature(),
36
+ createJobsFeature(),
37
+ tierEngineFeature,
38
+ createAdminShellFeature(),
39
+ ];
40
+ expect(() => validateBoot(withoutUser)).toThrow();
41
+ });
42
+
29
43
  test("registers tenant + platform workspaces with qualified ids", () => {
30
44
  const registry = createRegistry(features);
31
45
  expect(
@@ -82,6 +96,7 @@ describe("admin-shell boot + workspace composition", () => {
82
96
  });
83
97
  const registry = createRegistry([
84
98
  createConfigFeature(),
99
+ createUserFeature(),
85
100
  createTenantFeature(),
86
101
  createAuditFeature(),
87
102
  createJobsFeature(),
@@ -103,6 +118,7 @@ describe("admin-shell boot + workspace composition", () => {
103
118
  });
104
119
  const registry = createRegistry([
105
120
  createConfigFeature(),
121
+ createUserFeature(),
106
122
  createTenantFeature(),
107
123
  createAuditFeature(),
108
124
  createJobsFeature(),
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { JobQueries } from "../../jobs/constants";
3
3
  import { TenantQueries } from "../../tenant/constants";
4
+ import { UserQueries } from "../../user/constants";
4
5
  import {
5
6
  isOverviewQueryAllowed,
6
7
  PLATFORM_OVERVIEW_ALLOWED_QUERIES,
@@ -22,8 +23,16 @@ describe("overview query allowlist", () => {
22
23
  expect(TENANT_OVERVIEW_ALLOWED_QUERIES).toContain("config:query:readiness");
23
24
  });
24
25
 
25
- test("platform allowlist is tenant:list + jobs:list only", () => {
26
- expect(PLATFORM_OVERVIEW_ALLOWED_QUERIES).toEqual([TenantQueries.list, JobQueries.list]);
26
+ test("platform allowlist is tenant:list + jobs:list + user:list", () => {
27
+ expect(PLATFORM_OVERVIEW_ALLOWED_QUERIES).toEqual([
28
+ TenantQueries.list,
29
+ JobQueries.list,
30
+ UserQueries.list,
31
+ ]);
32
+ });
33
+
34
+ test("platform overview allows the user-count query (fw#891 regression)", () => {
35
+ expect(isOverviewQueryAllowed("platform", UserQueries.list)).toBe(true);
27
36
  });
28
37
 
29
38
  test("platform queries are not tenant-allowlisted", () => {
@@ -5,11 +5,13 @@ import { createConfigFeature } from "../../config/feature";
5
5
  import { createJobsFeature } from "../../jobs/feature";
6
6
  import { createTenantFeature } from "../../tenant/feature";
7
7
  import { tierEngineFeature } from "../../tier-engine/feature";
8
+ import { createUserFeature } from "../../user/feature";
8
9
  import { PLATFORM_OVERVIEW_SCREEN_ID, TENANT_OVERVIEW_SCREEN_ID } from "../constants";
9
10
  import { createAdminShellFeature } from "../feature";
10
11
 
11
12
  const features = [
12
13
  createConfigFeature(),
14
+ createUserFeature(),
13
15
  createTenantFeature(),
14
16
  createAuditFeature(),
15
17
  createJobsFeature(),
@@ -36,13 +36,14 @@ export function createAdminShellFeature(options: CreateAdminShellOptions = {}):
36
36
 
37
37
  return defineFeature(ADMIN_SHELL_FEATURE, (r) => {
38
38
  r.describe(
39
- "Registers tenant-admin and platform-admin workspaces with provider nav into owner-feature screens (`tenant:screen:members`, `audit:screen:audit-log`, `tenant:screen:tenant-list`, `jobs:screen:job-runs`, optional `tier-engine:screen:tier-admin`). Mount after tenant, audit, and jobs; pass `workspaceIds` to match app URL conventions (e.g. Studio `d`/`s`, PublicStatus `admin`/`sysadmin`). Client: `adminShellClient()`, `tenantClient()`, `auditClient()`, `jobsClient()`, optional `tierEngineClient()`.",
39
+ "Registers tenant-admin and platform-admin workspaces with provider nav into owner-feature screens (`tenant:screen:members`, `audit:screen:audit-log`, `tenant:screen:tenant-list`, `jobs:screen:job-runs`, optional `tier-engine:screen:tier-admin`). Mount after user, tenant, audit, and jobs; pass `workspaceIds` to match app URL conventions (e.g. Studio `d`/`s`, PublicStatus `admin`/`sysadmin`). Client: `adminShellClient()`, `tenantClient()`, `auditClient()`, `jobsClient()`, optional `tierEngineClient()`.",
40
40
  );
41
41
  r.uiHints({
42
42
  displayLabel: "Admin Shell",
43
43
  category: "operations",
44
44
  recommended: false,
45
45
  });
46
+ r.requires("user");
46
47
  r.requires("tenant");
47
48
  r.requires("audit");
48
49
  r.requires("jobs");
@@ -30,6 +30,7 @@ export const ADMIN_SHELL_I18N: Readonly<Record<string, LocalizedString>> = {
30
30
  en: "Check required settings",
31
31
  },
32
32
  "admin-shell:overview.tenants": { de: "Mandanten", en: "Tenants" },
33
+ "admin-shell:overview.users": { de: "Benutzer", en: "Users" },
33
34
  "admin-shell:overview.failedJobs": { de: "Fehlgeschlagene Jobs", en: "Failed jobs" },
34
35
  "admin-shell:overview.failedJobsHint": { de: "Job-Runs prüfen", en: "Review job runs" },
35
36
  };
@@ -13,7 +13,11 @@ export const TENANT_OVERVIEW_ALLOWED_QUERIES = [
13
13
  ] as const;
14
14
 
15
15
  /** Platform workspace overview may only call these queries. */
16
- export const PLATFORM_OVERVIEW_ALLOWED_QUERIES = ["tenant:query:list", "jobs:query:list"] as const;
16
+ export const PLATFORM_OVERVIEW_ALLOWED_QUERIES = [
17
+ "tenant:query:list",
18
+ "jobs:query:list",
19
+ "user:query:user:list",
20
+ ] as const;
17
21
 
18
22
  /** Regression guard — TenantAdmin overview must never touch these (HTTP 403). */
19
23
  export const TENANT_OVERVIEW_FORBIDDEN_QUERIES = [
@@ -4,6 +4,7 @@ import { useDispatcher, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
4
  import { type ReactNode, useEffect, useState } from "react";
5
5
  import { JobQueries } from "../../jobs/constants";
6
6
  import { TenantQueries } from "../../tenant/constants";
7
+ import { UserQueries } from "../../user/constants";
7
8
  import { OverviewLayout, type OverviewState } from "./overview-layout";
8
9
  import { overviewQuery } from "./overview-query";
9
10
 
@@ -27,6 +28,18 @@ export function PlatformOverviewScreen(): ReactNode {
27
28
  return;
28
29
  }
29
30
 
31
+ const usersRes = await overviewQuery<{ readonly rows: readonly unknown[] }>(
32
+ "platform",
33
+ dispatcher,
34
+ UserQueries.list,
35
+ {},
36
+ );
37
+ if (cancelled) return;
38
+ if (!usersRes.isSuccess) {
39
+ setState({ kind: "error", message: usersRes.error.message });
40
+ return;
41
+ }
42
+
30
43
  const failedJobsRes = await overviewQuery<{ readonly rows: readonly unknown[] }>(
31
44
  "platform",
32
45
  dispatcher,
@@ -46,6 +59,10 @@ export function PlatformOverviewScreen(): ReactNode {
46
59
  label: t("admin-shell:overview.tenants"),
47
60
  value: String(tenantsRes.data.rows.length),
48
61
  },
62
+ {
63
+ label: t("admin-shell:overview.users"),
64
+ value: String(usersRes.data.rows.length),
65
+ },
49
66
  {
50
67
  label: t("admin-shell:overview.failedJobs"),
51
68
  value: String(failedJobsRes.data.rows.length),
@@ -69,7 +86,7 @@ export function PlatformOverviewScreen(): ReactNode {
69
86
  title={t("admin-shell:overview.platformTitle")}
70
87
  state={state}
71
88
  loadingLabel={t("admin-shell:overview.loading")}
72
- columns={2}
89
+ columns={3}
73
90
  />
74
91
  );
75
92
  }
@@ -53,7 +53,7 @@ describe("groupBlocksByFolder", () => {
53
53
  args: { slug: "imprint", lang: "de" },
54
54
  });
55
55
  expect(root.children).toBeUndefined();
56
- expect(root.icon).toBeUndefined();
56
+ expect(root.icon).toBe("file");
57
57
  });
58
58
  test('folder="page" → Folder-Container mit child', () => {
59
59
  const nodes = inside(
@@ -101,7 +101,7 @@ describe("groupBlocksByFolder", () => {
101
101
  );
102
102
  expect(nodes).toHaveLength(2);
103
103
  expect(nodes[0]?.label).toBe("imprint");
104
- expect(nodes[0]?.icon).toBeUndefined();
104
+ expect(nodes[0]?.icon).toBe("file");
105
105
  expect(nodes[1]?.label).toBe("page");
106
106
  expect(nodes[1]?.icon).toBe("folder");
107
107
  });
@@ -184,7 +184,7 @@ describe("groupBlocksByFolder", () => {
184
184
  const leaf = nodes[0];
185
185
  const folder = nodes[1];
186
186
  expect(leaf?.label).toBe("Page-Root");
187
- expect(leaf?.icon).toBeUndefined();
187
+ expect(leaf?.icon).toBe("file");
188
188
  expect(leaf?.target).toBeDefined();
189
189
  expect(folder?.label).toBe("page");
190
190
  expect(folder?.icon).toBe("folder");
@@ -77,6 +77,7 @@ function newFolderNode(): FolderNode {
77
77
  function attachBlock(root: FolderNode, block: BlockSummary, tenantIdOverride?: string): void {
78
78
  const leaf: TreeNode = {
79
79
  label: block.title || block.slug,
80
+ icon: "file",
80
81
  target: {
81
82
  featureId: "text-content",
82
83
  action: "edit",
@@ -217,9 +218,11 @@ type SetResponse = { readonly slug: string; readonly lang: string; readonly isNe
217
218
 
218
219
  function TextContentEditor({
219
220
  target,
220
- onClose,
221
221
  }: {
222
222
  readonly target: TargetRef;
223
+ // onClose bleibt Teil des Resolver-Contracts (Visual-Panel liefert es),
224
+ // wird aber nicht mehr gebraucht: die Tree-Auswahl navigiert, kein
225
+ // Schließen-Button mehr. Nicht destrukturiert → kein unused binding.
223
226
  readonly onClose: () => void;
224
227
  }): ReactNode {
225
228
  // @cast-boundary visual-tree-args — TargetRef.args ist erased zu
@@ -309,61 +312,53 @@ function TextContentEditor({
309
312
  const disabled = submitting || loading || !canWrite;
310
313
 
311
314
  return (
312
- <div className="flex h-full flex-col">
313
- <header className="flex items-center justify-between border-b px-6 py-4">
314
- <div>
315
- <h2 className="text-lg font-semibold">Text-Block bearbeiten</h2>
316
- <p className="text-xs text-muted-foreground">
317
- {slug || "—"} ({lang || "—"})
318
- </p>
319
- </div>
320
- <Button variant="secondary" onClick={onClose}>
321
- schlie&szlig;en
322
- </Button>
323
- </header>
324
- <div className="flex-1 overflow-y-auto px-6 py-6">
325
- <Form onSubmit={onSubmit}>
326
- {loading && <Banner variant="loading">Lädt aktuellen Stand…</Banner>}
327
- {loadError !== null && (
328
- <Banner variant="error">Konnte Block nicht laden: {loadError.code}</Banner>
329
- )}
330
- {!canWrite && !loading && (
331
- <Banner variant="info">
332
- Read-only — TenantAdmin- oder SystemAdmin-Rolle f&uuml;r &Auml;nderungen erforderlich.
333
- </Banner>
334
- )}
335
- <Field id="text-content-title" label="Titel" required>
336
- <Input
337
- kind="text"
338
- id="text-content-title"
339
- name="text-content-title"
340
- value={title}
341
- onChange={setTitle}
342
- disabled={disabled}
343
- required
344
- />
345
- </Field>
346
- <Field id="text-content-body" label="Inhalt">
347
- <Input
348
- kind="textarea"
349
- id="text-content-body"
350
- name="text-content-body"
351
- value={body}
352
- onChange={setBody}
353
- disabled={disabled}
354
- rows={14}
355
- />
356
- </Field>
357
- {saveError !== null && <Banner variant="error">{saveError}</Banner>}
358
- {savedMsg !== null && <Banner variant="info">{savedMsg}</Banner>}
359
- {canWrite && (
360
- <Button type="submit" loading={submitting} disabled={disabled}>
361
- {submitting ? "Speichern…" : "Speichern"}
362
- </Button>
363
- )}
364
- </Form>
365
- </div>
366
- </div>
315
+ <Form
316
+ onSubmit={onSubmit}
317
+ testId="text-content-editor"
318
+ title={title || slug || "—"}
319
+ subtitle={lang !== "" ? `(${lang})` : undefined}
320
+ actions={
321
+ canWrite ? (
322
+ <Button type="submit" loading={submitting} disabled={disabled}>
323
+ {submitting ? "Speichern…" : "Speichern"}
324
+ </Button>
325
+ ) : undefined
326
+ }
327
+ >
328
+ {loading && <Banner variant="loading">Lädt aktuellen Stand…</Banner>}
329
+ {loadError !== null && (
330
+ <Banner variant="error">Konnte Block nicht laden: {loadError.code}</Banner>
331
+ )}
332
+ {!canWrite && !loading && (
333
+ <Banner variant="info">
334
+ Read-only — TenantAdmin- oder SystemAdmin-Rolle f&uuml;r &Auml;nderungen erforderlich.
335
+ </Banner>
336
+ )}
337
+ <Field id="text-content-title" label="Titel" required>
338
+ <Input
339
+ kind="text"
340
+ id="text-content-title"
341
+ name="text-content-title"
342
+ value={title}
343
+ onChange={setTitle}
344
+ disabled={disabled}
345
+ required
346
+ />
347
+ </Field>
348
+ <Field id="text-content-body" label="Inhalt">
349
+ <Input
350
+ kind="textarea"
351
+ id="text-content-body"
352
+ name="text-content-body"
353
+ value={body}
354
+ onChange={setBody}
355
+ disabled={disabled}
356
+ rows={14}
357
+ />
358
+ </Field>
359
+ {saveError !== null && <Banner variant="error">{saveError}</Banner>}
360
+ {savedMsg !== null && <Banner variant="info">{savedMsg}</Banner>}
361
+ </Form>
367
362
  );
368
363
  }
369
364
 
@@ -1,3 +1,4 @@
1
+ // @runtime client
1
2
  // Feature name
2
3
  export const USER_FEATURE = "user" as const;
3
4