@cosmicdrift/kumiko-samples 0.393.0 → 0.395.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +15 -5
- package/packages/bundled-features/src/account-security/README.md +1 -1
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/feature.ts +9 -1
- package/packages/bundled-features/src/audit/event-classification.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
- package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
- package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
- package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
- package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
- package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
- package/packages/bundled-features/src/cap-overview/types.ts +2 -0
- package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
- package/packages/bundled-features/src/files/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +143 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
- package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
- package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
- package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
- package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -2
- package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +93 -15
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
- package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
- package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
- package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
- package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +238 -89
- package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
- package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
- package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
- package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
- package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +36 -0
- package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +124 -41
- package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/constants.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +61 -136
- package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
- package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +9 -0
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
- package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
- package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
- package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +4 -6
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +75 -45
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +19 -8
- package/packages/bundled-features/src/inbound-provider-imap/screens.ts +203 -109
- package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
- package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
- package/packages/bundled-features/src/inbound-provider-imap/watch-mailbox.ts +109 -0
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
- package/packages/bundled-features/src/jobs/changes.json +18 -0
- package/packages/bundled-features/src/managed-pages/changes.json +7 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +7 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +6 -4
- package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +2 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-body-guard.ts +61 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-body-issues.ts +26 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-crud.ts +17 -10
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +15 -4
- package/packages/bundled-features/src/managed-pages/render-page-body.ts +28 -0
- package/packages/bundled-features/src/managed-pages/screens/page-screens.ts +6 -2
- package/packages/bundled-features/src/managed-pages/seeding.ts +13 -2
- package/packages/bundled-features/src/managed-pages/table.ts +6 -2
- package/packages/bundled-features/src/managed-pages/web/client-plugin.tsx +6 -4
- package/packages/bundled-features/src/managed-pages/web/constants.ts +14 -0
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +14 -0
- package/packages/bundled-features/src/managed-pages/web/lossy-markdown.ts +71 -0
- package/packages/bundled-features/src/managed-pages/web/markdown-to-document.ts +27 -0
- package/packages/bundled-features/src/managed-pages/web/page-body-section.tsx +73 -0
- package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
- package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
- package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
- package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
- package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
- package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
- package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
- package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
- package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
- package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
- package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
- package/packages/bundled-features/src/template-resolver/README.md +2 -2
- package/packages/bundled-features/src/template-resolver/changes.json +20 -0
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +16 -16
- package/packages/bundled-features/src/template-resolver/feature.ts +14 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +24 -6
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +32 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -0
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +9 -3
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +13 -480
- package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +105 -0
- package/packages/bundled-features/src/template-resolver/web/collection-lookup.ts +33 -0
- package/packages/bundled-features/src/template-resolver/web/constants.ts +3 -0
- package/packages/bundled-features/src/template-resolver/web/content-field.tsx +61 -0
- package/packages/bundled-features/src/template-resolver/web/content-tree-provider.ts +256 -0
- package/packages/bundled-features/src/template-resolver/web/document-reference-name.ts +12 -0
- package/packages/bundled-features/src/template-resolver/web/document-schema.ts +3 -11
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +9 -2
- package/packages/bundled-features/src/template-resolver/web/legacy-variable-text.ts +30 -0
- package/packages/bundled-features/src/template-resolver/web/tenant-override.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/text-block-editor.tsx +92 -0
- package/packages/bundled-features/src/template-resolver/web/use-collection-editor-context.ts +42 -0
- package/packages/bundled-features/src/template-resolver/web/use-collection-entry-create.ts +92 -0
- package/packages/bundled-features/src/template-resolver/web/use-content-write.ts +81 -0
- package/packages/bundled-features/src/template-resolver/web/use-text-block-editor.ts +127 -0
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +9 -1
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +89 -40
- package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
- package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/client.tsx +8 -1
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
- package/samples/apps/use-all-bundled/src/app/seed.ts +17 -2
- package/samples/apps/use-all-bundled/src/run-config.ts +19 -11
- package/samples/recipes/cross-feature-events/README.md +10 -0
- package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
- package/samples/recipes/dashboard-stats/README.md +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
- package/samples/recipes/env-composition/README.md +2 -2
- package/samples/recipes/env-composition/src/env.ts +2 -2
- package/samples/recipes/inbound-mail-triage/README.md +8 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
- package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
- package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
- package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
- package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
- package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"dev": "PORT=4186 bun --env-file=../../../.env run src/app/server.ts",
|
|
11
11
|
"screenshots": "bun x playwright test e2e/screenshots.spec.ts --config=playwright.config.ts",
|
|
12
12
|
"billing-gallery": "bun x playwright test --config=playwright.billing-gallery.config.ts",
|
|
13
|
+
"docs-guides": "bun x playwright test e2e/docs-guides.screenshots.spec.ts --config=playwright.config.ts && bun x playwright test --config=playwright.billing-docs.config.ts",
|
|
13
14
|
"e2e": "bun x playwright test e2e/mfa-login.spec.ts --config=playwright.config.ts",
|
|
14
15
|
"gen:manifest": "bun run scripts/gen-feature-manifest.ts",
|
|
15
16
|
"gen:sample-index": "bun run scripts/gen-sample-index.ts"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @runtime test
|
|
2
|
+
// English docs screenshots of the billing plans screen. Boots the gallery server with the docs
|
|
3
|
+
// variant switched on (net prices with a note, gross recurring price in the plan switch); the
|
|
4
|
+
// German review gallery has its own config. Gated by SCREENSHOT_DIR, see the `billing-docs` script.
|
|
5
|
+
import { defineAppE2eConfig, PLAYWRIGHT_DEMO_ENV } from "@cosmicdrift/kumiko-testing/e2e";
|
|
6
|
+
import { E2E_PORTS } from "../../e2e/e2e-ports";
|
|
7
|
+
import { GALLERY_DOCS_VARIANT_ENV } from "./e2e-billing-gallery/gallery-constants";
|
|
8
|
+
|
|
9
|
+
export default defineAppE2eConfig({
|
|
10
|
+
port: E2E_PORTS["framework/use-all-bundled-billing-gallery"],
|
|
11
|
+
env: { ...PLAYWRIGHT_DEMO_ENV, [GALLERY_DOCS_VARIANT_ENV]: "1" },
|
|
12
|
+
locale: "en",
|
|
13
|
+
testDir: "./e2e-billing-docs",
|
|
14
|
+
serverEntry: "e2e-billing-gallery/server.ts",
|
|
15
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CapSpec } from "@cosmicdrift/kumiko-bundled-features/cap-overview";
|
|
2
2
|
import { noteEntryTable } from "@cosmicdrift/kumiko-bundled-features/notes-history";
|
|
3
3
|
import { tagEntity } from "@cosmicdrift/kumiko-bundled-features/tags";
|
|
4
|
+
import { templateResourcesTable } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
4
5
|
import { tenantMembershipsTable } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
5
6
|
|
|
6
7
|
import { buildEntityTable, selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
@@ -11,10 +12,11 @@ const tagTable = buildEntityTable("tag", tagEntity);
|
|
|
11
12
|
const NOTE_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 10, pro: 100 };
|
|
12
13
|
const TAG_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 5, pro: 50 };
|
|
13
14
|
const SEAT_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 5, pro: 20 };
|
|
15
|
+
const TEMPLATE_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 3, pro: 30 };
|
|
14
16
|
const DEFAULT_LIMIT = 10;
|
|
15
17
|
|
|
16
|
-
//
|
|
17
|
-
// (notes-history, tags, tenant-membership) — demonstrates the caller-supplied
|
|
18
|
+
// Five example caps built from entities this sample already mounts
|
|
19
|
+
// (notes-history, tags, tenant-membership, template-resolver) — demonstrates the caller-supplied
|
|
18
20
|
// CapSpec contract cap-overview reads against, the same shape a real app
|
|
19
21
|
// would provide for its own usage tables.
|
|
20
22
|
export const CAP_OVERVIEW_CAPS: readonly CapSpec[] = [
|
|
@@ -68,4 +70,16 @@ export const CAP_OVERVIEW_CAPS: readonly CapSpec[] = [
|
|
|
68
70
|
},
|
|
69
71
|
icon: "gauge",
|
|
70
72
|
},
|
|
73
|
+
{
|
|
74
|
+
id: "templates",
|
|
75
|
+
label: "cap-overview.caps.templates",
|
|
76
|
+
limit: (tier) => TEMPLATE_LIMIT_BY_TIER[tier] ?? DEFAULT_LIMIT,
|
|
77
|
+
usage: async (db, tenantId) => {
|
|
78
|
+
const rows = await selectMany(db, templateResourcesTable, { tenantId: [tenantId] });
|
|
79
|
+
return rows.length;
|
|
80
|
+
},
|
|
81
|
+
// Counter only: the limit stays enforced server-side but is not shown to the tenant.
|
|
82
|
+
showLimit: false,
|
|
83
|
+
icon: "file",
|
|
84
|
+
},
|
|
71
85
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
|
|
3
|
-
// Labels for the
|
|
3
|
+
// Labels for the five example caps (notes, tags, seats, apiCalls, templates) wired up in
|
|
4
4
|
// cap-overview-caps.ts. Cap ids are the app's choice, so their labels are
|
|
5
5
|
// too — cap-overview itself carries no vocabulary for app-owned caps, same
|
|
6
6
|
// reasoning as collection-labels-feature.ts for content collections.
|
|
@@ -12,6 +12,7 @@ export const capOverviewLabelsFeature = defineFeature("cap-overview-labels", (r)
|
|
|
12
12
|
"cap-overview.caps.tags": { de: "Tags", en: "Tags" },
|
|
13
13
|
"cap-overview.caps.seats": { de: "Plätze", en: "Seats" },
|
|
14
14
|
"cap-overview.caps.apiCalls": { de: "API-Aufrufe", en: "API calls" },
|
|
15
|
+
"cap-overview.caps.templates": { de: "Vorlagen", en: "Templates" },
|
|
15
16
|
},
|
|
16
17
|
});
|
|
17
18
|
return {};
|
|
@@ -26,7 +26,9 @@ import { customFieldsClient } from "@cosmicdrift/kumiko-bundled-features/custom-
|
|
|
26
26
|
import { deliveryClient } from "@cosmicdrift/kumiko-bundled-features/delivery/web";
|
|
27
27
|
import { featureTogglesClient } from "@cosmicdrift/kumiko-bundled-features/feature-toggles/web";
|
|
28
28
|
import { foldersClient } from "@cosmicdrift/kumiko-bundled-features/folders/web";
|
|
29
|
+
import { inboundMailFoundationClient } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation/web";
|
|
29
30
|
import { jobsClient } from "@cosmicdrift/kumiko-bundled-features/jobs/web";
|
|
31
|
+
import { managedPagesClient } from "@cosmicdrift/kumiko-bundled-features/managed-pages/web";
|
|
30
32
|
import { sessionsClient } from "@cosmicdrift/kumiko-bundled-features/sessions/web";
|
|
31
33
|
import { tagsClient } from "@cosmicdrift/kumiko-bundled-features/tags/web";
|
|
32
34
|
import { textBlocksClient } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
@@ -99,6 +101,9 @@ createKumikoApp({
|
|
|
99
101
|
shell: AppShell,
|
|
100
102
|
clientFeatures: [
|
|
101
103
|
localeDeClient(),
|
|
104
|
+
userDataRightsClient({
|
|
105
|
+
publicDeletion: { requestPath: "/account/delete", confirmPath: "/account/delete/confirm" },
|
|
106
|
+
}),
|
|
102
107
|
emailPasswordClient({ mfaVerifyScreen: MfaVerifyScreen }),
|
|
103
108
|
authMfaClient(),
|
|
104
109
|
adminShellClient(),
|
|
@@ -112,14 +117,16 @@ createKumikoApp({
|
|
|
112
117
|
tierEngineClient(),
|
|
113
118
|
capOverviewClient(),
|
|
114
119
|
userProfileClient(),
|
|
115
|
-
userDataRightsClient(),
|
|
116
120
|
customFieldsClient(),
|
|
117
121
|
foldersClient(),
|
|
122
|
+
inboundMailFoundationClient(),
|
|
118
123
|
complianceProfilesClient(),
|
|
119
124
|
featureTogglesClient(),
|
|
120
125
|
// tag-list management screen (TagManager) + TagSection/TagFilter extension
|
|
121
126
|
// slots + tag i18n. Required for the dev-only notesFeature host to render.
|
|
122
127
|
tagsClient(),
|
|
128
|
+
// Document editor of the page-edit body section.
|
|
129
|
+
managedPagesClient(),
|
|
123
130
|
// template-resolver text-block tree + editor under the app-owned "content"
|
|
124
131
|
// nav node. Points at SYSTEM_TENANT because that is where the legal and
|
|
125
132
|
// marketing copy is seeded.
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
// (#1258): an open screen not reachable via nav is no longer an eligible
|
|
6
6
|
// fallback candidate.
|
|
7
7
|
|
|
8
|
+
import { MAIL_ACCOUNT_LIST_SCREEN_QN } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
8
9
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
10
|
+
import { ADMIN_NAV_QN, ADMIN_NAV_WORKSPACES } from "./admin-nav-feature";
|
|
9
11
|
|
|
10
12
|
export const appScreensFeature: FeatureDefinition = defineFeature("app-screens", (r) => {
|
|
11
|
-
r.describe("App-side nav placement for the user-profile self-service page.");
|
|
12
|
-
r.requires({ features: ["user-profile"] });
|
|
13
|
+
r.describe("App-side nav placement for the user-profile self-service page and the mailbox list.");
|
|
14
|
+
r.requires({ features: ["user-profile", "inbound-mail-foundation"] });
|
|
13
15
|
r.nav({
|
|
14
16
|
id: "profile",
|
|
15
17
|
label: "screen:profile.title",
|
|
@@ -29,8 +31,18 @@ export const appScreensFeature: FeatureDefinition = defineFeature("app-screens",
|
|
|
29
31
|
access: { roles: ["SystemAdmin"] },
|
|
30
32
|
workspaces: ["admin-shell:workspace:platform"],
|
|
31
33
|
});
|
|
34
|
+
r.nav({
|
|
35
|
+
id: "mail-accounts",
|
|
36
|
+
label: "nav:mail-accounts.title",
|
|
37
|
+
icon: "mail",
|
|
38
|
+
screen: MAIL_ACCOUNT_LIST_SCREEN_QN,
|
|
39
|
+
parent: ADMIN_NAV_QN,
|
|
40
|
+
workspaces: ADMIN_NAV_WORKSPACES,
|
|
41
|
+
order: 20,
|
|
42
|
+
});
|
|
32
43
|
r.translations({
|
|
33
44
|
keys: {
|
|
45
|
+
"nav:mail-accounts.title": { de: "Postfächer", en: "Mailboxes" },
|
|
34
46
|
"screen:profile.title": { de: "Profil", en: "Profile" },
|
|
35
47
|
"nav:content.title": { de: "Inhalte", en: "Content" },
|
|
36
48
|
},
|
|
@@ -335,7 +335,19 @@ export const seedScreenshotData: SeedFn = async (stack) => {
|
|
|
335
335
|
slug: "about",
|
|
336
336
|
lang: "en",
|
|
337
337
|
title: "About Acme",
|
|
338
|
-
body:
|
|
338
|
+
body: [
|
|
339
|
+
"# About Acme",
|
|
340
|
+
"",
|
|
341
|
+
"We build calm software for small teams. Acme was founded in 2019 in Hamburg and now supports more than 400 companies across Europe.",
|
|
342
|
+
"",
|
|
343
|
+
"## What we stand for",
|
|
344
|
+
"",
|
|
345
|
+
"- Software that stays out of your way",
|
|
346
|
+
"- Fair, per-seat pricing without surprises",
|
|
347
|
+
"- Your data stays in the EU",
|
|
348
|
+
"",
|
|
349
|
+
"Read more about how we handle data in our [privacy policy](/legal/privacy).",
|
|
350
|
+
].join("\n"),
|
|
339
351
|
published: true,
|
|
340
352
|
});
|
|
341
353
|
await seedPage(stack.db, {
|
|
@@ -468,10 +480,13 @@ export const seedScreenshotData: SeedFn = async (stack) => {
|
|
|
468
480
|
// carry whatever the handler derives instead of a hand-built projection.
|
|
469
481
|
// SYSTEM_TENANT, not the dev tenant: the screenshot admin reads the system
|
|
470
482
|
// tenant — the same reason the text-block tree above seeds there.
|
|
483
|
+
// The real admin, not TestUsers.systemAdmin's id: the seed signs in with a live session row,
|
|
484
|
+
// and the session list shows that row's user. The sessionChecker re-derives roles from the
|
|
485
|
+
// admin's global SystemAdmin role, which the collection's access accepts.
|
|
471
486
|
const snippetAuthor: SessionUser = {
|
|
472
487
|
...TestUsers.systemAdmin,
|
|
473
488
|
tenantId: SYSTEM_TENANT_ID,
|
|
474
|
-
|
|
489
|
+
id: devAdminId,
|
|
475
490
|
};
|
|
476
491
|
for (const snippet of [
|
|
477
492
|
{
|
|
@@ -75,8 +75,8 @@ import { createRendererPdfFeature } from "@cosmicdrift/kumiko-bundled-features/r
|
|
|
75
75
|
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
76
76
|
import { createSeoFeature } from "@cosmicdrift/kumiko-bundled-features/seo";
|
|
77
77
|
import { createStepDispatcherFeature } from "@cosmicdrift/kumiko-bundled-features/step-dispatcher";
|
|
78
|
-
import { createSubscriptionDodoFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-dodo";
|
|
79
78
|
import { createSubscriptionMollieFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-mollie";
|
|
79
|
+
import { createSubscriptionPaddleFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-paddle";
|
|
80
80
|
import { createSubscriptionStripeFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
|
|
81
81
|
import { createTagsFeature } from "@cosmicdrift/kumiko-bundled-features/tags";
|
|
82
82
|
import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
@@ -212,10 +212,12 @@ export const APP_FEATURES = [
|
|
|
212
212
|
webhookSecret: "whsec_smoke",
|
|
213
213
|
priceToTier: { price_smoke: "free" },
|
|
214
214
|
}),
|
|
215
|
-
|
|
216
|
-
apiKey: "
|
|
217
|
-
webhookSecret: "
|
|
218
|
-
|
|
215
|
+
createSubscriptionPaddleFeature({
|
|
216
|
+
apiKey: "paddle_smoke",
|
|
217
|
+
webhookSecret: "pdl_ntfset_smoke",
|
|
218
|
+
tenantBindingKey: "paddle_smoke_binding_key",
|
|
219
|
+
clientToken: "test_smoke_client_token",
|
|
220
|
+
priceToTier: { pri_smoke: "free" },
|
|
219
221
|
}),
|
|
220
222
|
createSubscriptionMollieFeature({
|
|
221
223
|
apiKey: "test_smoke_key",
|
|
@@ -314,10 +316,10 @@ export const APP_FEATURES = [
|
|
|
314
316
|
label: "templateResolver:nav.snippets",
|
|
315
317
|
workspaces: ["admin-shell:workspace:platform"],
|
|
316
318
|
},
|
|
317
|
-
//
|
|
319
|
+
// document + variableSchema so the smoke app renders the WYSIWYG with
|
|
318
320
|
// insertable chips, not just the plain textarea — the preview
|
|
319
321
|
// substitutes these example values for `{{customerName}}` & co.
|
|
320
|
-
contentFormat: "
|
|
322
|
+
contentFormat: "document",
|
|
321
323
|
variableSchema: {
|
|
322
324
|
customerName: "Max Mustermann",
|
|
323
325
|
orderId: "A-1042",
|
|
@@ -333,7 +335,7 @@ export const APP_FEATURES = [
|
|
|
333
335
|
label: "templateResolver:nav.signatures",
|
|
334
336
|
workspaces: ["admin-shell:workspace:platform"],
|
|
335
337
|
},
|
|
336
|
-
contentFormat: "
|
|
338
|
+
contentFormat: "document",
|
|
337
339
|
},
|
|
338
340
|
],
|
|
339
341
|
}),
|
|
@@ -350,12 +352,18 @@ export const APP_FEATURES = [
|
|
|
350
352
|
// PUPPETEER_SERVICE_URL + PUPPETEER_AUTH_TOKEN and only asked when a PDF is generated.
|
|
351
353
|
createRendererPuppeteerClientFeature(),
|
|
352
354
|
|
|
353
|
-
// managed-pages: requires config (auto-bundled).
|
|
354
|
-
//
|
|
355
|
+
// managed-pages: requires config (auto-bundled). Every host resolves to the
|
|
356
|
+
// dev tenant so the document-editor e2e can read a saved page back from the
|
|
357
|
+
// public route. allowCustomCss:true boot-validates the CSS code path + emits
|
|
355
358
|
// the branding-custom-css key into the manifest. The managed-pages-css
|
|
356
359
|
// companion toggle is handler-less, has no subpath export, and is recipe-
|
|
357
360
|
// covered — not standalone-mounted here (smoke = one mount per subpath-export).
|
|
358
|
-
createManagedPagesFeature({
|
|
361
|
+
createManagedPagesFeature({
|
|
362
|
+
// Mapping every host to the dev tenant is for this sample and its e2e only; production maps
|
|
363
|
+
// its known hosts and returns null for any other.
|
|
364
|
+
resolveApexTenant: () => DEV_TENANT_ID as TenantId,
|
|
365
|
+
allowCustomCss: true,
|
|
366
|
+
}),
|
|
359
367
|
|
|
360
368
|
// Template overview, layout editor and PDF template editor; needs template-resolver and managed-pages above.
|
|
361
369
|
createDocumentDesignerFeature(),
|
|
@@ -26,6 +26,13 @@ Three building blocks:
|
|
|
26
26
|
external system). With `table`, the apply materializes a persistent
|
|
27
27
|
read model.
|
|
28
28
|
|
|
29
|
+
- **`r.job(name, { trigger: { on: event.name, catchUp: true } }, run)`**
|
|
30
|
+
runs a job per stored event. With `catchUp` the job owns its own
|
|
31
|
+
consumer cursor (`job:<jobQn>`) that starts at the beginning of the
|
|
32
|
+
event store, so a job added to a running app also processes the
|
|
33
|
+
events from before its deploy. Each event enqueues at most one run.
|
|
34
|
+
Without `catchUp` a job only sees events appended after it went live.
|
|
35
|
+
|
|
29
36
|
## Flow
|
|
30
37
|
|
|
31
38
|
```
|
|
@@ -61,3 +68,6 @@ The integration test pins three guarantees:
|
|
|
61
68
|
```bash
|
|
62
69
|
bun kumiko test integration samples/cross-feature-events
|
|
63
70
|
```
|
|
71
|
+
4. **Catch-up job** — `catch-up-job.integration.test.ts` places orders
|
|
72
|
+
before the first dispatcher pass and checks that the `catchUp` job
|
|
73
|
+
invoices each of them exactly once.
|
|
@@ -60,6 +60,7 @@ export type CapturedEvent = {
|
|
|
60
60
|
readonly tenantId: string;
|
|
61
61
|
};
|
|
62
62
|
export const capturedEvents: CapturedEvent[] = [];
|
|
63
|
+
export const invoicedOrderIds: string[] = [];
|
|
63
64
|
|
|
64
65
|
// --- Feature ---
|
|
65
66
|
|
|
@@ -103,6 +104,19 @@ export const pubsubOrderFeature = defineFeature("pubsubOrders", (r) => {
|
|
|
103
104
|
access: { roles: ["Admin", "Customer"] },
|
|
104
105
|
});
|
|
105
106
|
|
|
107
|
+
// A job can react to the same event. `catchUp: true` gives the job its own
|
|
108
|
+
// consumer cursor that starts at the beginning of the event store, so a job
|
|
109
|
+
// added to a running app also processes the orders placed before it existed.
|
|
110
|
+
// Without catchUp the job only sees events appended after its deploy.
|
|
111
|
+
r.job(
|
|
112
|
+
"invoice-order",
|
|
113
|
+
{ trigger: { on: orderPlaced.name, catchUp: true }, runIn: "worker" },
|
|
114
|
+
async (payload) => {
|
|
115
|
+
const order = orderPlaced.schema.parse(payload);
|
|
116
|
+
invoicedOrderIds.push(order.id);
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
106
120
|
// Async consumer — Marten-style. Fires after commit, via the event-
|
|
107
121
|
// dispatcher. No `table` means this MSP is pure side-effect (no state
|
|
108
122
|
// persisted). Delivery is at-least-once and ordered per consumer.
|
|
@@ -20,6 +20,14 @@ query and the fields to read, the query does the counting.
|
|
|
20
20
|
`list` with `filter`, `limit: 1` and `totalCount: true`. The total is counted
|
|
21
21
|
over the whole WHERE clause, so no rows are loaded just to be counted.
|
|
22
22
|
- **Labels are translation keys**: declared with `r.translations`.
|
|
23
|
+
- **Record-bound dashboard**: `ticket-record-dashboard` binds to a record with `record`
|
|
24
|
+
and embeds the `ticket-card` projectionDetail as a screen panel; the record id
|
|
25
|
+
becomes its entityId.
|
|
26
|
+
- **Gated toolbar button**: `visibleWhen` on a toolbar action asks a flat
|
|
27
|
+
capability query; the button is hidden while it loads or fails.
|
|
28
|
+
- **Fallback by reason**: `fallback: { forbidden, notFound }` renders a screen on
|
|
29
|
+
403/404 of the gate query; other reasons get the default notice or an error
|
|
30
|
+
banner with retry.
|
|
23
31
|
|
|
24
32
|
## When to reach for it
|
|
25
33
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Dashboard Stats Sample
|
|
2
2
|
// Shows: a dashboard screen with a stat-group and a single stat, backed by one
|
|
3
|
-
// query handler that counts in the database.
|
|
3
|
+
// query handler that counts in the database. A second, record-bound dashboard embeds a
|
|
4
|
+
// projectionDetail as a panel, hides a toolbar button behind a capability query, and picks its
|
|
5
|
+
// fallback screen by the reason the gate closed.
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
createEntity,
|
|
@@ -22,6 +24,7 @@ export const ticketEntity = createEntity({
|
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
export const TICKET_STATS_QUERY = "tickets:query:ticket:stats";
|
|
27
|
+
export const TICKET_CAPABILITIES_QUERY = "tickets:query:ticket:capabilities";
|
|
25
28
|
|
|
26
29
|
const readerRoles = { roles: ["Admin", "User"] } as const;
|
|
27
30
|
|
|
@@ -57,8 +60,51 @@ export const ticketFeature = defineFeature("tickets", (r) => {
|
|
|
57
60
|
access: readerRoles,
|
|
58
61
|
});
|
|
59
62
|
|
|
63
|
+
// Gate for the screens below: a flat answer with one boolean the screens test with `eq`.
|
|
64
|
+
r.queryHandler({
|
|
65
|
+
name: "ticket:capabilities",
|
|
66
|
+
schema: z.object({}),
|
|
67
|
+
outputSchema: z.object({ canClose: z.boolean() }),
|
|
68
|
+
handler: async (query) => ({ canClose: query.user.roles.includes("Admin") }),
|
|
69
|
+
access: readerRoles,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
r.queryHandler({
|
|
73
|
+
name: "ticket:card",
|
|
74
|
+
schema: z.object({ id: z.string() }),
|
|
75
|
+
handler: async (query, ctx) => {
|
|
76
|
+
const page = await ticketExecutor.list(
|
|
77
|
+
{ filter: { field: "id", op: "eq", value: query.payload.id }, limit: 1 },
|
|
78
|
+
query.user,
|
|
79
|
+
ctx.db,
|
|
80
|
+
);
|
|
81
|
+
const ticket = page.rows[0];
|
|
82
|
+
return { id: query.payload.id, title: ticket?.title ?? "", status: ticket?.status ?? "" };
|
|
83
|
+
},
|
|
84
|
+
access: readerRoles,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
r.queryHandler({
|
|
88
|
+
name: "ticket:rows",
|
|
89
|
+
schema: z.object({}),
|
|
90
|
+
handler: async (query, ctx) => {
|
|
91
|
+
const page = await ticketExecutor.list({ limit: 50 }, query.user, ctx.db);
|
|
92
|
+
return { rows: page.rows, nextCursor: null };
|
|
93
|
+
},
|
|
94
|
+
access: readerRoles,
|
|
95
|
+
});
|
|
96
|
+
|
|
60
97
|
r.translations({
|
|
61
98
|
keys: {
|
|
99
|
+
"screen:ticket-list.title": { en: "Tickets" },
|
|
100
|
+
"screen:ticket-card.title": { en: "Ticket" },
|
|
101
|
+
"screen:ticket-record-dashboard.title": { en: "Ticket overview" },
|
|
102
|
+
"entity:ticket:field:title": { en: "Title" },
|
|
103
|
+
"entity:ticket:field:status": { en: "Status" },
|
|
104
|
+
"entity:__projection-detail__:field:title": { en: "Title" },
|
|
105
|
+
"entity:__projection-detail__:field:status": { en: "Status" },
|
|
106
|
+
"tickets.toolbar.close": { en: "Close all done" },
|
|
107
|
+
"tickets.dashboard.card": { en: "Ticket" },
|
|
62
108
|
"tickets.dashboard.group": { en: "Tickets by status" },
|
|
63
109
|
"tickets.dashboard.open": { en: "Open" },
|
|
64
110
|
"tickets.dashboard.closed": { en: "Closed" },
|
|
@@ -101,4 +147,48 @@ export const ticketFeature = defineFeature("tickets", (r) => {
|
|
|
101
147
|
],
|
|
102
148
|
access: readerRoles,
|
|
103
149
|
});
|
|
150
|
+
|
|
151
|
+
// Three additions: a projectionDetail embedded in a record-bound dashboard, a gated toolbar
|
|
152
|
+
// button, and a fallback chosen by reason.
|
|
153
|
+
const gate = { query: TICKET_CAPABILITIES_QUERY, field: "canClose", eq: true } as const;
|
|
154
|
+
|
|
155
|
+
// The list doubles as the fallback target: it has no visibleWhen of its own.
|
|
156
|
+
r.screen({
|
|
157
|
+
id: "ticket-list",
|
|
158
|
+
type: "projectionList",
|
|
159
|
+
query: "tickets:query:ticket:rows",
|
|
160
|
+
columns: ["title", "status"],
|
|
161
|
+
access: readerRoles,
|
|
162
|
+
toolbarActions: [
|
|
163
|
+
{
|
|
164
|
+
kind: "writeHandler",
|
|
165
|
+
id: "close-done",
|
|
166
|
+
label: "tickets.toolbar.close",
|
|
167
|
+
handler: "tickets:write:ticket:update",
|
|
168
|
+
// Live and fail-closed: hidden while the query loads and when it fails.
|
|
169
|
+
visibleWhen: gate,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
r.screen({
|
|
175
|
+
id: "ticket-card",
|
|
176
|
+
type: "projectionDetail",
|
|
177
|
+
query: "tickets:query:ticket:card",
|
|
178
|
+
layout: { sections: [{ title: "tickets.dashboard.card", fields: ["title", "status"] }] },
|
|
179
|
+
access: readerRoles,
|
|
180
|
+
visibleWhen: gate,
|
|
181
|
+
// 403 and 404 on the gate query get their own screen; an unmet condition keeps the
|
|
182
|
+
// default notice and any other failure shows an error banner with retry.
|
|
183
|
+
fallback: { forbidden: "ticket-list", notFound: "ticket-list" },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// `record` reads the id from the URL; the embedded projectionDetail receives it as its entityId.
|
|
187
|
+
r.screen({
|
|
188
|
+
id: "ticket-record-dashboard",
|
|
189
|
+
type: "dashboard",
|
|
190
|
+
record: { param: "ticketId", payloadKey: "id" },
|
|
191
|
+
panels: [{ kind: "screen", id: "card", screen: "ticket-card" }],
|
|
192
|
+
access: readerRoles,
|
|
193
|
+
});
|
|
104
194
|
});
|
|
@@ -10,7 +10,7 @@ instead of keeping a hand-written copy of every feature's variables.
|
|
|
10
10
|
`kmsEnvSchema` and `s3EnvSchema`. `composeEnvSchema` collects them and
|
|
11
11
|
records which feature declared each variable.
|
|
12
12
|
- **App-level blocks in `extend`**: `buildInfoEnvSchema`, `demoAdminEnvSchema`,
|
|
13
|
-
`stripeEnvSchema({ prices })`, `
|
|
13
|
+
`stripeEnvSchema({ prices })`, `paddleEnvSchema({ prices })` and
|
|
14
14
|
`createS3EnvSchema({ prefix })` for a
|
|
15
15
|
second bucket. These belong to no mounted feature, so the app adds them.
|
|
16
16
|
- **Typed result**: `parseEnv(composed.schema, …)` returns the merged type,
|
|
@@ -27,7 +27,7 @@ instead of keeping a hand-written copy of every feature's variables.
|
|
|
27
27
|
| `smtpEnvSchema` | `channel-email` | `createChannelEmailFeature()` |
|
|
28
28
|
| `s3EnvSchema`, `createS3EnvSchema({ prefix })` | `file-provider-s3` | `createFileProviderS3Feature({ fromEnv: true })` |
|
|
29
29
|
| `stripeEnvSchema({ prices })` | `subscription-stripe` | app (`extend`); the feature registers the bridge keys |
|
|
30
|
-
| `
|
|
30
|
+
| `paddleEnvSchema({ prices })` | `subscription-paddle` | app (`extend`); the feature registers the bridge keys |
|
|
31
31
|
| `mfaTokenSecretsEnvSchema` | `auth-mfa` | `createAuthMfaFeature()` |
|
|
32
32
|
| `buildInfoEnvSchema`, `demoAdminEnvSchema` | `foundation-shared` | app (`extend`) |
|
|
33
33
|
| `prometheusMetricsEnvSchema` | `framework/observability` | app (`extend`) |
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
demoAdminEnvSchema,
|
|
16
16
|
} from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
|
|
17
17
|
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
18
|
-
import {
|
|
18
|
+
import { paddleEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-paddle";
|
|
19
19
|
import { stripeEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
|
|
20
20
|
import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";
|
|
21
21
|
import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
|
|
@@ -37,7 +37,7 @@ const appEnvSchema = z.object({
|
|
|
37
37
|
...buildInfoEnvSchema.shape,
|
|
38
38
|
...demoAdminEnvSchema.shape,
|
|
39
39
|
...stripeEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
|
|
40
|
-
...
|
|
40
|
+
...paddleEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
|
|
41
41
|
...createS3EnvSchema({ prefix: "RELEASES_S3_" }).shape,
|
|
42
42
|
APP_PUBLIC_URL: z.url().describe("Public origin of the app, used in mail links."),
|
|
43
43
|
});
|
|
@@ -28,6 +28,14 @@ mail-triage ← this sample (the app-side cons
|
|
|
28
28
|
|
|
29
29
|
When the provider rejects the stored login, the supervisor sets the account to `auth_error` and stops fetching. With `inbound-provider-imap` mounted, the mail accounts list offers **Reconnect** on that row: the form `mail-account-reconnect-imap` is prefilled with the stored server settings (never the password), `inbound-provider-imap:write:reconnect-mailbox` checks the new login and puts the same account back to `active`. Threads, sync cursor and seen messages stay, so the next poll continues where the account stopped instead of ingesting the mailbox again. The cases are covered in `inbound-provider-imap/__tests__/reconnect-mailbox.integration.test.ts`; they need greenmail, which this sample's in-memory provider does not.
|
|
30
30
|
|
|
31
|
+
## Mail content, quarantine and status
|
|
32
|
+
|
|
33
|
+
Every ingested mail now carries its full text (`bodyText`, at most 32,000 characters, flagged by `bodyTextTruncated`), the original (`.eml`, or `.html` for HTML-only mails) and its attachments as file references; `inbound-mail-foundation:query:message:content` returns them, while `message:list` stays light and never carries the body. Retention removes the mail row but keeps the attachment files. Needs file storage in the app; without it the mail is still imported and the mailbox shows **Unterbrochen** (`file_storage_unavailable`).
|
|
34
|
+
|
|
35
|
+
A mail the provider cannot parse is set aside in quarantine instead of blocking the mailbox: the rest imports, the sync cursor moves on, the account shows `degraded` with `lastError: message_quarantined` and the count, and `acknowledge-quarantine` clears it. Three failed polls in a row (`connection_failed`, `rate_limited`) also mark the mailbox `degraded` until the next success.
|
|
36
|
+
|
|
37
|
+
A disconnected mailbox keeps its account id: `reactivate-account` (or connecting the same OAuth address again) puts it back to `active` with its cursor, so nothing is imported twice, and the mail arrives right away instead of at the next poll. Connecting an address that is already connected is refused with 409.
|
|
38
|
+
|
|
31
39
|
## Tests
|
|
32
40
|
|
|
33
41
|
- E2E through `createAllInOneEntrypoint` (HTTP → dispatcher → BullMQ worker lane, no framework-internal shortcuts): connect → ingest → triage item appears.
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// FileRef: renderTablePdf draws the table (pure JS, no browser). The storage
|
|
5
5
|
// write runs in the beforeTransaction phase, so the transaction holds no pool
|
|
6
6
|
// connection while the bytes upload; the handler then only creates the
|
|
7
|
-
// fileRef row from event.prepared.
|
|
7
|
+
// fileRef row from event.prepared. Called from another handler (a nested
|
|
8
|
+
// write) there is no such phase, so the handler renders and stores inside the
|
|
9
|
+
// caller's transaction instead.
|
|
8
10
|
|
|
9
11
|
import { renderTablePdf } from "@cosmicdrift/kumiko-bundled-features/renderer-pdf";
|
|
10
12
|
import {
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
20
22
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
21
23
|
import {
|
|
24
|
+
createFileRefFromBytes,
|
|
22
25
|
createFileRefFromUploadedBytes,
|
|
23
26
|
uploadFileBytesBeforeTransaction,
|
|
24
27
|
} from "@cosmicdrift/kumiko-framework/files";
|
|
@@ -60,6 +63,62 @@ export const pdfAuditExportFeature = defineFeature("pdf-audit-export", (r) => {
|
|
|
60
63
|
r.entity({ name: "inspection", ...inspectionEntity });
|
|
61
64
|
const { executor: inspectionExecutor } = createEntityExecutor("inspection", inspectionEntity);
|
|
62
65
|
|
|
66
|
+
type AuditPayload = { assetName: string; from: string; to: string };
|
|
67
|
+
type ExecutorUser = Parameters<typeof inspectionExecutor.list>[1];
|
|
68
|
+
type ExecutorDb = Parameters<typeof inspectionExecutor.list>[2];
|
|
69
|
+
|
|
70
|
+
async function renderAudit(payload: AuditPayload, user: ExecutorUser, db: ExecutorDb) {
|
|
71
|
+
const { assetName, from, to } = payload;
|
|
72
|
+
const inspections = await inspectionExecutor.list(
|
|
73
|
+
{
|
|
74
|
+
filters: [
|
|
75
|
+
{ field: "assetName", op: "eq", value: assetName },
|
|
76
|
+
{ field: "inspectedOn", op: "gte", value: from },
|
|
77
|
+
{ field: "inspectedOn", op: "lte", value: to },
|
|
78
|
+
],
|
|
79
|
+
sort: "inspectedOn",
|
|
80
|
+
sortDirection: "asc",
|
|
81
|
+
limit: AUDIT_ROW_LIMIT + 1,
|
|
82
|
+
},
|
|
83
|
+
user,
|
|
84
|
+
db,
|
|
85
|
+
);
|
|
86
|
+
// A protocol that silently drops inspections would look complete.
|
|
87
|
+
if (inspections.rows.length > AUDIT_ROW_LIMIT) {
|
|
88
|
+
return writeFailure(
|
|
89
|
+
new UnprocessableError("audit_period_too_large", {
|
|
90
|
+
details: { limit: AUDIT_ROW_LIMIT },
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { pdfBytes } = await renderTablePdf({
|
|
96
|
+
title: `Prüfprotokoll ${assetName}`,
|
|
97
|
+
meta: [
|
|
98
|
+
{ label: "Zeitraum", value: `${formatGermanDate(from)} bis ${formatGermanDate(to)}` },
|
|
99
|
+
{ label: "Prüfungen", value: String(inspections.rows.length) },
|
|
100
|
+
],
|
|
101
|
+
columns: [{ header: "Datum" }, { header: "Ergebnis" }, { header: "Prüfer", weight: 2 }],
|
|
102
|
+
rows: inspections.rows.map((row) => [
|
|
103
|
+
formatGermanDate(String(row["inspectedOn"])),
|
|
104
|
+
RESULT_LABELS[z.enum(INSPECTION_RESULTS).parse(row["result"])],
|
|
105
|
+
String(row["inspector"] ?? ""),
|
|
106
|
+
]),
|
|
107
|
+
emptyText: "Keine Prüfungen im Zeitraum",
|
|
108
|
+
footerText: `Prüfbericht ${assetName}`,
|
|
109
|
+
pageLabel: "Seite {page} von {pages}",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
isSuccess: true as const,
|
|
114
|
+
data: {
|
|
115
|
+
fileName: `audit-${toFileNameSegment(assetName)}.pdf`,
|
|
116
|
+
mimeType: "application/pdf",
|
|
117
|
+
data: pdfBytes,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
63
122
|
r.writeHandler({
|
|
64
123
|
name: "inspection:create",
|
|
65
124
|
schema: z.object({
|
|
@@ -77,56 +136,20 @@ export const pdfAuditExportFeature = defineFeature("pdf-audit-export", (r) => {
|
|
|
77
136
|
name: "export-audit",
|
|
78
137
|
schema: z.object({ assetName: z.string().min(1), from: z.iso.date(), to: z.iso.date() }),
|
|
79
138
|
access: access.access,
|
|
139
|
+
// Nested in another command there is no phase of its own; the handler then does this work itself.
|
|
140
|
+
beforeTransactionWhenNested: "skip",
|
|
80
141
|
beforeTransaction: async (event, ctx) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
filters: [
|
|
85
|
-
{ field: "assetName", op: "eq", value: assetName },
|
|
86
|
-
{ field: "inspectedOn", op: "gte", value: from },
|
|
87
|
-
{ field: "inspectedOn", op: "lte", value: to },
|
|
88
|
-
],
|
|
89
|
-
sort: "inspectedOn",
|
|
90
|
-
sortDirection: "asc",
|
|
91
|
-
limit: AUDIT_ROW_LIMIT + 1,
|
|
92
|
-
},
|
|
93
|
-
event.user,
|
|
94
|
-
ctx.db,
|
|
95
|
-
);
|
|
96
|
-
// A protocol that silently drops inspections would look complete.
|
|
97
|
-
if (inspections.rows.length > AUDIT_ROW_LIMIT) {
|
|
98
|
-
return writeFailure(
|
|
99
|
-
new UnprocessableError("audit_period_too_large", {
|
|
100
|
-
details: { limit: AUDIT_ROW_LIMIT },
|
|
101
|
-
}),
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const { pdfBytes } = await renderTablePdf({
|
|
106
|
-
title: `Prüfprotokoll ${assetName}`,
|
|
107
|
-
meta: [
|
|
108
|
-
{ label: "Zeitraum", value: `${formatGermanDate(from)} bis ${formatGermanDate(to)}` },
|
|
109
|
-
{ label: "Prüfungen", value: String(inspections.rows.length) },
|
|
110
|
-
],
|
|
111
|
-
columns: [{ header: "Datum" }, { header: "Ergebnis" }, { header: "Prüfer", weight: 2 }],
|
|
112
|
-
rows: inspections.rows.map((row) => [
|
|
113
|
-
formatGermanDate(String(row["inspectedOn"])),
|
|
114
|
-
RESULT_LABELS[z.enum(INSPECTION_RESULTS).parse(row["result"])],
|
|
115
|
-
String(row["inspector"] ?? ""),
|
|
116
|
-
]),
|
|
117
|
-
emptyText: "Keine Prüfungen im Zeitraum",
|
|
118
|
-
footerText: `Prüfbericht ${assetName}`,
|
|
119
|
-
pageLabel: "Seite {page} von {pages}",
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
return uploadFileBytesBeforeTransaction(ctx, {
|
|
123
|
-
fileName: `audit-${toFileNameSegment(assetName)}.pdf`,
|
|
124
|
-
mimeType: "application/pdf",
|
|
125
|
-
data: pdfBytes,
|
|
126
|
-
});
|
|
142
|
+
const rendered = await renderAudit(event.payload, event.user, ctx.db);
|
|
143
|
+
if (!rendered.isSuccess) return rendered;
|
|
144
|
+
return uploadFileBytesBeforeTransaction(ctx, rendered.data);
|
|
127
145
|
},
|
|
128
146
|
handler: async (event, ctx) => {
|
|
129
|
-
const created =
|
|
147
|
+
const created =
|
|
148
|
+
event.prepared !== undefined
|
|
149
|
+
? await createFileRefFromUploadedBytes(ctx, event.prepared)
|
|
150
|
+
: await renderAudit(event.payload, event.user, ctx.db).then((rendered) =>
|
|
151
|
+
rendered.isSuccess ? createFileRefFromBytes(ctx, rendered.data) : rendered,
|
|
152
|
+
);
|
|
130
153
|
if (!created.isSuccess) return created;
|
|
131
154
|
return { isSuccess: true as const, data: { fileRefId: created.data.id } };
|
|
132
155
|
},
|