@anthusai/papyrus 2.0.0-next.8 → 2.0.0-next.80
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/amplify/data/function-handler.js +20 -0
- package/amplify/data/function-handler.ts +40 -0
- package/amplify/data/resource.js +151 -61
- package/amplify/data/resource.ts +157 -61
- package/amplify/data/schema.js +2096 -1980
- package/amplify/data/schema.ts +198 -69
- package/amplify/functions/assignment-action/data-access.js +14 -15
- package/amplify/functions/assignment-action/data-access.ts +13 -14
- package/amplify/functions/assignment-action/handler.ts +1 -1
- package/amplify/functions/automation-view/handler.py +17 -0
- package/amplify/functions/automation-view/resource.js +24 -0
- package/amplify/functions/automation-view/resource.ts +34 -0
- package/amplify/functions/automation-view/schema.js +26 -0
- package/amplify/functions/automation-view/schema.ts +28 -0
- package/amplify/functions/automation-view/wire.js +56 -0
- package/amplify/functions/automation-view/wire.ts +76 -0
- package/amplify/functions/content-actions/handler.py +61 -2
- package/amplify/functions/content-actions/requirements.txt +1 -0
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +3 -3
- package/amplify/functions/editorial-checks/requirements.txt +1 -0
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +7 -7
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +7 -7
- package/amplify/functions/editorial-checks/schema.js +122 -0
- package/amplify/{plugins → functions}/editorial-checks/schema.ts +40 -12
- package/amplify/functions/editorial-checks/wire.js +53 -0
- package/amplify/functions/editorial-checks/wire.ts +61 -0
- package/amplify/functions/monitoring-view/handler.py +11 -0
- package/amplify/functions/monitoring-view/resource.js +24 -0
- package/amplify/functions/monitoring-view/resource.ts +34 -0
- package/amplify/functions/monitoring-view/schema.js +17 -0
- package/amplify/functions/monitoring-view/schema.ts +19 -0
- package/amplify/functions/monitoring-view/wire.js +37 -0
- package/amplify/functions/monitoring-view/wire.ts +47 -0
- package/amplify/functions/public-forms/core.js +247 -0
- package/amplify/functions/public-forms/core.ts +310 -0
- package/amplify/functions/public-forms/handler.ts +41 -0
- package/amplify/functions/public-forms/origins.js +35 -0
- package/amplify/functions/public-forms/origins.ts +43 -0
- package/amplify/functions/public-forms/resource.js +9 -0
- package/amplify/functions/public-forms/resource.ts +10 -0
- package/amplify/functions/public-forms/wire.js +25 -0
- package/amplify/functions/public-forms/wire.ts +32 -0
- package/amplify/functions/shared/editorial-context.ts +63 -4
- package/amplify/functions/shared/python-bundle.js +20 -14
- package/amplify/functions/shared/python-bundle.ts +25 -18
- package/amplify/plugins/newsletter/config.js +245 -0
- package/amplify/plugins/newsletter/config.ts +305 -0
- package/amplify/plugins/newsletter/function/handler.py +6 -0
- package/amplify/plugins/newsletter/function/requirements.txt +5 -0
- package/amplify/plugins/newsletter/ops/handler.py +12 -0
- package/amplify/plugins/newsletter/ops/resource.js +27 -0
- package/amplify/plugins/newsletter/ops/resource.ts +37 -0
- package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
- package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
- package/amplify/plugins/newsletter/schema.js +57 -0
- package/amplify/plugins/newsletter/schema.ts +62 -0
- package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
- package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
- package/amplify/plugins/newsletter/sending.js +140 -0
- package/amplify/plugins/newsletter/sending.ts +186 -0
- package/amplify/plugins/reader-steering/config.js +182 -0
- package/amplify/plugins/reader-steering/config.ts +290 -0
- package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
- package/amplify/plugins/reader-steering/function/handler.py +1554 -0
- package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
- package/amplify/plugins/reader-steering/function/moderation.py +316 -0
- package/amplify/plugins/reader-steering/function/proposals.py +255 -0
- package/amplify/plugins/reader-steering/function/resource.js +27 -0
- package/amplify/plugins/reader-steering/function/resource.ts +35 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.js +186 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +212 -0
- package/amplify/plugins/reader-steering/schema.js +205 -0
- package/amplify/plugins/reader-steering/schema.ts +207 -0
- package/amplify/plugins/registry.js +4 -2
- package/amplify/plugins/registry.ts +4 -2
- package/amplify/plugins/signup/schema.js +2 -1
- package/amplify/plugins/signup/schema.ts +2 -1
- package/amplify/plugins/types.ts +5 -0
- package/amplify/plugins/wire.js +1 -0
- package/amplify/plugins/wire.ts +1 -0
- package/amplify/publication/projection-lease.js +30 -0
- package/amplify/publication/projection-lease.ts +39 -0
- package/amplify/publication/redirect-and-index.js +105 -0
- package/amplify/publication/redirect-rules.js +73 -0
- package/amplify/publication/redirect-rules.ts +93 -0
- package/amplify/publication/s3-projection.js +7 -1
- package/amplify/publication/s3-projection.ts +9 -1
- package/amplify/site-backend-config.ts +18 -1
- package/amplify/site-backend-names.js +4 -2
- package/amplify/site-backend-names.ts +4 -2
- package/amplify/site-backend.js +41 -2
- package/amplify/site-backend.ts +46 -2
- package/app/api/auth/prospectus/sign-in/route.ts +4 -0
- package/app/api/newsletter/confirm/route.ts +13 -0
- package/app/api/newsletter/preferences/route.ts +13 -0
- package/app/api/newsletter/state/route.ts +9 -0
- package/app/api/newsletter/subscribe/route.ts +9 -0
- package/app/api/newsletter/unsubscribe/route.ts +13 -0
- package/app/api/newsroom/curation/context/route.ts +9 -0
- package/app/api/newsroom/curation/priming/route.ts +9 -0
- package/app/api/newsroom/curation/publication/route.ts +9 -0
- package/app/api/reader/comment/delete/route.ts +9 -0
- package/app/api/reader/comment/edit/route.ts +9 -0
- package/app/api/reader/comment/route.ts +9 -0
- package/app/api/reader/flag/route.ts +9 -0
- package/app/api/reader/handle/route.ts +9 -0
- package/app/api/reader/me/route.ts +9 -0
- package/app/api/reader/rating/route.ts +9 -0
- package/app/api/reader/report/route.ts +9 -0
- package/app/api/reader/retired/route.ts +9 -0
- package/app/api/reader/submit/route.ts +9 -0
- package/app/api/reader/vote/route.ts +9 -0
- package/bin/papyrus-infra.mjs +20 -1
- package/components/automation-panel.tsx +49 -0
- package/components/automation-run-now.tsx +97 -0
- package/components/automation-view.tsx +192 -0
- package/components/editorial-checks-admin-panel.tsx +36 -11
- package/components/moderation-panel.tsx +248 -0
- package/components/monitoring-overview-view.tsx +241 -0
- package/components/monitoring-panel.tsx +40 -0
- package/components/news-desk-client-provider.tsx +18 -18
- package/components/news-desk-taxonomy-client.ts +28 -8
- package/components/newsletter-panel.tsx +474 -0
- package/components/newsletter-sections.tsx +163 -0
- package/components/newsroom-access-gate.tsx +15 -1
- package/components/newsroom-article-editor.tsx +6 -6
- package/components/newsroom-articles-view.tsx +6 -6
- package/components/newsroom-board-view.tsx +8 -3
- package/components/newsroom-curation-bootstrap.tsx +45 -0
- package/components/newsroom-ops-overview.tsx +22 -92
- package/components/newsroom-ops-shell.tsx +2 -2
- package/components/newsroom-organizer.css +33 -0
- package/components/newsroom-overview-skeleton.tsx +25 -0
- package/components/newsroom-references-view.tsx +192 -49
- package/components/notification-center.tsx +273 -0
- package/components/publication-cyclotron-panel.tsx +15 -10
- package/components/reader-auth-state.ts +1 -1
- package/components/reference-add-entry.tsx +75 -0
- package/components/reference-curation-controls.tsx +31 -22
- package/components/reference-cyclotron-priming.tsx +58 -19
- package/components/reference-cyclotron-suggestion.tsx +10 -6
- package/components/reference-priming.tsx +14 -10
- package/components/reference-relevance-review.tsx +3 -3
- package/components/reference-section-suggestion.tsx +1 -1
- package/components/topic-steering-workspace.tsx +223 -144
- package/components/use-editorial-checks.ts +12 -10
- package/infra/amplify-app-shell.js +94 -16
- package/infra/amplify-app-shell.ts +106 -18
- package/infra/cloud-email-config.js +32 -0
- package/infra/cloud-email-config.ts +39 -0
- package/infra/cloud-email.js +57 -0
- package/infra/cloud-email.test.js +69 -0
- package/infra/cloud-email.test.ts +66 -0
- package/infra/cloud-email.ts +60 -0
- package/infra/index.js +1 -0
- package/infra/secure-string-parameter-handler.js +41 -0
- package/infra/secure-string-parameter-handler.ts +41 -0
- package/infra/secure-string-parameter.js +79 -0
- package/infra/secure-string-parameter.ts +91 -0
- package/infra/site-config.js +88 -6
- package/infra/site-config.ts +134 -6
- package/lib/automation-client.ts +54 -0
- package/lib/automation-fixture.ts +93 -0
- package/lib/automation-overview.ts +232 -0
- package/lib/category-repository.ts +11 -9
- package/lib/curation-routes.ts +345 -0
- package/lib/cyclotron-suggestions.ts +4 -4
- package/lib/editorial-checks-backend.ts +1 -1
- package/lib/editorial-checks-client.ts +19 -1
- package/lib/editorial-checks-demo.ts +2 -2
- package/lib/editorial-checks-settings.ts +3 -0
- package/lib/editorial-checks.ts +8 -11
- package/lib/editorial-preview.ts +5 -5
- package/lib/monitoring-client.ts +29 -0
- package/lib/monitoring-overview-fixture.ts +51 -0
- package/lib/monitoring-overview.ts +145 -0
- package/lib/news-desk-session.ts +12 -21
- package/lib/newsletter-api.ts +418 -0
- package/lib/newsletter-client.ts +46 -0
- package/lib/newsletter-fixture.ts +184 -0
- package/lib/newsletter-model.ts +202 -0
- package/lib/newsroom-organizer-nav.ts +2 -1
- package/lib/prospectus-session.ts +15 -14
- package/lib/prospectus-sign-in-routes.ts +84 -19
- package/lib/prospectus-site-identity.ts +24 -1
- package/lib/reader-api.ts +145 -0
- package/lib/reader-moderation.ts +220 -0
- package/lib/reader-retirement-route.ts +89 -0
- package/lib/reader-subject.md +29 -0
- package/lib/reader-subject.ts +53 -0
- package/lib/reference-review-details.ts +71 -0
- package/lib/reference-review-request.ts +14 -0
- package/lib/relevance-decisions.ts +42 -10
- package/lib/relevance-status.ts +2 -2
- package/lib/review-client.ts +99 -0
- package/lib/review-fixture.ts +45 -0
- package/lib/review-requests.ts +238 -0
- package/lib/semantic-graph.ts +2 -0
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +6 -3
- package/lib/site-origin.ts +67 -0
- package/lib/use-reference-review-data.ts +44 -0
- package/middleware.ts +14 -3
- package/package.json +56 -3
- package/routes.manifest.json +215 -2
- package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
- package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -71
- package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -87
- package/amplify/plugins/editorial-checks/schema.js +0 -94
package/infra/site-config.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseCloudEmail, type CloudEmailConfig } from "./cloud-email-config";
|
|
2
|
+
|
|
1
3
|
export type CmsRedirect = {
|
|
2
4
|
source: string;
|
|
3
5
|
status: 301 | 302;
|
|
@@ -32,6 +34,8 @@ export type CmsConfig = {
|
|
|
32
34
|
appName?: string;
|
|
33
35
|
domainName?: string;
|
|
34
36
|
domainPrefix?: string;
|
|
37
|
+
/** Public https origin of a site whose host is served by its own edge (no Amplify domain association). */
|
|
38
|
+
siteOrigin?: string;
|
|
35
39
|
staging?: boolean;
|
|
36
40
|
stagingPreview?: StagingPreviewConfig;
|
|
37
41
|
cognitoDomainPrefix: string;
|
|
@@ -42,6 +46,12 @@ export type CmsConfig = {
|
|
|
42
46
|
stagingDomainPrefix?: string;
|
|
43
47
|
redirects?: CmsRedirect[];
|
|
44
48
|
prospectusAccountsApiStageArn?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The site is a public-reader site that polls Prospectus for retired reader subjects. The template then generates a
|
|
51
|
+
* shared secret that the scheduled snapshot run presents to the CMS route `/api/reader/retired` (requires
|
|
52
|
+
* `prospectusAccountsApiStageArn`; docs/reader-steering.md).
|
|
53
|
+
*/
|
|
54
|
+
readerRetirementPoll?: boolean;
|
|
45
55
|
deployStackInBuild?: boolean;
|
|
46
56
|
};
|
|
47
57
|
|
|
@@ -53,7 +63,10 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
53
63
|
frontend: "pretext" | "markus-static";
|
|
54
64
|
hostedZoneId?: string;
|
|
55
65
|
stackName?: string;
|
|
66
|
+
aws?: SiteAwsConfig;
|
|
56
67
|
cms?: CmsConfig;
|
|
68
|
+
/** Required for cloud CMS sites; independent of newsletter feature enablement. */
|
|
69
|
+
email?: CloudEmailConfig;
|
|
57
70
|
reader?: {
|
|
58
71
|
appName?: string;
|
|
59
72
|
domainName?: string;
|
|
@@ -69,20 +82,67 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
69
82
|
};
|
|
70
83
|
papyrusVersion: string;
|
|
71
84
|
storagePreviewPrefix?: string;
|
|
85
|
+
/**
|
|
86
|
+
* The scheduled cloud knowledge run for this publication. The app-shell stack ignores it; it is validated by
|
|
87
|
+
* infra/scheduled-jobs/lib/knowledge-run-config.ts, which owns the schema.
|
|
88
|
+
*/
|
|
89
|
+
knowledgeRun?: unknown;
|
|
90
|
+
/**
|
|
91
|
+
* Merge-to-main deploy from GitHub Actions through OIDC. When true (the default) the stack creates the
|
|
92
|
+
* `<siteId>-github-deploy` role and turns off Amplify's own auto-build on the production branches, so the
|
|
93
|
+
* public repository's workflow is the only trigger. Set false for a site whose repository is private:
|
|
94
|
+
* private repositories get no Actions minutes, so Amplify's own repository-connected auto-build stays on.
|
|
95
|
+
*/
|
|
96
|
+
autoDeploy: boolean;
|
|
72
97
|
github?: {
|
|
73
98
|
owner: string;
|
|
74
99
|
repo: string;
|
|
75
100
|
branches: string[];
|
|
76
101
|
ciCanDeployInfra: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* GitHub's numeric ids of the owner and the repository. Repositories with the immutable OIDC subject
|
|
104
|
+
* (`repo:<owner>@<ownerId>/<repo>@<repoId>:ref:...`, see `gh api repos/<owner>/<repo>/actions/oidc/customization/sub`)
|
|
105
|
+
* present that form instead of `repo:<owner>/<repo>:ref:...`; with both ids set, roles trust both forms.
|
|
106
|
+
*/
|
|
107
|
+
ownerId?: number;
|
|
108
|
+
repoId?: number;
|
|
77
109
|
};
|
|
78
110
|
};
|
|
79
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Where the site's AWS resources live. `bin/app-shell.ts` deploys the stack into exactly this account and
|
|
114
|
+
* region (see `resolveDeployEnvironment`), and `papyrus site google-secrets` asserts `accountId` with STS
|
|
115
|
+
* before it writes anything.
|
|
116
|
+
*/
|
|
117
|
+
export type SiteAwsConfig = {
|
|
118
|
+
profile?: string;
|
|
119
|
+
accountId?: string;
|
|
120
|
+
region?: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
80
123
|
export type CmsSiteConfig = AmplifyAppShellSiteConfig & { cms: CmsConfig };
|
|
81
124
|
|
|
82
125
|
export function hasCmsApp(config: AmplifyAppShellSiteConfig): config is CmsSiteConfig {
|
|
83
126
|
return config.cms !== undefined;
|
|
84
127
|
}
|
|
85
128
|
|
|
129
|
+
export function resolveGithubRepository(config: Pick<AmplifyAppShellSiteConfig, "repository" | "github">): { owner: string; repo: string } {
|
|
130
|
+
if (config.github) return { owner: config.github.owner, repo: config.github.repo };
|
|
131
|
+
const [owner, repo] = config.repository.replace("https://github.com/", "").split("/");
|
|
132
|
+
return { owner, repo };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The OIDC `sub` values a GitHub Actions run on `ref` of the site's repository can present. */
|
|
136
|
+
export function githubOidcSubjects(config: Pick<AmplifyAppShellSiteConfig, "repository" | "github">, ref: string): string[] {
|
|
137
|
+
const { owner, repo } = resolveGithubRepository(config);
|
|
138
|
+
const subjects = [`repo:${owner}/${repo}:ref:refs/heads/${ref}`];
|
|
139
|
+
const { ownerId, repoId } = config.github ?? {};
|
|
140
|
+
if (ownerId !== undefined && repoId !== undefined) {
|
|
141
|
+
subjects.push(`repo:${owner}@${ownerId}/${repo}@${repoId}:ref:refs/heads/${ref}`);
|
|
142
|
+
}
|
|
143
|
+
return subjects;
|
|
144
|
+
}
|
|
145
|
+
|
|
86
146
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
87
147
|
|
|
88
148
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
@@ -90,6 +150,7 @@ const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_
|
|
|
90
150
|
const SECRETS_MANAGER_SECRET_ARN_PATTERN = /^arn:aws:secretsmanager:[a-z0-9-]+:\d{12}:secret:[A-Za-z0-9/_+=.@-]+$/;
|
|
91
151
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
92
152
|
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
153
|
+
const SITE_ORIGIN_PATTERN = /^https:\/\/([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(:\d{1,5})?$/;
|
|
93
154
|
const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
94
155
|
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
95
156
|
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
@@ -100,7 +161,7 @@ const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
|
100
161
|
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
101
162
|
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
102
163
|
const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
|
|
103
|
-
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "
|
|
164
|
+
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_EMAIL_FROM_ADDRESS", "PAPYRUS_EMAIL_IDENTITY_ARN", "PAPYRUS_EMAIL_CONFIGURATION_SET", "PAPYRUS_EMAIL_EVENT_TOPIC_NAME", "PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_SITE_PRODUCT", "PAPYRUS_READER_POLL_SECRET_PARAMETER", "PAPYRUS_SITE_ORIGIN"];
|
|
104
165
|
|
|
105
166
|
/** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
|
|
106
167
|
export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
|
|
@@ -127,6 +188,14 @@ function originOfRedirectUrl(value: string): string {
|
|
|
127
188
|
}
|
|
128
189
|
}
|
|
129
190
|
|
|
191
|
+
export function resolveProspectusSessionSealingParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
192
|
+
return `/papyrus/${config.siteId}/prospectus-session-sealing`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function resolveReaderPollSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
196
|
+
return `/papyrus/${config.siteId}/reader-poll-secret`;
|
|
197
|
+
}
|
|
198
|
+
|
|
130
199
|
export function resolveRevalidateSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
131
200
|
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
132
201
|
}
|
|
@@ -251,6 +320,10 @@ export function resolveCmsHostName(config: Pick<CmsSiteConfig, "cms">): string |
|
|
|
251
320
|
return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
|
|
252
321
|
}
|
|
253
322
|
|
|
323
|
+
export function resolveSiteOrigin(config: Pick<CmsSiteConfig, "cms">): string | undefined {
|
|
324
|
+
return config.cms.siteOrigin;
|
|
325
|
+
}
|
|
326
|
+
|
|
254
327
|
export function resolveStagingDomainName(config: Pick<CmsSiteConfig, "cms">): string | undefined {
|
|
255
328
|
if (!isStagingEnabled(config)) return undefined;
|
|
256
329
|
if (config.cms.domainPrefix) {
|
|
@@ -282,7 +355,7 @@ function parseStagingPreview(value: unknown): StagingPreviewConfig | undefined {
|
|
|
282
355
|
|
|
283
356
|
function parseCms(value: unknown): CmsConfig {
|
|
284
357
|
const cmsRecord = requireObject(value, "cms");
|
|
285
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
|
|
358
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "readerRetirementPoll", "deployStackInBuild"], "cms");
|
|
286
359
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
287
360
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
288
361
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -293,6 +366,12 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
293
366
|
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
294
367
|
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
295
368
|
}
|
|
369
|
+
if (cmsRecord.readerRetirementPoll !== undefined && typeof cmsRecord.readerRetirementPoll !== "boolean") {
|
|
370
|
+
fail("cms.readerRetirementPoll", `must be a boolean, got ${JSON.stringify(cmsRecord.readerRetirementPoll)}`);
|
|
371
|
+
}
|
|
372
|
+
if (cmsRecord.readerRetirementPoll === true && cmsRecord.prospectusAccountsApiStageArn === undefined) {
|
|
373
|
+
fail("cms.readerRetirementPoll", "requires cms.prospectusAccountsApiStageArn: the poll calls Prospectus as the site");
|
|
374
|
+
}
|
|
296
375
|
if (cmsRecord.deployStackInBuild !== undefined && typeof cmsRecord.deployStackInBuild !== "boolean") {
|
|
297
376
|
fail("cms.deployStackInBuild", `must be a boolean, got ${JSON.stringify(cmsRecord.deployStackInBuild)}`);
|
|
298
377
|
}
|
|
@@ -300,6 +379,7 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
300
379
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
301
380
|
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
302
381
|
domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
382
|
+
siteOrigin: optionalString(cmsRecord.siteOrigin, "cms.siteOrigin", SITE_ORIGIN_PATTERN, "https origin with no path, query or trailing slash, such as https://shipped.anth.us"),
|
|
303
383
|
staging: cmsRecord.staging as boolean | undefined,
|
|
304
384
|
cognitoDomainPrefix: requireString(
|
|
305
385
|
cmsRecord.cognitoDomainPrefix,
|
|
@@ -314,6 +394,7 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
314
394
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
315
395
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
316
396
|
deployStackInBuild: cmsRecord.deployStackInBuild as boolean | undefined,
|
|
397
|
+
readerRetirementPoll: cmsRecord.readerRetirementPoll as boolean | undefined,
|
|
317
398
|
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
318
399
|
prospectusAccountsApiStageArn: optionalString(
|
|
319
400
|
cmsRecord.prospectusAccountsApiStageArn,
|
|
@@ -329,10 +410,13 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
329
410
|
}
|
|
330
411
|
for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
|
|
331
412
|
if (key in cms.environment) {
|
|
332
|
-
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn");
|
|
413
|
+
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn and the public site origin comes from the site host");
|
|
333
414
|
}
|
|
334
415
|
}
|
|
335
416
|
|
|
417
|
+
if (cms.siteOrigin && cms.domainName) {
|
|
418
|
+
fail("cms.siteOrigin", "is for sites whose host is served by their own edge; remove it when cms.domainName is set");
|
|
419
|
+
}
|
|
336
420
|
if (cms.staging === false && cms.stagingDomainName) {
|
|
337
421
|
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
338
422
|
}
|
|
@@ -395,11 +479,22 @@ function validateOauthRedirectUrls(config: CmsSiteConfig): void {
|
|
|
395
479
|
}
|
|
396
480
|
}
|
|
397
481
|
|
|
482
|
+
function parseAws(value: unknown): SiteAwsConfig | undefined {
|
|
483
|
+
if (value === undefined) return undefined;
|
|
484
|
+
const record = requireObject(value, "aws");
|
|
485
|
+
rejectUnknownKeys(record, ["profile", "accountId", "region"], "aws");
|
|
486
|
+
return {
|
|
487
|
+
profile: optionalString(record.profile, "aws.profile", /^[A-Za-z0-9_.-]+$/, "AWS CLI profile name"),
|
|
488
|
+
accountId: optionalString(record.accountId, "aws.accountId", /^\d{12}$/, "12-digit account id"),
|
|
489
|
+
region: optionalString(record.region, "aws.region", /^[a-z]{2}(-[a-z]+)+-\d$/, "AWS region such as us-east-1"),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
398
493
|
export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
399
494
|
const record = requireObject(raw, "site");
|
|
400
495
|
rejectUnknownKeys(
|
|
401
496
|
record,
|
|
402
|
-
["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
497
|
+
["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "aws", "cms", "email", "reader", "papyrusVersion", "storagePreviewPrefix", "knowledgeRun", "autoDeploy", "github"],
|
|
403
498
|
"site",
|
|
404
499
|
);
|
|
405
500
|
|
|
@@ -415,6 +510,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
415
510
|
}
|
|
416
511
|
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
417
512
|
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
513
|
+
const aws = parseAws(record.aws);
|
|
418
514
|
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
419
515
|
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
420
516
|
|
|
@@ -452,11 +548,14 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
452
548
|
fail("cms.stagingPreview", "must not be set when cms.staging is false");
|
|
453
549
|
}
|
|
454
550
|
|
|
551
|
+
const autoDeploy = record.autoDeploy ?? true;
|
|
552
|
+
if (typeof autoDeploy !== "boolean") fail("autoDeploy", "must be a boolean");
|
|
553
|
+
|
|
455
554
|
let github: AmplifyAppShellSiteConfig["github"];
|
|
456
555
|
if (record.github !== undefined) {
|
|
457
556
|
if (cms === undefined) fail("github", "requires cms (the CI role is scoped to the CMS app)");
|
|
458
557
|
const githubRecord = requireObject(record.github, "github");
|
|
459
|
-
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
558
|
+
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra", "ownerId", "repoId"], "github");
|
|
460
559
|
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
461
560
|
const repo = requireString(githubRecord.repo, "github.repo", GITHUB_REPO_PATTERN);
|
|
462
561
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
@@ -476,7 +575,15 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
476
575
|
}
|
|
477
576
|
const ciCanDeployInfra = githubRecord.ciCanDeployInfra ?? false;
|
|
478
577
|
if (typeof ciCanDeployInfra !== "boolean") fail("github.ciCanDeployInfra", "must be a boolean");
|
|
479
|
-
|
|
578
|
+
const optionalId = (value: unknown, name: string): number | undefined => {
|
|
579
|
+
if (value === undefined) return undefined;
|
|
580
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) fail(name, "must be a positive integer (GitHub numeric id)");
|
|
581
|
+
return value as number;
|
|
582
|
+
};
|
|
583
|
+
const ownerId = optionalId(githubRecord.ownerId, "github.ownerId");
|
|
584
|
+
const repoId = optionalId(githubRecord.repoId, "github.repoId");
|
|
585
|
+
if ((ownerId === undefined) !== (repoId === undefined)) fail("github", "ownerId and repoId must be set together");
|
|
586
|
+
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean, ...(ownerId !== undefined ? { ownerId, repoId } : {}) };
|
|
480
587
|
}
|
|
481
588
|
|
|
482
589
|
const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
|
|
@@ -495,14 +602,35 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
495
602
|
frontend,
|
|
496
603
|
hostedZoneId,
|
|
497
604
|
stackName,
|
|
605
|
+
aws,
|
|
498
606
|
cms,
|
|
607
|
+
email: parseCloudEmail(record.email, cms !== undefined, siteId),
|
|
499
608
|
reader,
|
|
500
609
|
papyrusVersion,
|
|
501
610
|
storagePreviewPrefix,
|
|
611
|
+
autoDeploy: autoDeploy as boolean,
|
|
502
612
|
github,
|
|
613
|
+
...(record.knowledgeRun !== undefined ? { knowledgeRun: record.knowledgeRun } : {}),
|
|
503
614
|
};
|
|
504
615
|
|
|
505
616
|
if (hasCmsApp(config)) validateOauthRedirectUrls(config);
|
|
506
617
|
|
|
507
618
|
return config;
|
|
508
619
|
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* The CDK environment for a site's app-shell stack, taken only from the site's own `aws` block. There is
|
|
623
|
+
* deliberately no default account: a deploy that guesses an account can land in, or fail against, the wrong
|
|
624
|
+
* one.
|
|
625
|
+
*/
|
|
626
|
+
export function resolveDeployEnvironment(config: Pick<AmplifyAppShellSiteConfig, "siteId" | "aws">): { account: string; region: string } {
|
|
627
|
+
const account = config.aws?.accountId;
|
|
628
|
+
const region = config.aws?.region;
|
|
629
|
+
const missing = [account ? "" : "aws.accountId", region ? "" : "aws.region"].filter(Boolean);
|
|
630
|
+
if (!account || !region) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
`site "${config.siteId}" does not say where it deploys: add ${missing.join(" and ")} to its site.json, e.g. "aws": { "accountId": "<12-digit account>", "region": "us-east-1" }. No account is assumed.`,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
return { account, region };
|
|
636
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { generateClient } from "aws-amplify/data";
|
|
4
|
+
import type { AutomationResult, RunNowResult } from "./automation-overview";
|
|
5
|
+
|
|
6
|
+
type GraphQLResult = { data?: { automationOverview?: unknown }; errors?: { message?: string; errorType?: string }[] };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reads the automations through the site's own authenticated API (user pool, editor or admin).
|
|
10
|
+
* The browser holds no table name, key or secret: the function behind the query does the reads.
|
|
11
|
+
*/
|
|
12
|
+
export async function loadAutomationOverview(): Promise<AutomationResult> {
|
|
13
|
+
const client = generateClient({ authMode: "userPool" });
|
|
14
|
+
let result: GraphQLResult;
|
|
15
|
+
try {
|
|
16
|
+
result = (await client.graphql({
|
|
17
|
+
query: `query AutomationOverview($automationId: String) { automationOverview(automationId: $automationId) }`,
|
|
18
|
+
variables: {},
|
|
19
|
+
})) as GraphQLResult;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
// Amplify throws the response when AppSync returns errors.
|
|
22
|
+
result = (error && typeof error === "object" && "errors" in error ? error : { errors: [{ message: "failed" }] }) as GraphQLResult;
|
|
23
|
+
}
|
|
24
|
+
const refused = result.errors?.some((entry) => /unauthorized|not authorized|forbidden/i.test(`${entry.errorType ?? ""} ${entry.message ?? ""}`));
|
|
25
|
+
if (refused) return { error: "forbidden" };
|
|
26
|
+
if (result.errors?.length || result.data?.automationOverview === undefined || result.data.automationOverview === null) return { state: "unavailable" };
|
|
27
|
+
const raw = result.data.automationOverview;
|
|
28
|
+
return (typeof raw === "string" ? JSON.parse(raw) : raw) as AutomationResult;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type RunGraphQLResult = { data?: { runAutomation?: unknown }; errors?: { message?: string; errorType?: string }[] };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Starts a run of the automation's procedure now, as the signed-in editor or admin. The function behind the
|
|
35
|
+
* mutation checks the groups again, refuses a disabled automation or one at its limit, and writes the row that
|
|
36
|
+
* links the run to the automation's history. `input` is sent as JSON text; null uses the record's own input.
|
|
37
|
+
*/
|
|
38
|
+
export async function runAutomationNow(automationId: string, input: Record<string, unknown> | null): Promise<RunNowResult> {
|
|
39
|
+
const client = generateClient({ authMode: "userPool" });
|
|
40
|
+
let result: RunGraphQLResult;
|
|
41
|
+
try {
|
|
42
|
+
result = (await client.graphql({
|
|
43
|
+
query: `mutation RunAutomation($automationId: String!, $input: AWSJSON) { runAutomation(automationId: $automationId, input: $input) }`,
|
|
44
|
+
variables: { automationId, input: input === null ? null : JSON.stringify(input) },
|
|
45
|
+
})) as RunGraphQLResult;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
result = (error && typeof error === "object" && "errors" in error ? error : { errors: [{ message: "failed" }] }) as RunGraphQLResult;
|
|
48
|
+
}
|
|
49
|
+
const refused = result.errors?.some((entry) => /unauthorized|not authorized|forbidden/i.test(`${entry.errorType ?? ""} ${entry.message ?? ""}`));
|
|
50
|
+
if (refused) return { error: "forbidden" };
|
|
51
|
+
if (result.errors?.length || result.data?.runAutomation === undefined || result.data.runAutomation === null) return { state: "unavailable" };
|
|
52
|
+
const raw = result.data.runAutomation;
|
|
53
|
+
return (typeof raw === "string" ? JSON.parse(raw) : raw) as RunNowResult;
|
|
54
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { AutomationOverview, AutomationRun } from "./automation-overview";
|
|
2
|
+
|
|
3
|
+
const NOT_RECORDED = "not recorded for this run";
|
|
4
|
+
const ESTIMATED = "estimated from the model usage reported by the run";
|
|
5
|
+
|
|
6
|
+
function run(fields: Partial<AutomationRun> & Pick<AutomationRun, "status" | "startedAt">): AutomationRun {
|
|
7
|
+
return {
|
|
8
|
+
origin: "monitor", requestedBy: null, reason: null, sourceId: null, referenceId: null, triggerStatus: fields.status, runId: null,
|
|
9
|
+
finishedAt: null, durationSeconds: null, error: null, runRecord: "none", storyId: null, storyVersionId: null, storyTitle: null,
|
|
10
|
+
costUsd: null, costNote: NOT_RECORDED, budgetUsd: null, overBudget: false, usage: null, ...fields,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A realistic overview for tests and the `?demo=1` newsroom: one live monitor automation and one stored schedule. */
|
|
15
|
+
export const FIXTURE_AUTOMATION_OVERVIEW: AutomationOverview = {
|
|
16
|
+
state: "ready",
|
|
17
|
+
asOf: "2026-10-23T12:00:00Z",
|
|
18
|
+
monitoring: "ready",
|
|
19
|
+
automations: [
|
|
20
|
+
{
|
|
21
|
+
id: "example.ttab.ruling-analysis",
|
|
22
|
+
title: "Analyze new TTAB rulings",
|
|
23
|
+
description: "Drafts a story for each new Board decision.",
|
|
24
|
+
enabled: true,
|
|
25
|
+
enabledNote: "the router starts nothing for a disabled automation",
|
|
26
|
+
trigger: { kind: "monitor", config: { monitorId: "ttab", publicationId: "example" } },
|
|
27
|
+
supported: true,
|
|
28
|
+
supportNote: null,
|
|
29
|
+
procedureKey: "example.ttab.ruling-analysis",
|
|
30
|
+
inputTemplate: { proceeding: "{sourceIdPrefix}", opinion_url: "{documentUrl}" },
|
|
31
|
+
limits: { timeoutSeconds: 840, maxConcurrent: 2, budgetUsdPerRun: 0.05 },
|
|
32
|
+
limitsNote: "the timeout and the number at a time (when set) are enforced when a run starts; the budget is advisory and only flagged",
|
|
33
|
+
runNow: { available: true, reason: null, needsInput: true, inputNames: ["opinion_url", "proceeding"] },
|
|
34
|
+
createdAt: "2026-10-23T10:00:00Z",
|
|
35
|
+
updatedAt: "2026-10-23T10:00:00Z",
|
|
36
|
+
link: { state: "linked", text: "The router's trigger matches this record.", routerMode: "live", routerEnabled: true },
|
|
37
|
+
notice: null,
|
|
38
|
+
skippedAlreadyHadReference: 2,
|
|
39
|
+
lastRun: null,
|
|
40
|
+
runs: [
|
|
41
|
+
run({ sourceId: "91000001-EXA_26", referenceId: "ref-1", runId: "run-1", status: "completed", triggerStatus: "completed",
|
|
42
|
+
startedAt: "2026-10-23T11:30:00Z", finishedAt: "2026-10-23T11:33:10Z", durationSeconds: 190, runRecord: "found",
|
|
43
|
+
storyId: "story-1", storyVersionId: "version-1", storyTitle: "Board affirms a refusal for likelihood of confusion",
|
|
44
|
+
costUsd: 0.0123, costNote: ESTIMATED, budgetUsd: 0.05,
|
|
45
|
+
usage: { calls: 4, promptTokens: 61000, completionTokens: 5200, models: { "gpt-6-luna": 4 }, complete: true, unpricedModels: [] } }),
|
|
46
|
+
run({ sourceId: "91000002-EXA_26", referenceId: "ref-2", runId: "run-2", status: "failed", triggerStatus: "failed",
|
|
47
|
+
startedAt: "2026-10-23T05:30:00Z", finishedAt: "2026-10-23T05:31:00Z", durationSeconds: 60, runRecord: "found",
|
|
48
|
+
error: "the opinion could not be downloaded" }),
|
|
49
|
+
run({ sourceId: "91000003-EXA_26", referenceId: "ref-3", status: "started", startedAt: "2026-10-23T11:59:00Z" }),
|
|
50
|
+
run({ origin: "manual", requestedBy: "ryan@example.test", runId: "run-4", status: "completed", triggerStatus: "completed",
|
|
51
|
+
startedAt: "2026-10-23T09:00:00Z", finishedAt: "2026-10-23T09:05:00Z", durationSeconds: 300, runRecord: "found",
|
|
52
|
+
costUsd: 0.081, costNote: ESTIMATED, budgetUsd: 0.05, overBudget: true }),
|
|
53
|
+
run({ sourceId: "91000005-EXA_26", referenceId: "ref-5", status: "deferred", startedAt: "2026-10-23T08:00:00Z",
|
|
54
|
+
reason: "deferred: concurrency limit: 2 running, at most 2 at a time" }),
|
|
55
|
+
run({ sourceId: "91000006-EXA_26", referenceId: "ref-6", status: "skipped", startedAt: "2026-10-22T08:00:00Z",
|
|
56
|
+
reason: "automation disabled" }),
|
|
57
|
+
],
|
|
58
|
+
outsideRuns: [
|
|
59
|
+
{ runId: "run-9", status: "completed", startedAt: "2026-10-22T15:00:00Z", finishedAt: "2026-10-22T15:02:00Z", durationSeconds: 120,
|
|
60
|
+
error: null, requestedBy: "papyrus-cli", storyId: null, storyVersionId: null, storyTitle: null, costUsd: null,
|
|
61
|
+
costNote: NOT_RECORDED, overBudget: false },
|
|
62
|
+
],
|
|
63
|
+
outsideNotice: null,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "example.weekly-digest",
|
|
67
|
+
title: "Weekly digest",
|
|
68
|
+
description: null,
|
|
69
|
+
enabled: false,
|
|
70
|
+
enabledNote: "the router starts nothing for a disabled automation",
|
|
71
|
+
trigger: { kind: "schedule", config: { cron: "0 9 * * 1" } },
|
|
72
|
+
supported: false,
|
|
73
|
+
supportNote: "not yet supported",
|
|
74
|
+
procedureKey: "example.weekly-digest",
|
|
75
|
+
inputTemplate: {},
|
|
76
|
+
limits: { timeoutSeconds: 840 },
|
|
77
|
+
limitsNote: "the timeout and the number at a time (when set) are enforced when a run starts; the budget is advisory and only flagged",
|
|
78
|
+
runNow: { available: false, reason: "not yet supported", needsInput: false, inputNames: [] },
|
|
79
|
+
createdAt: "2026-10-23T10:05:00Z",
|
|
80
|
+
updatedAt: "2026-10-23T10:05:00Z",
|
|
81
|
+
link: { state: "not_applicable", text: "not yet supported" },
|
|
82
|
+
notice: "not yet supported",
|
|
83
|
+
skippedAlreadyHadReference: 0,
|
|
84
|
+
lastRun: null,
|
|
85
|
+
runs: [],
|
|
86
|
+
outsideRuns: [],
|
|
87
|
+
outsideNotice: null,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
// Newest first, as the history is served.
|
|
92
|
+
FIXTURE_AUTOMATION_OVERVIEW.automations[0].runs.sort((left, right) => String(right.startedAt).localeCompare(String(left.startedAt)));
|
|
93
|
+
FIXTURE_AUTOMATION_OVERVIEW.automations[0].lastRun = FIXTURE_AUTOMATION_OVERVIEW.automations[0].runs[0];
|