@anthusai/papyrus 2.0.0-next.11 → 2.0.0-next.110
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/auth/resource.js +16 -1
- package/amplify/auth/resource.ts +19 -1
- package/amplify/data/function-handler.js +20 -0
- package/amplify/data/function-handler.ts +40 -0
- package/amplify/data/resource.js +152 -62
- package/amplify/data/resource.ts +158 -62
- package/amplify/data/schema.js +2101 -1987
- package/amplify/data/schema.ts +197 -70
- package/amplify/functions/assignment-action/data-access.js +2 -0
- package/amplify/functions/assignment-action/data-access.ts +2 -0
- 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 +64 -3
- 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/functions/editorial-checks/handler.py +2 -2
- package/amplify/functions/editorial-checks/requirements.txt +1 -1
- package/amplify/functions/editorial-checks/resource.js +3 -3
- package/amplify/functions/editorial-checks/resource.ts +3 -3
- package/amplify/functions/editorial-checks/schema.js +38 -8
- package/amplify/functions/editorial-checks/schema.ts +38 -8
- package/amplify/functions/editorial-checks/wire.js +4 -0
- package/amplify/functions/editorial-checks/wire.ts +4 -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 +29 -0
- package/amplify/functions/monitoring-view/schema.ts +31 -0
- package/amplify/functions/monitoring-view/wire.js +64 -0
- package/amplify/functions/monitoring-view/wire.ts +81 -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 +1589 -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 +487 -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 +187 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +213 -0
- package/amplify/plugins/reader-steering/schema.js +252 -0
- package/amplify/plugins/reader-steering/schema.ts +254 -0
- package/amplify/plugins/registry.js +4 -0
- package/amplify/plugins/registry.ts +4 -0
- 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.js +19 -0
- package/amplify/site-backend-config.ts +45 -1
- package/amplify/site-backend-names.js +4 -2
- package/amplify/site-backend-names.ts +4 -2
- package/amplify/site-backend.js +42 -4
- package/amplify/site-backend.ts +45 -4
- package/app/.well-known/papyrus-cli.json/route.ts +9 -0
- 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/chat/[...path]/route.ts +17 -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/app/layout.tsx +5 -1
- 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 +195 -0
- package/components/editorial-checks-admin-panel.tsx +32 -17
- package/components/moderation-panel.tsx +248 -0
- package/components/monitoring-overview-view.tsx +422 -0
- package/components/monitoring-panel.tsx +60 -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 +16 -2
- package/components/newsroom-article-editor.tsx +9 -7
- package/components/newsroom-articles-view.tsx +6 -6
- package/components/newsroom-assignment-detail.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 +26 -96
- 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 +281 -0
- package/components/papyrus-console-conversation.css +14 -0
- package/components/papyrus-console-conversation.tsx +55 -0
- package/components/papyrus-console-shell.tsx +62 -420
- package/components/papyrus-private-chat-panel.tsx +134 -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 +230 -144
- package/infra/amplify-app-shell.js +96 -16
- package/infra/amplify-app-shell.ts +108 -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 +92 -6
- package/infra/site-config.ts +138 -6
- package/lib/automation-client.ts +54 -0
- package/lib/automation-fixture.ts +93 -0
- package/lib/automation-overview.ts +252 -0
- package/lib/category-repository.ts +14 -12
- package/lib/cli-login-discovery.ts +78 -0
- package/lib/console-web-context.ts +1 -1
- package/lib/curation-routes.ts +345 -0
- package/lib/cyclotron-suggestions.ts +4 -4
- package/lib/editorial-checks-client.ts +18 -0
- package/lib/editorial-checks-demo.ts +1 -1
- package/lib/editorial-checks.ts +8 -4
- package/lib/editorial-preview.ts +5 -5
- package/lib/monitoring-client.ts +83 -0
- package/lib/monitoring-overview-fixture.ts +51 -0
- package/lib/monitoring-overview.ts +191 -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-demo-dashboard.ts +1 -1
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +2 -1
- package/lib/papyrus-chat-admission.ts +65 -0
- package/lib/papyrus-hosted-chat.ts +217 -0
- package/lib/papyrus-private-chat-client.ts +81 -0
- package/lib/prospectus-session.ts +30 -14
- package/lib/prospectus-sign-in-routes.ts +106 -31
- package/lib/prospectus-site-identity.ts +46 -6
- 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 +120 -0
- package/lib/review-fixture.ts +45 -0
- package/lib/review-requests.ts +246 -0
- package/lib/semantic-graph.ts +2 -0
- package/lib/shared-chat-admission.ts +182 -0
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +6 -3
- package/lib/site-origin-value.ts +50 -0
- package/lib/site-origin.ts +8 -0
- package/lib/use-reference-review-data.ts +44 -0
- package/middleware.ts +14 -3
- package/package.json +67 -3
- package/routes.manifest.json +238 -2
- package/src/sync.mjs +11 -2
- package/src/with-papyrus.d.mts +1 -0
- package/src/with-papyrus.mjs +17 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleNewsletterUnsubscribe } from "../../../../lib/newsletter-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleNewsletterUnsubscribe(request);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function POST(request: NextRequest) {
|
|
12
|
+
return handleNewsletterUnsubscribe(request);
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handlePapyrusHostedChatRequest } from "../../../../../lib/papyrus-hosted-chat";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
type RouteContext = { params: Promise<{ path: string[] }> };
|
|
8
|
+
|
|
9
|
+
export async function GET(request: NextRequest, context: RouteContext) {
|
|
10
|
+
const { path } = await context.params;
|
|
11
|
+
return handlePapyrusHostedChatRequest(request, path);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function POST(request: NextRequest, context: RouteContext) {
|
|
15
|
+
const { path } = await context.params;
|
|
16
|
+
return handlePapyrusHostedChatRequest(request, path);
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleCurationRoute } from "../../../../../lib/curation-routes";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleCurationRoute(request, "context");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleCurationRoute } from "../../../../../lib/curation-routes";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleCurationRoute(request, "priming");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleCurationRoute } from "../../../../../lib/curation-routes";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleCurationRoute(request, "publication");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "comment-delete");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "comment-edit");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "comment");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "flag");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "handle");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "me");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "rating");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "report");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRetiredSubjects } from "../../../../lib/reader-retirement-route";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleReaderRetiredSubjects(request);
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "submit");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "vote");
|
|
9
|
+
}
|
package/app/layout.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "chatticus/chat.css";
|
|
2
|
+
import "../components/papyrus-console-conversation.css";
|
|
1
3
|
import type { Metadata } from "next";
|
|
2
4
|
import { Playfair_Display, Plus_Jakarta_Sans } from "next/font/google";
|
|
3
5
|
import Script from "next/script";
|
|
@@ -152,7 +154,9 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
|
|
|
152
154
|
{showStagingBanner() ? <StagingBanner /> : null}
|
|
153
155
|
<AmplifyClientProvider>
|
|
154
156
|
<TooltipProvider>
|
|
155
|
-
<PapyrusConsoleShell
|
|
157
|
+
<PapyrusConsoleShell privateChatEnabled={process.env.PAPYRUS_PRIVATE_CHAT_ENABLED === "true"}>
|
|
158
|
+
{children}
|
|
159
|
+
</PapyrusConsoleShell>
|
|
156
160
|
</TooltipProvider>
|
|
157
161
|
</AmplifyClientProvider>
|
|
158
162
|
</body>
|
package/bin/papyrus-infra.mjs
CHANGED
|
@@ -19,6 +19,7 @@ const USAGE = [
|
|
|
19
19
|
" papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]",
|
|
20
20
|
" papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader",
|
|
21
21
|
" papyrus-infra deploy-edge --stack NAME --template FILE [--adoption-template FILE --adopt-resources FILE] [--parameter KEY=VALUE]... [--delete-function NAME] [--only-branch main] [--only-app-id ID] [--region R]",
|
|
22
|
+
" papyrus-infra compile-edge-function --static-dir web [--out FILE]",
|
|
22
23
|
" papyrus-infra deploy --site infra/site.json [--only-branch main] [--only-app-id ID] [--out cdk.out] [--account A] [--region R]",
|
|
23
24
|
].join("\n");
|
|
24
25
|
|
|
@@ -36,11 +37,29 @@ const resolveEnvironmentOrExit = () => {
|
|
|
36
37
|
}
|
|
37
38
|
};
|
|
38
39
|
const command = args[0];
|
|
39
|
-
if (command !== "synth" && command !== "print-buildspec" && command !== "deploy" && command !== "deploy-edge") {
|
|
40
|
+
if (command !== "synth" && command !== "print-buildspec" && command !== "deploy" && command !== "deploy-edge" && command !== "compile-edge-function") {
|
|
40
41
|
console.error(USAGE);
|
|
41
42
|
process.exit(command ? 1 : 0);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
if (command === "compile-edge-function") {
|
|
46
|
+
// The viewer-request CloudFront Function with the site's projection.json rules compiled in: for an edge stack that is
|
|
47
|
+
// not deployed by the CMS backend, so that no site writes its own redirect function.
|
|
48
|
+
const staticDir = opt("static-dir");
|
|
49
|
+
if (!staticDir) {
|
|
50
|
+
console.error(USAGE);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const { compileRedirectFunctionSource, readEdgeConfig } = await import("../amplify/publication/redirect-rules.js");
|
|
54
|
+
const functionSource = fs.readFileSync(new URL("../amplify/publication/redirect-and-index.js", import.meta.url), "utf8");
|
|
55
|
+
const config = readEdgeConfig(staticDir);
|
|
56
|
+
const compiled = compileRedirectFunctionSource(functionSource, config.rules, { negotiateMarkdown: config.negotiateMarkdown, directoryTwins: config.directoryTwins });
|
|
57
|
+
const out = opt("out");
|
|
58
|
+
if (out) fs.writeFileSync(path.resolve(out), compiled);
|
|
59
|
+
else process.stdout.write(compiled);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
if (command === "deploy-edge") {
|
|
45
64
|
const buildAppId = process.env.AWS_APP_ID;
|
|
46
65
|
const buildBranch = process.env.AWS_BRANCH;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { loadAutomationOverview, runAutomationNow } from "../lib/automation-client";
|
|
5
|
+
import { FIXTURE_AUTOMATION_OVERVIEW } from "../lib/automation-fixture";
|
|
6
|
+
import type { AutomationResult } from "../lib/automation-overview";
|
|
7
|
+
import { AutomationView } from "./automation-view";
|
|
8
|
+
|
|
9
|
+
const REFRESH_MILLISECONDS = 60_000;
|
|
10
|
+
|
|
11
|
+
/** Administration, Automation: loads the records and their recent runs, and refreshes each minute. */
|
|
12
|
+
export function AutomationPanel({ isDemo }: { isDemo: boolean }) {
|
|
13
|
+
const [result, setResult] = useState<AutomationResult | null>(null);
|
|
14
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (isDemo) {
|
|
18
|
+
setResult(FIXTURE_AUTOMATION_OVERVIEW);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let live = true;
|
|
22
|
+
const refresh = async () => {
|
|
23
|
+
try {
|
|
24
|
+
const next = await loadAutomationOverview();
|
|
25
|
+
if (live) setResult(next);
|
|
26
|
+
} catch {
|
|
27
|
+
if (live) setResult({ state: "unavailable" });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
void refresh();
|
|
31
|
+
const timer = window.setInterval(() => void refresh(), REFRESH_MILLISECONDS);
|
|
32
|
+
return () => {
|
|
33
|
+
live = false;
|
|
34
|
+
window.clearInterval(timer);
|
|
35
|
+
};
|
|
36
|
+
}, [isDemo]);
|
|
37
|
+
|
|
38
|
+
const runNow = isDemo ? undefined : async (automationId: string, input: Record<string, unknown> | null) => {
|
|
39
|
+
const outcome = await runAutomationNow(automationId, input);
|
|
40
|
+
try {
|
|
41
|
+
setResult(await loadAutomationOverview()); // show the new run without waiting for the minute
|
|
42
|
+
} catch {
|
|
43
|
+
/* the next refresh will */
|
|
44
|
+
}
|
|
45
|
+
return outcome;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return <AutomationView result={result} selectedId={selectedId} onSelect={setSelectedId} demo={isDemo} onRunNow={runNow} />;
|
|
49
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
atATimeText,
|
|
6
|
+
inputTemplateText,
|
|
7
|
+
parseRunInput,
|
|
8
|
+
runNowMessage,
|
|
9
|
+
type AutomationRecord,
|
|
10
|
+
type RunNowResult,
|
|
11
|
+
} from "../lib/automation-overview";
|
|
12
|
+
|
|
13
|
+
export type RunNowPhase = "idle" | "confirming" | "running" | "done";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* "Run now": a button, then a confirm step that says what will start and, when the record's input names
|
|
17
|
+
* placeholders for an item, asks for the input of this run. Starting costs money (the procedure calls a model),
|
|
18
|
+
* so nothing starts on the first click. `initialPhase` and `initialMessage` exist so every step renders from a test.
|
|
19
|
+
*/
|
|
20
|
+
export function RunNowControl({
|
|
21
|
+
record,
|
|
22
|
+
demo = false,
|
|
23
|
+
onRun,
|
|
24
|
+
initialPhase = "idle",
|
|
25
|
+
initialMessage = null,
|
|
26
|
+
}: {
|
|
27
|
+
record: Pick<AutomationRecord, "id" | "title" | "procedureKey" | "runNow" | "limits">;
|
|
28
|
+
demo?: boolean;
|
|
29
|
+
onRun?: (automationId: string, input: Record<string, unknown> | null) => Promise<RunNowResult>;
|
|
30
|
+
initialPhase?: RunNowPhase;
|
|
31
|
+
initialMessage?: string | null;
|
|
32
|
+
}) {
|
|
33
|
+
const [phase, setPhase] = useState<RunNowPhase>(initialPhase);
|
|
34
|
+
const [message, setMessage] = useState<string | null>(initialMessage);
|
|
35
|
+
const [text, setText] = useState(record.runNow.needsInput ? inputTemplateText(record.runNow.inputNames) : "");
|
|
36
|
+
const [problem, setProblem] = useState<string | null>(null);
|
|
37
|
+
|
|
38
|
+
if (!record.runNow.available) {
|
|
39
|
+
return (
|
|
40
|
+
<p className="news-desk-delegation-note" data-automation-run-now-unavailable>
|
|
41
|
+
Run now is not available: {record.runNow.reason === "disabled" ? "the automation is disabled. Enable it first." : (record.runNow.reason ?? "not supported")}.
|
|
42
|
+
</p>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function confirm() {
|
|
47
|
+
const parsed = parseRunInput(text);
|
|
48
|
+
if (!parsed.ok) {
|
|
49
|
+
setProblem(parsed.message);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (record.runNow.needsInput && parsed.value === null) {
|
|
53
|
+
setProblem("This automation's input names an item, so give the input for this run.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setProblem(null);
|
|
57
|
+
setPhase("running");
|
|
58
|
+
try {
|
|
59
|
+
const result = onRun ? await onRun(record.id, parsed.value) : ({ state: "unavailable" } as RunNowResult);
|
|
60
|
+
setMessage(runNowMessage(result));
|
|
61
|
+
} catch {
|
|
62
|
+
setMessage(runNowMessage({ state: "unavailable" }));
|
|
63
|
+
}
|
|
64
|
+
setPhase("done");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div data-automation-run-now={record.id} data-run-now-phase={phase}>
|
|
69
|
+
{phase === "idle" || phase === "done" ? (
|
|
70
|
+
<button type="button" data-automation-run-now-start onClick={() => { setMessage(null); setPhase("confirming"); }}>
|
|
71
|
+
Run now
|
|
72
|
+
</button>
|
|
73
|
+
) : null}
|
|
74
|
+
{phase === "confirming" || phase === "running" ? (
|
|
75
|
+
<div role="group" aria-label={`Confirm Run now for ${record.title}`} data-automation-run-now-confirm>
|
|
76
|
+
<p>
|
|
77
|
+
Run <strong>{record.procedureKey}</strong> now? It calls a model, which costs money, and it counts against the
|
|
78
|
+
limit ({atATimeText(record.limits)}). The run is linked to this automation.
|
|
79
|
+
</p>
|
|
80
|
+
{demo ? <p className="news-desk-delegation-note">This is the demo, so nothing will start.</p> : null}
|
|
81
|
+
<label>
|
|
82
|
+
Input for this run{record.runNow.needsInput ? " (required)" : " (optional; blank uses the record's input)"}
|
|
83
|
+
<textarea rows={4} value={text} onChange={(event) => setText(event.target.value)} disabled={phase === "running"} data-automation-run-now-input />
|
|
84
|
+
</label>
|
|
85
|
+
{problem ? <p role="alert" data-automation-run-now-problem>{problem}</p> : null}
|
|
86
|
+
<button type="button" onClick={() => void confirm()} disabled={phase === "running" || demo} data-automation-run-now-confirm-button>
|
|
87
|
+
{phase === "running" ? "Starting…" : "Confirm: start the run"}
|
|
88
|
+
</button>{" "}
|
|
89
|
+
<button type="button" onClick={() => { setPhase("idle"); setProblem(null); }} disabled={phase === "running"} data-automation-run-now-cancel>
|
|
90
|
+
Cancel
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
) : null}
|
|
94
|
+
{message ? <p role="status" data-automation-run-now-message>{message}</p> : null}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { RunNowControl } from "./automation-run-now";
|
|
3
|
+
import {
|
|
4
|
+
ENABLED_NOTE,
|
|
5
|
+
OUTSIDE_NOTICE,
|
|
6
|
+
atATimeText,
|
|
7
|
+
costText,
|
|
8
|
+
durationText,
|
|
9
|
+
enabledText,
|
|
10
|
+
formatUtc,
|
|
11
|
+
linkText,
|
|
12
|
+
noticeText,
|
|
13
|
+
proceduresHref,
|
|
14
|
+
statusMark,
|
|
15
|
+
statusText,
|
|
16
|
+
storyHref,
|
|
17
|
+
triggerText,
|
|
18
|
+
toolsText,
|
|
19
|
+
whoText,
|
|
20
|
+
type AutomationOutsideRun,
|
|
21
|
+
type AutomationRecord,
|
|
22
|
+
type AutomationResult,
|
|
23
|
+
type AutomationRun,
|
|
24
|
+
type RunNowResult,
|
|
25
|
+
} from "../lib/automation-overview";
|
|
26
|
+
|
|
27
|
+
function Message({ title, children, kind }: { title: string; children?: React.ReactNode; kind: string }) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="category-steering-empty" role={kind === "loading" ? "status" : "alert"} data-automation-state={kind}>
|
|
30
|
+
<strong>{title}</strong>
|
|
31
|
+
{children ? <p>{children}</p> : null}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function RunLine({ run, demo }: { run: AutomationRun; demo: boolean }) {
|
|
37
|
+
return (
|
|
38
|
+
<li data-automation-run={run.runId ?? run.sourceId ?? "unlinked"} data-run-status={run.status ?? "unknown"}>
|
|
39
|
+
<strong>{formatUtc(run.startedAt)}</strong>
|
|
40
|
+
<span><span aria-hidden="true">{statusMark(run.status)} </span>Status: {statusText(run.status)} · {durationText(run.durationSeconds)}</span>
|
|
41
|
+
<span>{whoText(run)}</span>
|
|
42
|
+
{run.reason ? <span data-automation-run-reason>Reason: {run.reason}</span> : null}
|
|
43
|
+
{run.error ? <span data-automation-run-error>Error: {run.error}</span> : null}
|
|
44
|
+
<span>
|
|
45
|
+
{run.runId ? <>Run {run.runId} (<Link href={proceduresHref(demo)}>Procedures</Link>)</> : run.triggerStatus === "started" ? "Run record not linked yet" : "No run record"}
|
|
46
|
+
{run.runRecord === "missing" ? " (the run record was not found)" : ""}
|
|
47
|
+
{run.runRecord === "unavailable" ? " (the run record could not be read)" : ""}
|
|
48
|
+
</span>
|
|
49
|
+
<span>
|
|
50
|
+
{run.storyId && run.storyExists === false ? <>Story: {run.storyTitle ?? run.storyId} (story deleted)</>
|
|
51
|
+
: run.storyId ? <>Story: <Link href={storyHref(run.storyId, demo)} data-automation-story-link>{run.storyTitle ?? run.storyId}</Link></> : "Story: none recorded"}
|
|
52
|
+
</span>
|
|
53
|
+
<span data-automation-run-tools>{toolsText(run)}</span>
|
|
54
|
+
<span data-automation-run-cost data-over-budget={run.overBudget || undefined}>
|
|
55
|
+
<span aria-hidden="true">{run.overBudget ? "! " : ""}</span>{costText(run)}
|
|
56
|
+
</span>
|
|
57
|
+
</li>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function OutsideRunLine({ run, demo }: { run: AutomationOutsideRun; demo: boolean }) {
|
|
62
|
+
return (
|
|
63
|
+
<li data-automation-outside-run={run.runId ?? "unlinked"} data-run-status={run.status ?? "unknown"}>
|
|
64
|
+
<strong>{formatUtc(run.startedAt)}</strong>
|
|
65
|
+
<span><span aria-hidden="true">{statusMark(run.status)} </span>Status: {statusText(run.status)} · {durationText(run.durationSeconds)}</span>
|
|
66
|
+
<span>Started by: {run.requestedBy ?? "unknown"}</span>
|
|
67
|
+
{run.error ? <span data-automation-run-error>Error: {run.error}</span> : null}
|
|
68
|
+
<span>{run.runId ? <>Run {run.runId} (<Link href={proceduresHref(demo)}>Procedures</Link>)</> : "No run id"}</span>
|
|
69
|
+
<span>
|
|
70
|
+
{run.storyId ? <>Story: <Link href={storyHref(run.storyId, demo)}>{run.storyTitle ?? run.storyId}</Link></> : "Story: none recorded"}
|
|
71
|
+
</span>
|
|
72
|
+
<span data-automation-run-cost>{costText({ ...run, budgetUsd: null })}</span>
|
|
73
|
+
</li>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function Detail({ record, demo, onRunNow }: { record: AutomationRecord; demo: boolean; onRunNow?: RunNowHandler }) {
|
|
78
|
+
const notice = noticeText(record.notice);
|
|
79
|
+
return (
|
|
80
|
+
<article className="news-desk-doctrine-card" data-automation-detail={record.id}>
|
|
81
|
+
<div className="news-desk-doctrine-card__header"><div><h3>{record.title}</h3><span>{record.description ?? record.id}</span></div></div>
|
|
82
|
+
<dl className="news-desk-monitoring__usage">
|
|
83
|
+
<div><dt>Trigger</dt><dd>{triggerText(record.trigger)}{record.supported ? "" : ` (${record.supportNote ?? "not yet supported"})`}</dd></div>
|
|
84
|
+
<div><dt>Procedure</dt><dd>{record.procedureKey}</dd></div>
|
|
85
|
+
<div><dt>Input</dt><dd>{Object.keys(record.inputTemplate).length ? Object.entries(record.inputTemplate).map(([name, template]) => `${name} = ${template}`).join("; ") : "none"}</dd></div>
|
|
86
|
+
<div><dt>Enabled</dt><dd>{enabledText(record)} ({ENABLED_NOTE})</dd></div>
|
|
87
|
+
<div><dt>Limits</dt><dd>timeout {record.limits.timeoutSeconds ?? "?"} s, {atATimeText(record.limits)}{record.limits.budgetUsdPerRun !== undefined ? `, about $${record.limits.budgetUsdPerRun} per run, advisory` : ""} ({record.limitsNote})</dd></div>
|
|
88
|
+
<div><dt>Router</dt><dd data-automation-link={record.link?.state ?? "unchecked"}>{linkText(record.link)}</dd></div>
|
|
89
|
+
</dl>
|
|
90
|
+
{notice ? <p className="news-desk-delegation-note" data-automation-notice>{notice}</p> : null}
|
|
91
|
+
<RunNowControl record={record} demo={demo} onRun={onRunNow} />
|
|
92
|
+
<h4>Recent runs</h4>
|
|
93
|
+
{record.runs.length ? (
|
|
94
|
+
<ul className="news-desk-monitoring__list" data-automation-runs>
|
|
95
|
+
{record.runs.map((run) => <RunLine key={run.runId ?? `${run.sourceId}-${run.startedAt}`} run={run} demo={demo} />)}
|
|
96
|
+
</ul>
|
|
97
|
+
) : <p data-automation-no-runs>No runs yet.</p>}
|
|
98
|
+
{record.skippedAlreadyHadReference > 0 ? (
|
|
99
|
+
<p className="news-desk-delegation-note" data-automation-skipped>
|
|
100
|
+
{record.skippedAlreadyHadReference} {record.skippedAlreadyHadReference === 1 ? "item was" : "items were"} not started because the Reference already existed.
|
|
101
|
+
</p>
|
|
102
|
+
) : null}
|
|
103
|
+
{record.outsideRuns.length || record.outsideNotice ? (
|
|
104
|
+
<>
|
|
105
|
+
<h4>Started outside this automation</h4>
|
|
106
|
+
<p className="news-desk-delegation-note">Other runs of {record.procedureKey} that this automation did not start.</p>
|
|
107
|
+
{record.outsideNotice ? <p className="news-desk-delegation-note" data-automation-notice>{OUTSIDE_NOTICE}</p> : null}
|
|
108
|
+
{record.outsideRuns.length ? (
|
|
109
|
+
<ul className="news-desk-monitoring__list" data-automation-outside-runs>
|
|
110
|
+
{record.outsideRuns.map((run) => <OutsideRunLine key={run.runId ?? run.startedAt ?? "run"} run={run} demo={demo} />)}
|
|
111
|
+
</ul>
|
|
112
|
+
) : null}
|
|
113
|
+
</>
|
|
114
|
+
) : null}
|
|
115
|
+
</article>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ListItem({ record, selected, onSelect }: { record: AutomationRecord; selected: boolean; onSelect: (id: string) => void }) {
|
|
120
|
+
const last = record.lastRun;
|
|
121
|
+
return (
|
|
122
|
+
<li data-automation={record.id}>
|
|
123
|
+
<div>
|
|
124
|
+
<strong>{record.title}</strong>
|
|
125
|
+
<span>{triggerText(record.trigger)}{record.supported ? "" : ` (${record.supportNote ?? "not yet supported"})`}</span>
|
|
126
|
+
</div>
|
|
127
|
+
<span>Procedure: {record.procedureKey}</span>
|
|
128
|
+
<span>{enabledText(record)} ({ENABLED_NOTE})</span>
|
|
129
|
+
<span data-automation-last-run>
|
|
130
|
+
Last run: <span aria-hidden="true">{statusMark(last?.status)} </span>{statusText(last?.status)}{last ? `, ${formatUtc(last.startedAt)}` : ""}
|
|
131
|
+
</span>
|
|
132
|
+
<button type="button" aria-pressed={selected} data-automation-select={record.id} onClick={() => onSelect(record.id)}>
|
|
133
|
+
{selected ? "Showing runs" : "Show runs"}
|
|
134
|
+
</button>
|
|
135
|
+
</li>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Presentation only: the panel fetches, this renders any result, so every state is testable from fixtures. */
|
|
140
|
+
export type RunNowHandler = (automationId: string, input: Record<string, unknown> | null) => Promise<RunNowResult>;
|
|
141
|
+
|
|
142
|
+
export function AutomationView({
|
|
143
|
+
result,
|
|
144
|
+
selectedId,
|
|
145
|
+
onSelect,
|
|
146
|
+
demo = false,
|
|
147
|
+
onRunNow,
|
|
148
|
+
}: {
|
|
149
|
+
result: AutomationResult | null;
|
|
150
|
+
selectedId: string | null;
|
|
151
|
+
onSelect: (id: string) => void;
|
|
152
|
+
demo?: boolean;
|
|
153
|
+
onRunNow?: RunNowHandler;
|
|
154
|
+
}) {
|
|
155
|
+
let body: React.ReactNode;
|
|
156
|
+
if (result === null) {
|
|
157
|
+
body = <Message kind="loading" title="Loading automations" />;
|
|
158
|
+
} else if ("error" in result) {
|
|
159
|
+
body = result.error === "forbidden"
|
|
160
|
+
? <Message kind="forbidden" title="Automations are for editors and admins">Your account does not have access to this page. Ask an admin for the editor role.</Message>
|
|
161
|
+
: <Message kind="error" title="Automations could not be read">The request was not understood. Reload the page.</Message>;
|
|
162
|
+
} else if (result.state === "unavailable") {
|
|
163
|
+
body = <Message kind="unavailable" title="Automations could not be read right now">Nothing is affected. Try again in a minute.</Message>;
|
|
164
|
+
} else if (result.state === "empty") {
|
|
165
|
+
body = <Message kind="empty" title="No automations yet">An operator creates one with <code>papyrus automations create</code>. Nothing is configured to run on its own from this page.</Message>;
|
|
166
|
+
} else {
|
|
167
|
+
const selected = result.automations.find((record) => record.id === selectedId) ?? (result.automations.length === 1 ? result.automations[0] : null);
|
|
168
|
+
body = (
|
|
169
|
+
<>
|
|
170
|
+
<article className="news-desk-doctrine-card" data-automation-section="list">
|
|
171
|
+
<div className="news-desk-doctrine-card__header"><div><h3>Automations</h3><span>What starts a procedure by itself</span></div></div>
|
|
172
|
+
<ul className="news-desk-monitoring__list">
|
|
173
|
+
{result.automations.map((record) => <ListItem key={record.id} record={record} selected={selected?.id === record.id} onSelect={onSelect} />)}
|
|
174
|
+
</ul>
|
|
175
|
+
{result.monitoring === "not_configured" ? <p className="news-desk-delegation-note" data-automation-notice>{noticeText("monitoring_not_configured")}</p> : null}
|
|
176
|
+
</article>
|
|
177
|
+
{selected ? <Detail record={selected} demo={demo} onRunNow={onRunNow} /> : <p className="news-desk-delegation-note" data-automation-pick>Choose an automation to see its recent runs.</p>}
|
|
178
|
+
</>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return (
|
|
182
|
+
<div data-news-desk-admin-panel="automations">
|
|
183
|
+
<article className="news-desk-doctrine-card">
|
|
184
|
+
<div className="news-desk-doctrine-card__header">
|
|
185
|
+
<div>
|
|
186
|
+
<h3>Automation</h3>
|
|
187
|
+
<span>Procedures that start on their own. Editors and admins can run one now; an operator changes the settings with the command line.</span>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
{result && "asOf" in result ? <p className="news-desk-delegation-note" data-automation-as-of>As of {formatUtc(result.asOf)}. Enabled: {ENABLED_NOTE}.</p> : null}
|
|
191
|
+
</article>
|
|
192
|
+
{body}
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|