@cosmicdrift/kumiko-bundled-features 0.329.0 → 0.330.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/README.md +17 -0
- package/dist/admin-shell/feature.d.ts +5 -0
- package/dist/admin-shell/feature.js +15 -61
- package/dist/admin-shell/i18n.js +35 -9
- package/dist/admin-shell/overview-allowlist.d.ts +2 -2
- package/dist/admin-shell/overview-allowlist.js +15 -9
- package/dist/admin-shell/overview-panels.d.ts +12 -0
- package/dist/admin-shell/overview-panels.js +167 -0
- package/dist/auth-email-password/email-templates.d.ts +1 -0
- package/dist/auth-email-password/email-templates.js +1 -1
- package/dist/auth-mfa/i18n.js +9 -0
- package/dist/auth-mfa/screens.js +3 -3
- package/dist/billing-foundation/checkout-core.d.ts +1 -0
- package/dist/billing-foundation/checkout-core.js +1 -0
- package/dist/billing-foundation/constants.d.ts +1 -0
- package/dist/billing-foundation/constants.js +1 -0
- package/dist/billing-foundation/get-subscription-for-tenant.d.ts +1 -0
- package/dist/billing-foundation/get-subscription-for-tenant.js +1 -0
- package/dist/billing-foundation/i18n.js +1 -1
- package/dist/billing-foundation/types.d.ts +1 -0
- package/dist/billing-foundation/types.js +1 -0
- package/dist/cap-counter/enforce-cap.d.ts +1 -0
- package/dist/cap-counter/enforce-cap.js +1 -0
- package/dist/cap-overview/handlers/caps-usage.query.js +3 -3
- package/dist/delivery/tables.js +4 -2
- package/dist/jobs/feature.js +2 -2
- package/dist/jobs/i18n.js +6 -0
- package/dist/jobs/job-run-table.js +1 -0
- package/dist/metrics/compute.d.ts +13 -0
- package/dist/metrics/compute.js +127 -0
- package/dist/metrics/constants.d.ts +7 -0
- package/dist/metrics/constants.js +14 -0
- package/dist/metrics/default-metrics.d.ts +9 -0
- package/dist/metrics/default-metrics.js +115 -0
- package/dist/metrics/feature.d.ts +4 -0
- package/dist/metrics/feature.js +119 -0
- package/dist/metrics/i18n.d.ts +5 -0
- package/dist/metrics/i18n.js +16 -0
- package/dist/metrics/index.d.ts +5 -0
- package/dist/metrics/index.js +6 -0
- package/dist/metrics/types.d.ts +70 -0
- package/dist/metrics/types.js +3 -0
- package/dist/metrics/validate.d.ts +3 -0
- package/dist/metrics/validate.js +74 -0
- package/dist/metrics/window.d.ts +13 -0
- package/dist/metrics/window.js +63 -0
- package/dist/personal-access-tokens/i18n.js +3 -0
- package/dist/personal-access-tokens/screens.js +6 -1
- package/dist/sessions/schema/user-session.js +1 -0
- package/dist/shared/grace-period.d.ts +1 -0
- package/dist/shared/grace-period.js +1 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +1 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +1 -0
- package/dist/tier-engine/feature.js +1 -1
- package/dist/tier-engine/i18n.js +3 -0
- package/dist/user-data-rights/email-templates.d.ts +1 -0
- package/dist/user-data-rights/email-templates.js +1 -1
- package/dist/user-data-rights/feature.js +2 -0
- package/dist/user-data-rights/web/i18n.js +2 -0
- package/dist/user-profile/feature.js +4 -3
- package/dist/user-profile/i18n.js +2 -1
- package/package.json +17 -9
- package/src/admin-shell/changes.json +13 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/delivery/changes.json +7 -0
- package/src/jobs/changes.json +7 -0
- package/src/metrics/changes.json +15 -0
- package/src/personal-access-tokens/changes.json +5 -0
- package/src/sessions/changes.json +7 -0
- 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 {
|
|
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
|
|
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: "
|
|
51
|
-
|
|
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: "
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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",
|
package/dist/admin-shell/i18n.js
CHANGED
|
@@ -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.
|
|
16
|
-
|
|
17
|
-
},
|
|
18
|
-
"admin-shell:overview.
|
|
19
|
-
"admin-shell:overview.
|
|
20
|
-
|
|
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.
|
|
23
|
-
"admin-shell:overview.
|
|
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 ["
|
|
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 ["
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
+
}
|
|
@@ -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,",
|
package/dist/auth-mfa/i18n.js
CHANGED
|
@@ -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": {
|
package/dist/auth-mfa/screens.js
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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
|
// 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
|
|
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
|
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
|
-
//
|
|
17
|
-
//
|
|
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
|
|
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) {
|
package/dist/delivery/tables.js
CHANGED
|
@@ -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 },
|