@cosmicdrift/kumiko-bundled-features 0.329.0 → 0.330.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.
Files changed (72) hide show
  1. package/README.md +17 -0
  2. package/dist/admin-shell/feature.d.ts +5 -0
  3. package/dist/admin-shell/feature.js +15 -61
  4. package/dist/admin-shell/i18n.js +35 -9
  5. package/dist/admin-shell/overview-allowlist.d.ts +2 -2
  6. package/dist/admin-shell/overview-allowlist.js +15 -9
  7. package/dist/admin-shell/overview-panels.d.ts +12 -0
  8. package/dist/admin-shell/overview-panels.js +167 -0
  9. package/dist/auth-email-password/email-templates.d.ts +1 -0
  10. package/dist/auth-email-password/email-templates.js +1 -1
  11. package/dist/auth-mfa/i18n.js +9 -0
  12. package/dist/auth-mfa/screens.js +3 -3
  13. package/dist/billing-foundation/checkout-core.d.ts +1 -0
  14. package/dist/billing-foundation/checkout-core.js +1 -0
  15. package/dist/billing-foundation/constants.d.ts +1 -0
  16. package/dist/billing-foundation/constants.js +1 -0
  17. package/dist/billing-foundation/get-subscription-for-tenant.d.ts +1 -0
  18. package/dist/billing-foundation/get-subscription-for-tenant.js +1 -0
  19. package/dist/billing-foundation/i18n.js +1 -1
  20. package/dist/billing-foundation/types.d.ts +1 -0
  21. package/dist/billing-foundation/types.js +1 -0
  22. package/dist/cap-counter/enforce-cap.d.ts +1 -0
  23. package/dist/cap-counter/enforce-cap.js +1 -0
  24. package/dist/cap-overview/handlers/caps-usage.query.js +3 -3
  25. package/dist/data-retention/run-retention-cleanup.js +6 -1
  26. package/dist/delivery/tables.js +4 -2
  27. package/dist/jobs/feature.js +2 -2
  28. package/dist/jobs/i18n.js +6 -0
  29. package/dist/jobs/job-run-table.js +1 -0
  30. package/dist/metrics/compute.d.ts +13 -0
  31. package/dist/metrics/compute.js +127 -0
  32. package/dist/metrics/constants.d.ts +7 -0
  33. package/dist/metrics/constants.js +14 -0
  34. package/dist/metrics/default-metrics.d.ts +9 -0
  35. package/dist/metrics/default-metrics.js +115 -0
  36. package/dist/metrics/feature.d.ts +4 -0
  37. package/dist/metrics/feature.js +119 -0
  38. package/dist/metrics/i18n.d.ts +5 -0
  39. package/dist/metrics/i18n.js +16 -0
  40. package/dist/metrics/index.d.ts +5 -0
  41. package/dist/metrics/index.js +6 -0
  42. package/dist/metrics/types.d.ts +70 -0
  43. package/dist/metrics/types.js +3 -0
  44. package/dist/metrics/validate.d.ts +3 -0
  45. package/dist/metrics/validate.js +74 -0
  46. package/dist/metrics/window.d.ts +13 -0
  47. package/dist/metrics/window.js +63 -0
  48. package/dist/personal-access-tokens/i18n.js +3 -0
  49. package/dist/personal-access-tokens/screens.js +6 -1
  50. package/dist/sessions/schema/user-session.js +1 -0
  51. package/dist/shared/grace-period.d.ts +1 -0
  52. package/dist/shared/grace-period.js +1 -0
  53. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +1 -0
  54. package/dist/tenant-lifecycle/run-tenant-destroy.js +1 -0
  55. package/dist/tier-engine/feature.js +1 -1
  56. package/dist/tier-engine/i18n.js +3 -0
  57. package/dist/user-data-rights/email-templates.d.ts +1 -0
  58. package/dist/user-data-rights/email-templates.js +1 -1
  59. package/dist/user-data-rights/feature.js +2 -0
  60. package/dist/user-data-rights/web/i18n.js +2 -0
  61. package/dist/user-profile/feature.js +4 -3
  62. package/dist/user-profile/i18n.js +2 -1
  63. package/package.json +17 -9
  64. package/src/admin-shell/changes.json +13 -0
  65. package/src/billing-foundation/changes.json +6 -0
  66. package/src/data-retention/changes.json +5 -0
  67. package/src/delivery/changes.json +7 -0
  68. package/src/jobs/changes.json +7 -0
  69. package/src/metrics/changes.json +15 -0
  70. package/src/personal-access-tokens/changes.json +5 -0
  71. package/src/sessions/changes.json +7 -0
  72. package/src/user-data-rights/changes.json +6 -0
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @cosmicdrift/kumiko-bundled-features
2
+
3
+ Built-in Kumiko features: tenant, user, auth, jobs, audit, delivery, billing and more. The pieces most apps rewrite anyway, already typed and tested.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @cosmicdrift/kumiko-bundled-features
9
+ ```
10
+
11
+ ```ts
12
+ import { createJobsFeature } from "@cosmicdrift/kumiko-bundled-features/jobs";
13
+ ```
14
+
15
+ Every feature is a subpath export (for example `@cosmicdrift/kumiko-bundled-features/tenant`) with a `create*Feature` factory or a ready-made feature value, and optional `/web` client plugins for the UI.
16
+
17
+ Documentation: https://kumiko.rocks
@@ -1,4 +1,5 @@
1
1
  import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type MetricDefinition } from "../metrics/index.js";
2
3
  export type CreateAdminShellOptions = {
3
4
  /** Short workspace id for tenant operators (URL segment). Default `tenant-admin`. */
4
5
  readonly workspaceIds?: {
@@ -13,5 +14,9 @@ export type CreateAdminShellOptions = {
13
14
  readonly includeCapOverview?: boolean;
14
15
  /** When false, only overview screens + nav are registered — app owns r.workspace(). Default true. */
15
16
  readonly registerWorkspaces?: boolean;
17
+ /** Metric list the overview panels are built from — pass the SAME list to
18
+ * createMetricsFeature / createSystemMetricsFeature. Panels exist only for
19
+ * ids in this list. Default DEFAULT_METRICS. */
20
+ readonly metrics?: readonly MetricDefinition[];
16
21
  };
17
22
  export declare function createAdminShellFeature(options?: CreateAdminShellOptions): FeatureDefinition;
@@ -2,20 +2,19 @@
2
2
  // Screens live in owner features (tenant, audit, jobs, …); this feature only
3
3
  // composes workspaces and cross-feature nav entries.
4
4
  import { access, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
5
- import { ConfigQueries } from "../config/index.js";
6
- import { JobQueries } from "../jobs/index.js";
7
- import { TenantQueries } from "../tenant/index.js";
8
- import { UserQueries } from "../user/index.js";
5
+ import { DEFAULT_METRICS } from "../metrics/index.js";
9
6
  import { ADMIN_SHELL_FEATURE, DEFAULT_PLATFORM_WORKSPACE_ID, DEFAULT_TENANT_WORKSPACE_ID, PLATFORM_OVERVIEW_SCREEN_ID, TENANT_OVERVIEW_SCREEN_ID, } from "./constants.js";
10
7
  import { ADMIN_SHELL_I18N } from "./i18n.js";
8
+ import { OVERVIEW_TIME_RANGE, PLATFORM_SCOPE, PLATFORM_TENANT_FILTER, platformOverviewPanels, tenantOverviewPanels, } from "./overview-panels.js";
11
9
  export function createAdminShellFeature(options = {}) {
12
10
  const tenantWsId = options.workspaceIds?.tenant ?? DEFAULT_TENANT_WORKSPACE_ID;
13
11
  const platformWsId = options.workspaceIds?.platform ?? DEFAULT_PLATFORM_WORKSPACE_ID;
14
12
  const includeTierAdmin = options.includeTierAdmin ?? true;
15
13
  const includeCapOverview = options.includeCapOverview ?? false;
16
14
  const registerWorkspaces = options.registerWorkspaces ?? true;
15
+ const metrics = options.metrics ?? DEFAULT_METRICS;
17
16
  return defineFeature(ADMIN_SHELL_FEATURE, (r) => {
18
- r.describe("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`, optional `cap-overview:screen:my-caps`/`cap-overview:screen:tenant-cap-list`). 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()`.");
17
+ r.describe("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`, optional `cap-overview:screen:my-caps`/`cap-overview:screen:tenant-cap-list`) and the two overview dashboards, built from the metrics list (`metrics` option, default DEFAULT_METRICS). Mount after user, tenant, audit, jobs, metrics and metrics-system; pass `workspaceIds` to match app URL conventions (e.g. Studio `d`/`s`, PublicStatus `admin`/`sysadmin`). Client: `adminShellClient()`, `tenantClient()`, `auditClient()`, `jobsClient()`, optional `tierEngineClient()`.");
19
18
  r.uiHints({
20
19
  displayLabel: "Admin Shell",
21
20
  category: "operations",
@@ -25,6 +24,8 @@ export function createAdminShellFeature(options = {}) {
25
24
  r.requires("tenant");
26
25
  r.requires("audit");
27
26
  r.requires("jobs");
27
+ r.requires("metrics");
28
+ r.requires("metrics-system");
28
29
  if (includeTierAdmin)
29
30
  r.requires("tier-engine");
30
31
  if (includeCapOverview)
@@ -47,35 +48,9 @@ export function createAdminShellFeature(options = {}) {
47
48
  id: TENANT_OVERVIEW_SCREEN_ID,
48
49
  type: "dashboard",
49
50
  access: { roles: access.admin },
50
- description: "Landing page of the tenant-admin workspace, showing pending invitation, member and missing-config counts for the caller's own tenant so an operator sees what needs attention there.",
51
- panels: [
52
- {
53
- kind: "stat",
54
- id: "pending-invitations",
55
- label: "admin-shell:overview.pendingInvitations",
56
- query: TenantQueries.invitations,
57
- // tenant:query:invitations returns a bare array, not a paged
58
- // envelope — "length" reads record["length"], i.e. the array's
59
- // own .length. Not a typo.
60
- valueField: "length",
61
- },
62
- {
63
- kind: "stat",
64
- id: "members",
65
- label: "admin-shell:overview.members",
66
- query: TenantQueries.members,
67
- // Same array-shaped response as invitations above.
68
- valueField: "length",
69
- },
70
- {
71
- kind: "stat",
72
- id: "missing-config",
73
- label: "admin-shell:overview.missingConfig",
74
- query: ConfigQueries.readiness,
75
- valueField: "missingCount",
76
- toneField: "missingTone",
77
- },
78
- ],
51
+ description: "admin-shell.screen.tenant-overview.subtitle",
52
+ timeRange: OVERVIEW_TIME_RANGE,
53
+ panels: [...tenantOverviewPanels(metrics, { includeCapOverview })],
79
54
  });
80
55
  r.nav({
81
56
  id: "tenant-overview",
@@ -88,33 +63,12 @@ export function createAdminShellFeature(options = {}) {
88
63
  id: PLATFORM_OVERVIEW_SCREEN_ID,
89
64
  type: "dashboard",
90
65
  access: { roles: access.systemAdmin },
91
- description: "Landing page of the platform-admin workspace, showing installation-wide tenant, user and failed-job counts so a system admin sees the health of the whole deployment at a glance.",
92
- panels: [
93
- {
94
- kind: "stat",
95
- id: "tenants",
96
- label: "admin-shell:overview.tenants",
97
- query: TenantQueries.list,
98
- params: { totalCount: true },
99
- valueField: "total",
100
- },
101
- {
102
- kind: "stat",
103
- id: "users",
104
- label: "admin-shell:overview.users",
105
- query: UserQueries.list,
106
- params: { totalCount: true },
107
- valueField: "total",
108
- },
109
- {
110
- kind: "stat",
111
- id: "failed-jobs",
112
- label: "admin-shell:overview.failedJobs",
113
- query: JobQueries.list,
114
- params: { status: "failed", totalCount: true },
115
- valueField: "total",
116
- },
117
- ],
66
+ description: "admin-shell.screen.platform-overview.subtitle",
67
+ scope: PLATFORM_SCOPE,
68
+ timeRange: OVERVIEW_TIME_RANGE,
69
+ // The picker needs a SystemAdmin {rows:{value,label}} tenant query; only cap-overview ships one.
70
+ ...(includeCapOverview && { filter: PLATFORM_TENANT_FILTER }),
71
+ panels: [...platformOverviewPanels(metrics)],
118
72
  });
119
73
  r.nav({
120
74
  id: "platform-overview",
@@ -4,6 +4,12 @@
4
4
  export const ADMIN_SHELL_I18N = {
5
5
  "screen:tenant-overview.title": { en: "Overview" },
6
6
  "screen:platform-overview.title": { en: "Overview" },
7
+ "admin-shell.screen.tenant-overview.subtitle": {
8
+ en: "Activity, failed jobs and deliveries, quotas and open admin tasks of this tenant.",
9
+ },
10
+ "admin-shell.screen.platform-overview.subtitle": {
11
+ en: "Tenant, user, job and delivery health across the whole installation.",
12
+ },
7
13
  "admin-shell:workspace.tenant": { en: "Administration" },
8
14
  "admin-shell:workspace.platform": { en: "Platform" },
9
15
  "admin-shell:nav.tenantOverview": { en: "Overview" },
@@ -12,14 +18,34 @@ export const ADMIN_SHELL_I18N = {
12
18
  "admin-shell:nav.tierAdmin": { en: "Assign tier" },
13
19
  "admin-shell:nav.myCaps": { en: "Plans & Caps" },
14
20
  "admin-shell:nav.tenantCaps": { en: "Plans & Caps" },
15
- "admin-shell:overview.pendingInvitations": {
16
- en: "Pending invitations",
17
- },
18
- "admin-shell:overview.members": { en: "Members" },
19
- "admin-shell:overview.missingConfig": {
20
- en: "Missing configuration",
21
+ "admin-shell:overview.kpi.activeTenants": { en: "Active tenants" },
22
+ "admin-shell:overview.kpi.activeUsers7d": { en: "Active users, 7 days" },
23
+ "admin-shell:overview.kpi.activeMembers7d": { en: "Active members, 7 days" },
24
+ "admin-shell:overview.kpi.failedJobs24h": { en: "Failed jobs, 24 h" },
25
+ "admin-shell:overview.kpi.failedDeliveries24h": { en: "Failed deliveries, 24 h" },
26
+ "admin-shell:overview.chart.jobRunsByStatus": { en: "Job runs by status" },
27
+ "admin-shell:overview.chart.deliveriesByChannel": { en: "Deliveries by channel" },
28
+ "admin-shell:overview.chart.auditWrites": { en: "Changes per day" },
29
+ "admin-shell:overview.chart.auditWritesSubtitle": { en: "Audit log, 14 days" },
30
+ "admin-shell:overview.list.recentFailures": { en: "Recent failures" },
31
+ "admin-shell:overview.list.recentFailuresEmpty": { en: "No failed jobs" },
32
+ "admin-shell:overview.list.quotas": { en: "Quotas" },
33
+ "admin-shell:overview.list.quotasEmpty": { en: "No quotas configured" },
34
+ "admin-shell:overview.col.job": { en: "Job" },
35
+ "admin-shell:overview.col.startedAt": { en: "Started" },
36
+ "admin-shell:overview.col.failedAt": { en: "Failed at" },
37
+ "admin-shell:overview.col.reason": { en: "Reason" },
38
+ "admin-shell:overview.col.quota": { en: "Quota" },
39
+ "admin-shell:overview.col.used": { en: "Used" },
40
+ "admin-shell:overview.col.limit": { en: "Limit" },
41
+ "admin-shell:overview.col.usage": { en: "Usage" },
42
+ "admin-shell:overview.range.24h": { en: "24 hours" },
43
+ "admin-shell:overview.range.7d": { en: "7 days" },
44
+ "admin-shell:overview.range.30d": { en: "30 days" },
45
+ "admin-shell:overview.scope.badge": { en: "All tenants" },
46
+ "admin-shell:overview.scope.notice": {
47
+ en: "You are viewing data of all tenants. Cross-tenant queries are recorded in the audit log.",
21
48
  },
22
- "admin-shell:overview.tenants": { en: "Tenants" },
23
- "admin-shell:overview.users": { en: "Users" },
24
- "admin-shell:overview.failedJobs": { en: "Failed jobs" },
49
+ "admin-shell:overview.filter.tenant": { en: "Tenant" },
50
+ "admin-shell:overview.filter.allTenants": { en: "All tenants" },
25
51
  };
@@ -1,8 +1,8 @@
1
1
  export type OverviewWorkspaceKind = "tenant" | "platform";
2
2
  /** Tenant workspace overview may only call these queries. */
3
- export declare const TENANT_OVERVIEW_ALLOWED_QUERIES: readonly ["tenant:query:invitations", "tenant:query:members", "config:query:readiness", "cap-counter:query:get-counter"];
3
+ export declare const TENANT_OVERVIEW_ALLOWED_QUERIES: readonly ["jobs:query:failures", "cap-overview:query:caps:usage"];
4
4
  /** Platform workspace overview may only call these queries. */
5
- export declare const PLATFORM_OVERVIEW_ALLOWED_QUERIES: readonly ["tenant:query:list", "jobs:query:list", "user:query:user:list"];
5
+ export declare const PLATFORM_OVERVIEW_ALLOWED_QUERIES: readonly ["jobs:query:list", "cap-overview:query:tenant-options"];
6
6
  /** Regression guard — TenantAdmin overview must never touch these (HTTP 403). */
7
7
  export declare const TENANT_OVERVIEW_FORBIDDEN_QUERIES: readonly ["tenant:query:list", "jobs:query:list", "feature-toggles:query:list", "feature-toggles:query:registered", "user:query:user:list"];
8
8
  export declare function overviewAllowedQueries(kind: OverviewWorkspaceKind): readonly string[];
@@ -1,18 +1,23 @@
1
- // @runtime client
2
1
  // Hard-coded query allowlists for overview-home screens — security boundary
3
2
  // against privilege escalation via accidental cross-workspace fetches.
3
+ import { CapOverviewQueries } from "../cap-overview/index.js";
4
+ import { JobQueries } from "../jobs/index.js";
5
+ import { METRICS_FEATURE, METRICS_SYSTEM_FEATURE } from "../metrics/index.js";
4
6
  /** Tenant workspace overview may only call these queries. */
5
7
  export const TENANT_OVERVIEW_ALLOWED_QUERIES = [
6
- "tenant:query:invitations",
7
- "tenant:query:members",
8
- "config:query:readiness",
9
- "cap-counter:query:get-counter",
8
+ JobQueries.failures,
9
+ CapOverviewQueries.capsUsage,
10
10
  ];
11
+ // Metric ids come from the app's metric list, so the boundary is the feature
12
+ // prefix: tenant-scope metrics for the tenant overview, system-scope for the platform one.
13
+ const METRIC_QUERY_PREFIX = {
14
+ tenant: `${METRICS_FEATURE}:query:`,
15
+ platform: `${METRICS_SYSTEM_FEATURE}:query:`,
16
+ };
11
17
  /** Platform workspace overview may only call these queries. */
12
18
  export const PLATFORM_OVERVIEW_ALLOWED_QUERIES = [
13
- "tenant:query:list",
14
- "jobs:query:list",
15
- "user:query:user:list",
19
+ JobQueries.list,
20
+ CapOverviewQueries.tenantOptions,
16
21
  ];
17
22
  /** Regression guard — TenantAdmin overview must never touch these (HTTP 403). */
18
23
  export const TENANT_OVERVIEW_FORBIDDEN_QUERIES = [
@@ -26,5 +31,6 @@ export function overviewAllowedQueries(kind) {
26
31
  return kind === "tenant" ? TENANT_OVERVIEW_ALLOWED_QUERIES : PLATFORM_OVERVIEW_ALLOWED_QUERIES;
27
32
  }
28
33
  export function isOverviewQueryAllowed(kind, queryName) {
29
- return overviewAllowedQueries(kind).includes(queryName);
34
+ return (overviewAllowedQueries(kind).includes(queryName) ||
35
+ queryName.startsWith(METRIC_QUERY_PREFIX[kind]));
30
36
  }
@@ -0,0 +1,12 @@
1
+ import type { DashboardFilterDefinition, DashboardPanelDefinition, DashboardTimeRangeDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { type MetricDefinition } from "../metrics/index.js";
3
+ export declare const OVERVIEW_TIME_RANGE: DashboardTimeRangeDefinition;
4
+ export declare const PLATFORM_TENANT_FILTER: DashboardFilterDefinition;
5
+ export declare const PLATFORM_SCOPE: {
6
+ readonly badge: "admin-shell:overview.scope.badge";
7
+ readonly notice: "admin-shell:overview.scope.notice";
8
+ };
9
+ export declare function platformOverviewPanels(metrics: readonly MetricDefinition[]): readonly DashboardPanelDefinition[];
10
+ export declare function tenantOverviewPanels(metrics: readonly MetricDefinition[], options: {
11
+ readonly includeCapOverview: boolean;
12
+ }): readonly DashboardPanelDefinition[];
@@ -0,0 +1,167 @@
1
+ // Declarative panel sets of the two overview dashboards. Panels are derived from
2
+ // the metric list the app mounts, so a reduced list yields a reduced dashboard
3
+ // instead of panels pointing at queries that do not exist.
4
+ import { CapOverviewQueries } from "../cap-overview/index.js";
5
+ import { JobQueries } from "../jobs/index.js";
6
+ import { metricQueryName } from "../metrics/index.js";
7
+ const I = "admin-shell:overview";
8
+ export const OVERVIEW_TIME_RANGE = {
9
+ id: "range",
10
+ options: [
11
+ { value: "24h", label: `${I}.range.24h` },
12
+ { value: "7d", label: `${I}.range.7d` },
13
+ { value: "30d", label: `${I}.range.30d` },
14
+ ],
15
+ default: "7d",
16
+ };
17
+ export const PLATFORM_TENANT_FILTER = {
18
+ id: "tenantId",
19
+ label: `${I}.filter.tenant`,
20
+ kind: "select",
21
+ allLabel: `${I}.filter.allTenants`,
22
+ optionsQuery: CapOverviewQueries.tenantOptions,
23
+ };
24
+ export const PLATFORM_SCOPE = {
25
+ badge: `${I}.scope.badge`,
26
+ notice: `${I}.scope.notice`,
27
+ };
28
+ function contextFor(metrics, scope) {
29
+ return {
30
+ scope,
31
+ has: (metricId) => metrics.some((metric) => metric.id === metricId && metric.scopes.includes(scope)),
32
+ };
33
+ }
34
+ function kpi(ctx, metricId, label, options = {}) {
35
+ if (!ctx.has(metricId))
36
+ return [];
37
+ return [
38
+ {
39
+ kind: "stat",
40
+ id: `kpi-${metricId}`,
41
+ label: `${I}.kpi.${label}`,
42
+ query: metricQueryName(ctx.scope, metricId),
43
+ valueField: "value",
44
+ ...(options.trend === true && {
45
+ deltaField: "delta",
46
+ deltaDirectionField: "deltaDirection",
47
+ sparklineField: "points",
48
+ }),
49
+ ...(options.negative === true && { tone: "negative" }),
50
+ },
51
+ ];
52
+ }
53
+ function kpiStrip(stats) {
54
+ return stats.length === 0 ? [] : [{ kind: "stat-group", id: "kpis", stats }];
55
+ }
56
+ function chart(ctx, metricId, panel) {
57
+ if (!ctx.has(metricId))
58
+ return [];
59
+ return [{ kind: "chart", query: metricQueryName(ctx.scope, metricId), ...panel }];
60
+ }
61
+ const DELIVERY_SEGMENT_TONES = {
62
+ sent: "positive",
63
+ failed: "negative",
64
+ skipped: "neutral",
65
+ queued: "neutral",
66
+ };
67
+ const HALF = { span: "half" };
68
+ function recentFailuresList(query, params, columns, span) {
69
+ return {
70
+ kind: "list",
71
+ id: "recent-failures",
72
+ label: `${I}.list.recentFailures`,
73
+ query,
74
+ params,
75
+ ignoreScreenFilter: true,
76
+ columns,
77
+ span,
78
+ emptyLabel: `${I}.list.recentFailuresEmpty`,
79
+ };
80
+ }
81
+ export function platformOverviewPanels(metrics) {
82
+ const ctx = contextFor(metrics, "system");
83
+ // Sources without a tenant column cannot be narrowed by the tenant picker.
84
+ const tenantless = (panel) => ({
85
+ ...panel,
86
+ ignoreScreenFilter: true,
87
+ });
88
+ return [
89
+ ...kpiStrip([
90
+ ...kpi(ctx, "active-tenants", "activeTenants").map(tenantless),
91
+ ...kpi(ctx, "active-users", "activeUsers7d", { trend: true }),
92
+ ...kpi(ctx, "failed-job-runs", "failedJobs24h", { trend: true, negative: true }).map(tenantless),
93
+ ...kpi(ctx, "failed-deliveries", "failedDeliveries24h", { trend: true, negative: true }),
94
+ ]),
95
+ ...chart(ctx, "job-runs-by-status", {
96
+ id: "job-runs-by-status",
97
+ label: `${I}.chart.jobRunsByStatus`,
98
+ chart: "stacked-bars",
99
+ ...HALF,
100
+ ignoreScreenFilter: true,
101
+ seriesTones: {
102
+ completed: "positive",
103
+ failed: "negative",
104
+ running: "active",
105
+ queued: "neutral",
106
+ },
107
+ }),
108
+ ...chart(ctx, "deliveries-by-channel", {
109
+ id: "deliveries-by-channel",
110
+ label: `${I}.chart.deliveriesByChannel`,
111
+ chart: "segment-bars",
112
+ ...HALF,
113
+ seriesTones: DELIVERY_SEGMENT_TONES,
114
+ }),
115
+ recentFailuresList(JobQueries.list, { status: "failed", sort: "startedAt", sortDirection: "desc", limit: 5 }, [
116
+ { field: "jobName", label: `${I}.col.job` },
117
+ { field: "startedAt", label: `${I}.col.startedAt` },
118
+ ], "full"),
119
+ ];
120
+ }
121
+ export function tenantOverviewPanels(metrics, options) {
122
+ const ctx = contextFor(metrics, "tenant");
123
+ return [
124
+ ...kpiStrip([
125
+ ...kpi(ctx, "active-users", "activeMembers7d", { trend: true }),
126
+ ...kpi(ctx, "tenant-job-failures", "failedJobs24h", { trend: true, negative: true }),
127
+ ...kpi(ctx, "failed-deliveries", "failedDeliveries24h", { trend: true, negative: true }),
128
+ ]),
129
+ ...chart(ctx, "deliveries-by-channel", {
130
+ id: "deliveries-by-channel",
131
+ label: `${I}.chart.deliveriesByChannel`,
132
+ chart: "segment-bars",
133
+ ...HALF,
134
+ seriesTones: DELIVERY_SEGMENT_TONES,
135
+ }),
136
+ ...(options.includeCapOverview
137
+ ? [
138
+ {
139
+ kind: "list",
140
+ id: "quotas",
141
+ label: `${I}.list.quotas`,
142
+ query: CapOverviewQueries.capsUsage,
143
+ columns: [
144
+ { field: "label", label: `${I}.col.quota` },
145
+ { field: "used", label: `${I}.col.used` },
146
+ { field: "limit", label: `${I}.col.limit` },
147
+ { field: "fraction", label: `${I}.col.usage`, display: "bar" },
148
+ ],
149
+ ...HALF,
150
+ emptyLabel: `${I}.list.quotasEmpty`,
151
+ },
152
+ ]
153
+ : []),
154
+ ...chart(ctx, "audit-writes", {
155
+ id: "audit-writes",
156
+ label: `${I}.chart.auditWrites`,
157
+ subtitle: `${I}.chart.auditWritesSubtitle`,
158
+ chart: "stacked-bars",
159
+ ...HALF,
160
+ }),
161
+ recentFailuresList(JobQueries.failures, { limit: 5 }, [
162
+ { field: "jobName", label: `${I}.col.job` },
163
+ { field: "messageKey", label: `${I}.col.reason` },
164
+ { field: "failedAt", label: `${I}.col.failedAt` },
165
+ ], "half"),
166
+ ];
167
+ }
@@ -1,4 +1,5 @@
1
1
  export type AuthMailLocale = string;
2
+ export declare const AUTH_MAIL_EN: Readonly<Record<string, string>>;
2
3
  export type RenderTokenContentArgs = {
3
4
  readonly url: string;
4
5
  readonly expiresAt: string;
@@ -9,7 +9,7 @@
9
9
  // (localeDe() / localeEs() call registerMailTranslations).
10
10
  import { mailT, registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
11
11
  import { Temporal } from "temporal-polyfill";
12
- const AUTH_MAIL_EN = {
12
+ export const AUTH_MAIL_EN = {
13
13
  "auth.mail.appNameDefault": "Account",
14
14
  "auth.mail.reset.subject": "{app} — Reset your password",
15
15
  "auth.mail.reset.greeting": "Hi,",
@@ -2,6 +2,15 @@ export const AUTH_MFA_FEATURE_I18N = {
2
2
  "screen:auth-mfa-enable.title": {
3
3
  en: "Two-factor authentication",
4
4
  },
5
+ "auth-mfa.screen.auth-mfa-enable.subtitle": {
6
+ en: "Protect your account with a code from an authenticator app.",
7
+ },
8
+ "auth-mfa.screen.auth-mfa-disable.subtitle": {
9
+ en: "Turn off two-factor authentication. Other sessions and access tokens are signed out.",
10
+ },
11
+ "auth-mfa.screen.auth-mfa-regenerate-recovery.subtitle": {
12
+ en: "Replace all recovery codes. The new codes are shown only once.",
13
+ },
5
14
  "mfa.enable.start": { en: "Start setup" },
6
15
  "mfa.enable.reveal.title": { en: "Set up your authenticator app" },
7
16
  "mfa.enable.reveal.warning": {
@@ -19,7 +19,7 @@ export const mfaEnableScreen = {
19
19
  // Reached only via a direct link from account settings, never from a
20
20
  // list — no nav area to resolve in isolation.
21
21
  dormant: true,
22
- description: "Self-service screen where a signed-in user enrolls in TOTP two-factor authentication: it shows the QR code and manual secret, reveals the recovery codes once, and confirms enrollment with a code from their authenticator app.",
22
+ description: "auth-mfa.screen.auth-mfa-enable.subtitle",
23
23
  reveal: {
24
24
  title: i18nKey("mfa.enable.reveal.title"),
25
25
  warning: i18nKey("mfa.enable.reveal.warning"),
@@ -75,7 +75,7 @@ export const mfaDisableScreen = {
75
75
  // Reached only via a direct link from account settings, never from a
76
76
  // list — no nav area to resolve in isolation.
77
77
  dormant: true,
78
- description: "Self-service screen where a signed-in user turns two-factor authentication off by entering a code from their authenticator app or a recovery code; their other sessions and access tokens are signed out.",
78
+ description: "auth-mfa.screen.auth-mfa-disable.subtitle",
79
79
  };
80
80
  export const mfaRegenerateRecoveryScreen = {
81
81
  id: MFA_REGENERATE_RECOVERY_SCREEN_ID,
@@ -93,7 +93,7 @@ export const mfaRegenerateRecoveryScreen = {
93
93
  // Reached only via a direct link from account settings, never from a
94
94
  // list — no nav area to resolve in isolation.
95
95
  dormant: true,
96
- description: "Self-service screen where a signed-in user with two-factor authentication replaces all recovery codes after confirming with a current code; the new codes are shown once.",
96
+ description: "auth-mfa.screen.auth-mfa-regenerate-recovery.subtitle",
97
97
  reveal: {
98
98
  title: i18nKey("mfa.regenerate.reveal.title"),
99
99
  warning: i18nKey("mfa.regenerate.reveal.warning"),
@@ -1,3 +1,4 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
1
2
  import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { getSubscriptionForTenant } from "./get-subscription-for-tenant.js";
3
4
  import type { BillingPlanCatalog, SubscriptionProviderPlugin } from "./types.js";
@@ -1,3 +1,4 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
1
2
  // Shared checkout plumbing for create-checkout-session, start-plan-checkout
2
3
  // and switch-plan — provider/catalog resolution, redirect-origin hardening,
3
4
  // and the price/subscription-state gate that turns a bare priceId-driven
@@ -1,3 +1,4 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
1
2
  import { Temporal as TemporalPolyfill } from "temporal-polyfill";
2
3
  export declare const BILLING_FOUNDATION_FEATURE: "billing-foundation";
3
4
  export declare const SUBSCRIPTION_PROVIDER_EXTENSION: "subscriptionProvider";
@@ -1,3 +1,4 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
1
2
  // @runtime client
2
3
  // temporal-polyfill is a plain npm dependency (not a framework runtime
3
4
  // module), so importing it here doesn't break browser bundles. Aliased —
@@ -1,3 +1,4 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
1
2
  import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
3
  export type SubscriptionView = {
3
4
  readonly tier: string;
@@ -1,3 +1,4 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
1
2
  // Resolver-helper: liest die current subscription-row für einen Tenant
2
3
  // aus der read_subscriptions-projection.
3
4
  import { configuredPiiSubjectKms, decryptPiiFieldValues, } from "@cosmicdrift/kumiko-framework/crypto";
@@ -52,7 +52,7 @@ export const BILLING_FOUNDATION_I18N = {
52
52
  en: "This tenant has no active subscription to switch.",
53
53
  },
54
54
  "billing-foundation.errors.cancellationScheduled": {
55
- en: "This subscription is scheduled to cancel. Reactivate it before switching plans.",
55
+ en: "This subscription is scheduled to end. Reactivate it before switching plans.",
56
56
  },
57
57
  "billing-foundation.errors.alreadyOnPlan": { en: "This tenant is already on that plan." },
58
58
  "billing-foundation.errors.planSwitchNotSupported": {
@@ -1,3 +1,4 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
1
2
  import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
3
  import type { HandlerContext, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
4
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
@@ -1,3 +1,4 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
1
2
  // Plugin-Contract + Domain-Types der subscription-foundation. Provider-
2
3
  // Plugins (subscription-stripe, subscription-mollie, ...) implementieren
3
4
  // `SubscriptionProviderPlugin` und registrieren via
@@ -1,3 +1,4 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
1
2
  import { type HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { KumikoError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  /**
@@ -1,3 +1,4 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
1
2
  import { createEntityExecutor } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { KumikoError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { markCapSoftWarned, readRollingCapUsage } from "./book-cap-usage.js";
@@ -13,10 +13,10 @@ export function createCapsUsageQuery(caps) {
13
13
  description: "Returns the calling user's own tenant's configured caps with used amount, tier limit and utilisation fraction; use it to answer how close a tenant is to its quota (only SystemAdmin may target another tenant via tenantId).",
14
14
  schema: z.object({ tenantId: z.string().min(1).optional() }),
15
15
  access: { roles: MY_CAPS_ACCESS_ROLES },
16
- // App-owned CapSpec.usage() providers get this handler's db; a SUM has
17
- // no TenantDb helper (count() is COUNT(*)-only), so they need raw SQL.
16
+ // COUNT/SUM/AVG are covered by TenantDb.count/TenantDb.aggregate; the grant
17
+ // stays for app-owned CapSpec.usage() providers that still use unsafeRaw.
18
18
  escapeHatch: {
19
- reason: "cap-overview:caps:usage — grants app-owned CapSpec.usage() providers raw SQL for aggregates TenantDb has no typed helper for (e.g. a SUM)",
19
+ reason: "cap-overview:caps:usage — grants app-owned CapSpec.usage() providers that still use unsafeRaw; COUNT/SUM/AVG are covered by TenantDb.count/TenantDb.aggregate",
20
20
  },
21
21
  handler: async (query, ctx) => {
22
22
  if (!ctx.systemDb) {
@@ -495,7 +495,12 @@ export async function runRetentionCleanup(args) {
495
495
  case "blockDelete": {
496
496
  const targets = await resolveAnonymizeTargets(entity.fields);
497
497
  if (!targets) {
498
- skipped.push({ entityName, reason: "missing_anonymize_fields" });
498
+ // blockDelete without anonymize fields is a pure hold: a subjectRef-only
499
+ // person link outlives the hold and is cut only by the user-data
500
+ // forget hook, so expiry has nothing to do here and is not reported.
501
+ if (policy.strategy === "anonymize") {
502
+ skipped.push({ entityName, reason: "missing_anonymize_fields" });
503
+ }
499
504
  break;
500
505
  }
501
506
  const executor = createEventStoreExecutor(proj.table, entity, { entityName });
@@ -1,4 +1,4 @@
1
- import { boolean, buildBaseColumns, defineUnmanagedTable, instant, table as pgTable, sql, text, uniqueIndex, uuid, } from "@cosmicdrift/kumiko-framework/db";
1
+ import { boolean, buildBaseColumns, defineUnmanagedTable, index, instant, table as pgTable, sql, text, uniqueIndex, uuid, } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { createBooleanField, createEntity, createTextField, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  // Delivery-log is an append-only stream of per-attempt records. The stream
4
4
  // of truth lives in the events-Tabelle (one aggregate per attempt, event
@@ -12,6 +12,7 @@ import { createBooleanField, createEntity, createTextField, } from "@cosmicdrift
12
12
  // PK = event aggregate-id (uuid). Keeps the projection row linked back to
13
13
  // its event stream 1:1 — same convention as jobRunsTable + tenantSecretsTable.
14
14
  // Event replays stay idempotent (primary-key conflict instead of duplicate rows).
15
+ const DELIVERY_ATTEMPTS_TENANT_CREATED_INDEX = "store_delivery_attempts_tenant_id_created_at_idx";
15
16
  export const deliveryAttemptsTable = pgTable("store_delivery_attempts", {
16
17
  id: uuid("id").primaryKey(),
17
18
  tenantId: uuid("tenant_id").notNull(),
@@ -26,7 +27,7 @@ export const deliveryAttemptsTable = pgTable("store_delivery_attempts", {
26
27
  // notify() priority from the event payload.
27
28
  priority: text("priority").notNull().default("normal").$type(),
28
29
  createdAt: instant("created_at").default(sql `now()`).notNull(),
29
- });
30
+ }, (t) => [index(DELIVERY_ATTEMPTS_TENANT_CREATED_INDEX).on(t.tenantId, t.createdAt)]);
30
31
  // **Unmanaged table** — bewusst KEIN createEntity. Begründung:
31
32
  // - id kommt aus dem Aggregate-Stream (kein gen_random_uuid()-DEFAULT)
32
33
  // - kein version/inserted_by/modified_by/modified_at — keine in-place-
@@ -37,6 +38,7 @@ export const deliveryAttemptsTable = pgTable("store_delivery_attempts", {
37
38
  // aus dieser Meta ab.
38
39
  export const deliveryAttemptsTableMeta = defineUnmanagedTable({
39
40
  tableName: "store_delivery_attempts",
41
+ indexes: [{ name: DELIVERY_ATTEMPTS_TENANT_CREATED_INDEX, columns: ["tenant_id", "created_at"] }],
40
42
  columns: [
41
43
  { name: "id", pgType: "uuid", notNull: true, primaryKey: true },
42
44
  { name: "tenant_id", pgType: "uuid", notNull: true },