@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { EditorialCheckResult, EditorialFeedbackInput, EditorialFinding, RewriteResult, Span } from "./editorial-checks";
|
|
2
2
|
import type { EditorialSettings } from "./editorial-checks-settings";
|
|
3
3
|
|
|
4
|
-
/** What the article editor needs from editorial checks: the
|
|
4
|
+
/** What the article editor needs from editorial checks: the site's GraphQL API, or the demo. */
|
|
5
5
|
export type EditorialChecksBackend = {
|
|
6
6
|
loadSettings: () => Promise<{ settings: EditorialSettings; exists: boolean }>;
|
|
7
7
|
dismissedFindingIds: (itemId: string) => Promise<Set<string>>;
|
|
@@ -161,6 +161,24 @@ export async function importStarterPack(name: string): Promise<{ created: number
|
|
|
161
161
|
return parsed(data.importEditorialStarterPack);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
export type StarterPackSummary = {
|
|
165
|
+
name: string;
|
|
166
|
+
title: string;
|
|
167
|
+
version?: string | null;
|
|
168
|
+
language?: string | null;
|
|
169
|
+
guidance?: string | null;
|
|
170
|
+
policyCount: number;
|
|
171
|
+
importedCount: number;
|
|
172
|
+
isDefault: boolean;
|
|
173
|
+
policies: { id: string; title?: string | null; severity?: string | null; tags: string[]; lanes: string[] }[];
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** The starter packs the site's Humanus ships, with how many of each pack's policies the site already has. */
|
|
177
|
+
export async function listStarterPacks(): Promise<{ humanusVersion?: string | null; packs: StarterPackSummary[] }> {
|
|
178
|
+
const data = await call<{ listEditorialStarterPacks: unknown }>(`query ListStarterPacks { listEditorialStarterPacks }`);
|
|
179
|
+
return parsed(data.listEditorialStarterPacks);
|
|
180
|
+
}
|
|
181
|
+
|
|
164
182
|
export async function tryPolicy(policy: Record<string, unknown>, text: string): Promise<EditorialCheckResult> {
|
|
165
183
|
const data = await call<{ tryEditorialPolicy: unknown }>(
|
|
166
184
|
`query TryEditorialPolicy($policy: AWSJSON!, $text: String!) { tryEditorialPolicy(policy: $policy, text: $text, format: "markus") }`,
|
|
@@ -184,7 +202,7 @@ export async function recalibrate(): Promise<{ updated: number; detectors: { pol
|
|
|
184
202
|
return parsed(data.recalibrateEditorialPolicies);
|
|
185
203
|
}
|
|
186
204
|
|
|
187
|
-
/** The
|
|
205
|
+
/** The site's GraphQL API, signed in as the editor. */
|
|
188
206
|
export const graphqlEditorialBackend: EditorialChecksBackend = {
|
|
189
207
|
loadSettings,
|
|
190
208
|
dismissedFindingIds,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Demo-mode editorial checks: no backend, a few stock phrases, canned rewrites.
|
|
3
|
-
* The shapes match
|
|
4
|
-
* rewrite flow behave as they do against the real
|
|
3
|
+
* The shapes match Humanus results so the editor, the preview marks and the
|
|
4
|
+
* rewrite flow behave as they do against the real backend.
|
|
5
5
|
*/
|
|
6
6
|
import type { EditorialCheckResult, EditorialFinding, EditorialSegment, RewriteResult, Span } from "./editorial-checks";
|
|
7
7
|
import type { EditorialChecksBackend } from "./editorial-checks-backend";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Runtime settings for editorial checks, stored in EditorialCheckSettings (id "settings"). */
|
|
2
2
|
export type EditorialSettings = {
|
|
3
|
+
/** Core to every site and on by default; a publication turns its checks off here. */
|
|
4
|
+
enabled: boolean;
|
|
3
5
|
lanes: { rules: boolean; classifier: boolean };
|
|
4
6
|
classifierModel: string;
|
|
5
7
|
rewriteModel: string;
|
|
@@ -12,6 +14,7 @@ export type EditorialSettings = {
|
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export const DEFAULT_EDITORIAL_SETTINGS: EditorialSettings = {
|
|
17
|
+
enabled: true,
|
|
15
18
|
lanes: { rules: true, classifier: true },
|
|
16
19
|
classifierModel: "jev-latest",
|
|
17
20
|
rewriteModel: "gpt-6-luna",
|
package/lib/editorial-checks.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Editorial checks: the
|
|
3
|
-
* over them.
|
|
2
|
+
* Editorial checks: the Humanus result shapes the newsroom uses, and pure helpers
|
|
3
|
+
* over them. Humanus counts offsets in UTF-16 code units, which is exactly how
|
|
4
4
|
* JavaScript indexes strings, so spans apply to the body text as they are.
|
|
5
5
|
*/
|
|
6
|
-
import { pluginSettings, type PluginOutputs } from "./plugin-settings";
|
|
7
|
-
|
|
8
|
-
export const EDITORIAL_CHECKS_PLUGIN = "editorial-checks";
|
|
9
6
|
|
|
10
7
|
export type Span = { start: number; end: number };
|
|
11
8
|
export type Severity = "info" | "warn" | "error";
|
|
@@ -41,7 +38,8 @@ export type EditorialSegment = {
|
|
|
41
38
|
text?: string;
|
|
42
39
|
};
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
/** "skipped": Papyrus did not ask for judgments because the publication has no mission or policy for them to apply. */
|
|
42
|
+
export type LaneStatus = { status: "ran" | "disabled" | "unavailable" | "failed" | "partial" | "skipped"; reason?: string; detectors?: number };
|
|
45
43
|
|
|
46
44
|
export type EditorialCheckResult = {
|
|
47
45
|
schemaVersion: 1;
|
|
@@ -52,7 +50,8 @@ export type EditorialCheckResult = {
|
|
|
52
50
|
protected: { kind: string; start: number; end: number }[];
|
|
53
51
|
findings: EditorialFinding[];
|
|
54
52
|
coverage: unknown[];
|
|
55
|
-
lanes
|
|
53
|
+
/** lanes.section is "skipped" when the story's section has no NewsroomSection record to take a mission and policy from. */
|
|
54
|
+
lanes: { rules: LaneStatus; classifier: LaneStatus; section?: LaneStatus };
|
|
56
55
|
usage?: Record<string, number>;
|
|
57
56
|
};
|
|
58
57
|
|
|
@@ -91,10 +90,6 @@ export const UNSAVED_ITEM_ID = "unsaved";
|
|
|
91
90
|
|
|
92
91
|
const SEVERITY_ORDER: Record<Severity, number> = { error: 0, warn: 1, info: 2 };
|
|
93
92
|
|
|
94
|
-
export function editorialChecksEnabled(outputs: PluginOutputs | null | undefined): boolean {
|
|
95
|
-
return pluginSettings(outputs, EDITORIAL_CHECKS_PLUGIN) !== null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
93
|
/** Findings worth showing: not dismissed, not covering the whole document, most severe first, then in reading order. */
|
|
99
94
|
export function visibleFindings(findings: EditorialFinding[], dismissed: ReadonlySet<string>): EditorialFinding[] {
|
|
100
95
|
return findings
|
|
@@ -171,8 +166,10 @@ export function describeProbability(finding: EditorialFinding): string | null {
|
|
|
171
166
|
|
|
172
167
|
export function laneNotice(result: EditorialCheckResult): string | null {
|
|
173
168
|
const classifier = result.lanes.classifier;
|
|
169
|
+
if (classifier.status === "skipped") return `Mission-based checks skipped: ${classifier.reason ?? "this publication has no mission yet"}.`;
|
|
174
170
|
if (classifier.status === "partial") return `Some sentences were not judged: ${classifier.reason ?? "the check ran out of time"}.`;
|
|
175
171
|
if (classifier.status === "failed") return `The judgment checks failed: ${classifier.reason ?? "unknown error"}. Rule checks still ran.`;
|
|
176
172
|
if (classifier.status === "unavailable" && (classifier.detectors ?? 0) === 0 && classifier.reason) return `Judgment checks are not configured: ${classifier.reason}.`;
|
|
173
|
+
if (result.lanes.section?.status === "skipped") return `Section-based checks skipped: ${result.lanes.section.reason ?? "no section configuration"}.`;
|
|
177
174
|
return null;
|
|
178
175
|
}
|
package/lib/editorial-preview.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Places
|
|
2
|
+
* Places Humanus findings on the rendered Markus preview.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Humanus reports spans in the markup; the preview renders an IR tree with no
|
|
5
5
|
* character offsets. Both sides agree on the words a reader sees, so findings
|
|
6
|
-
* are placed by text: each
|
|
6
|
+
* are placed by text: each Humanus paragraph is matched, in document order, to
|
|
7
7
|
* the rendered prose block with the same words, and each finding's plain
|
|
8
8
|
* excerpt is located inside that block. Whitespace is ignored while matching,
|
|
9
|
-
* because rendering and
|
|
9
|
+
* because rendering and Humanus collapse it differently (citations, breaks).
|
|
10
10
|
*
|
|
11
11
|
* "Rendered text" is defined by `inlineLeaves`, which the preview renderer also
|
|
12
12
|
* uses, so offsets computed here are offsets the renderer can split on.
|
|
@@ -131,7 +131,7 @@ function blockKey(text: string): string {
|
|
|
131
131
|
return compact(text).value.replace(/[*_`~]/g, "");
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/** Match every
|
|
134
|
+
/** Match every Humanus paragraph to a rendered block and place its findings and sentences there. */
|
|
135
135
|
export function planPreview(
|
|
136
136
|
document: MarkusDocument,
|
|
137
137
|
result: EditorialCheckResult,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { generateClient } from "aws-amplify/data";
|
|
4
|
+
import type { MonitoringResult } from "./monitoring-overview";
|
|
5
|
+
|
|
6
|
+
type GraphQLResult = { data?: { monitoringOverview?: unknown }; errors?: { message?: string; errorType?: string }[] };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reads the monitoring overview 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 read.
|
|
11
|
+
*/
|
|
12
|
+
export async function loadMonitoringOverview(scope: "publication" | "all"): Promise<MonitoringResult> {
|
|
13
|
+
const client = generateClient({ authMode: "userPool" });
|
|
14
|
+
let result: GraphQLResult;
|
|
15
|
+
try {
|
|
16
|
+
result = (await client.graphql({
|
|
17
|
+
query: `query MonitoringOverview($scope: String) { monitoringOverview(scope: $scope) }`,
|
|
18
|
+
variables: { scope },
|
|
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?.monitoringOverview === undefined || result.data.monitoringOverview === null) return { state: "unavailable" };
|
|
27
|
+
const raw = result.data.monitoringOverview;
|
|
28
|
+
return (typeof raw === "string" ? JSON.parse(raw) : raw) as MonitoringResult;
|
|
29
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MonitoringOverview } from "./monitoring-overview";
|
|
2
|
+
|
|
3
|
+
/** A realistic overview for tests and the `?demo=1` newsroom. Public Hacker News titles only. */
|
|
4
|
+
export const FIXTURE_MONITORING_OVERVIEW: MonitoringOverview = {
|
|
5
|
+
state: "ready",
|
|
6
|
+
asOf: "2026-10-21T02:40:00Z",
|
|
7
|
+
scope: { mode: "publication", publicationId: "desk-a", site: "a.example.test", canViewAll: true },
|
|
8
|
+
feeds: [{ id: "hn", sourceUrl: "https://news.ycombinator.com/rss", intervalSeconds: 900, limit: 30, enabled: true,
|
|
9
|
+
lastStatus: "complete", lastFinishedAt: "2026-10-21T02:33:00Z", nextDueAt: "2026-10-21T02:50:00Z" }],
|
|
10
|
+
runs: [
|
|
11
|
+
{ runId: "r3", startedAt: "2026-10-21T02:33:00.000000Z", status: "complete", new: 2, revised: 1, unchanged: 27, itemCount: 30, errorCount: 0, errors: [] },
|
|
12
|
+
{ runId: "r1", startedAt: "2026-10-21T02:18:00.000000Z", status: "complete", new: 0, revised: 0, unchanged: 30, itemCount: 30, errorCount: 0, errors: [] },
|
|
13
|
+
{ runId: "r0", startedAt: "2026-10-21T02:03:00.000000Z", status: "partial", new: 1, revised: 0, unchanged: 28, itemCount: 30, errorCount: 1, errors: ["ConnectionError: timeout"] },
|
|
14
|
+
],
|
|
15
|
+
routerRuns: [{ runId: "q2", startedAt: "2026-10-21T02:37:00.000000Z", mode: "live", status: "stopped", stoppedBy: "request_ceiling_reached",
|
|
16
|
+
counts: { examined: 3, decided: 1, delivered: 1, deliveryFailed: 1, routed: 1 }, errorCount: 0 }],
|
|
17
|
+
router: { mode: "live", feedback: true, enabled: true, maxItemsPerRun: 5 },
|
|
18
|
+
usage: { day: "2026-10-21", decisionsToday: 2, decisionsTotal: 3, ceilings: { decisionsTotal: 300, decisionsPerDay: 100, items: 300 } },
|
|
19
|
+
hiddenNotRelevant: 1,
|
|
20
|
+
items: [
|
|
21
|
+
{ sourceId: "104", revision: 1, title: "Fresh Show HN", url: "https://example.org/104", discussionUrl: "https://news.ycombinator.com/item?id=104",
|
|
22
|
+
observedAt: "2026-10-21T02:30:00.000000Z", routing: "collected", routingNote: null, decidedAt: null,
|
|
23
|
+
relevance: [{ publicationId: "desk-a", site: "a.example.test", relevant: null, label: null, confidence: null, delivery: { state: null } }] },
|
|
24
|
+
{ sourceId: "105", revision: 1, title: "Broken request", url: "https://example.org/105", discussionUrl: "https://news.ycombinator.com/item?id=105",
|
|
25
|
+
observedAt: "2026-10-21T01:30:00.000000Z", routing: "error", routingNote: "invalid request: bad title", decidedAt: null,
|
|
26
|
+
relevance: [{ publicationId: "desk-a", site: "a.example.test", relevant: null, label: null, confidence: null, delivery: { state: null } }] },
|
|
27
|
+
{ sourceId: "101", revision: 1, title: "Quantum paper on arXiv", url: "https://example.org/101", discussionUrl: "https://news.ycombinator.com/item?id=101",
|
|
28
|
+
observedAt: "2026-10-21T00:10:00.000000Z", routing: "routed", routingNote: null, decidedAt: "2026-10-21T01:00:00Z",
|
|
29
|
+
relevance: [{ publicationId: "desk-a", site: "a.example.test", relevant: true, label: "include", confidence: 0.91,
|
|
30
|
+
delivery: { state: "delivered", referenceId: "ref-1", referenceLineageId: "lin-1", deliveredAt: "2026-10-21T01:00:05Z" } }] },
|
|
31
|
+
{ sourceId: "103", revision: 1, title: "Phishing kit leaks", url: "https://example.org/103", discussionUrl: "https://news.ycombinator.com/item?id=103",
|
|
32
|
+
observedAt: "2026-10-20T23:00:00.000000Z", routing: "decided", routingNote: null, decidedAt: "2026-10-20T23:10:00Z",
|
|
33
|
+
relevance: [{ publicationId: "desk-a", site: "a.example.test", relevant: true, label: "include", confidence: 0.7,
|
|
34
|
+
delivery: { state: "failed", error: "ClientError: throttled", attempts: 2 } }] },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const FIXTURE_ALL_PIPELINE_OVERVIEW: MonitoringOverview = {
|
|
39
|
+
...FIXTURE_MONITORING_OVERVIEW,
|
|
40
|
+
scope: { mode: "all", publicationId: "desk-a", site: "a.example.test", canViewAll: true },
|
|
41
|
+
hiddenNotRelevant: 0,
|
|
42
|
+
items: FIXTURE_MONITORING_OVERVIEW.items.map((item) => item.sourceId !== "101" ? item : {
|
|
43
|
+
...item,
|
|
44
|
+
relevance: [
|
|
45
|
+
...item.relevance,
|
|
46
|
+
{ publicationId: "desk-b", site: "b.example.test", relevant: false, label: "exclude", confidence: 0.8, delivery: { state: "not_applicable" } },
|
|
47
|
+
{ publicationId: "desk-c", site: "desk-c", relevant: true, label: "include", confidence: null,
|
|
48
|
+
delivery: { state: "delivered", referenceId: "ref-2", referenceLineageId: "lin-2" } },
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Shapes and wording for the newsroom Monitoring page. The function in src/papyrus_content/monitoring_view.py produces them. */
|
|
2
|
+
import { getNewsroomNavHref } from "./newsroom-nav";
|
|
3
|
+
|
|
4
|
+
export type MonitoringDeliveryState = "delivered" | "failed" | "pending" | "not_applicable" | null;
|
|
5
|
+
|
|
6
|
+
export type MonitoringRelevance = {
|
|
7
|
+
publicationId: string;
|
|
8
|
+
site: string | null;
|
|
9
|
+
relevant: boolean | null;
|
|
10
|
+
label: string | null;
|
|
11
|
+
confidence: number | null;
|
|
12
|
+
delivery: {
|
|
13
|
+
state: MonitoringDeliveryState;
|
|
14
|
+
referenceId?: string | null;
|
|
15
|
+
referenceLineageId?: string | null;
|
|
16
|
+
deliveredAt?: string | null;
|
|
17
|
+
error?: string | null;
|
|
18
|
+
attempts?: number | null;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type MonitoringItem = {
|
|
23
|
+
sourceId: string;
|
|
24
|
+
revision: number;
|
|
25
|
+
title: string;
|
|
26
|
+
url: string | null;
|
|
27
|
+
discussionUrl: string | null;
|
|
28
|
+
observedAt: string | null;
|
|
29
|
+
routing: string;
|
|
30
|
+
routingNote: string | null;
|
|
31
|
+
decidedAt: string | null;
|
|
32
|
+
relevance: MonitoringRelevance[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type MonitoringRun = {
|
|
36
|
+
runId: string;
|
|
37
|
+
startedAt: string;
|
|
38
|
+
status: string;
|
|
39
|
+
new: number;
|
|
40
|
+
revised: number;
|
|
41
|
+
unchanged: number;
|
|
42
|
+
itemCount: number | null;
|
|
43
|
+
errorCount: number;
|
|
44
|
+
errors: (string | null)[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type MonitoringRouterRun = {
|
|
48
|
+
runId: string;
|
|
49
|
+
startedAt: string;
|
|
50
|
+
mode: string;
|
|
51
|
+
status: string;
|
|
52
|
+
stoppedBy: string | null;
|
|
53
|
+
waitingForClassifier?: string[];
|
|
54
|
+
counts: Record<string, number>;
|
|
55
|
+
errorCount: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type MonitoringFeed = {
|
|
59
|
+
id: string;
|
|
60
|
+
sourceUrl: string | null;
|
|
61
|
+
intervalSeconds: number | null;
|
|
62
|
+
limit: number | null;
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
lastStatus: string | null;
|
|
65
|
+
lastFinishedAt: string | null;
|
|
66
|
+
nextDueAt: string | null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type MonitoringOverview = {
|
|
70
|
+
state: "ready" | "empty";
|
|
71
|
+
asOf: string;
|
|
72
|
+
notice?: "publication_not_routed";
|
|
73
|
+
scope: { mode: "publication" | "all"; publicationId: string | null; site: string | null; canViewAll: boolean };
|
|
74
|
+
feeds: MonitoringFeed[];
|
|
75
|
+
runs: MonitoringRun[];
|
|
76
|
+
routerRuns: MonitoringRouterRun[];
|
|
77
|
+
router: { mode: string; feedback: boolean; enabled: boolean; maxItemsPerRun: number | null } | null;
|
|
78
|
+
usage: {
|
|
79
|
+
day: string;
|
|
80
|
+
decisionsToday: number;
|
|
81
|
+
decisionsTotal: number;
|
|
82
|
+
ceilings: { decisionsTotal?: number; decisionsPerDay?: number; items?: number } | null;
|
|
83
|
+
} | null;
|
|
84
|
+
items: MonitoringItem[];
|
|
85
|
+
hiddenNotRelevant: number;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type MonitoringResult =
|
|
89
|
+
| MonitoringOverview
|
|
90
|
+
| { state: "not_configured" }
|
|
91
|
+
| { state: "unavailable" }
|
|
92
|
+
| { error: "forbidden" | "bad_request" };
|
|
93
|
+
|
|
94
|
+
const ROUTING_WORDS: Record<string, string> = {
|
|
95
|
+
collected: "Collected, waiting for the router",
|
|
96
|
+
"dry-run-validated": "Checked in a dry run, not sent",
|
|
97
|
+
decided: "Judged, delivery not finished",
|
|
98
|
+
routed: "Routed",
|
|
99
|
+
refused: "Refused by the shared Cyclotron",
|
|
100
|
+
uncertain: "Uncertain, needs a person",
|
|
101
|
+
error: "Error",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export function routingLabel(routing: string): string {
|
|
105
|
+
return ROUTING_WORDS[routing] ?? routing;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Fixed-format UTC time so the page reads the same everywhere. */
|
|
109
|
+
export function formatUtc(iso: string | null | undefined): string {
|
|
110
|
+
if (!iso) return "never";
|
|
111
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(iso);
|
|
112
|
+
return match ? `${match[1]} ${match[2]} UTC` : iso;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function confidenceText(confidence: number | null): string {
|
|
116
|
+
return confidence === null ? "confidence not reported" : `${Math.round(confidence * 100)}% confidence`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function usageText(used: number, ceiling: number | undefined): string {
|
|
120
|
+
return ceiling === undefined ? `${used} used, no ceiling set` : `${used} of ${ceiling}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function everyText(seconds: number | null): string {
|
|
124
|
+
if (!seconds) return "on a schedule";
|
|
125
|
+
return seconds % 3600 === 0 ? `every ${seconds / 3600} h` : seconds % 60 === 0 ? `every ${seconds / 60} min` : `every ${seconds} s`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function referenceHref(lineageId: string, demo?: boolean): string {
|
|
129
|
+
return getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function describeRelevance(entry: MonitoringRelevance): string {
|
|
133
|
+
if (entry.relevant === null) return "Not judged yet";
|
|
134
|
+
return entry.relevant ? "Relevant" : "Not relevant";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function describeDelivery(entry: MonitoringRelevance): string {
|
|
138
|
+
switch (entry.delivery.state) {
|
|
139
|
+
case "delivered": return "In the References queue";
|
|
140
|
+
case "failed": return "Not in the References queue: delivery failed";
|
|
141
|
+
case "pending": return "Not in the References queue yet";
|
|
142
|
+
case "not_applicable": return "Nothing to deliver";
|
|
143
|
+
default: return "";
|
|
144
|
+
}
|
|
145
|
+
}
|
package/lib/news-desk-session.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { CategorySteeringDashboard } from "./category-repository";
|
|
2
|
-
import { createEmptyCategorySteeringDashboard } from "./category-dashboard";
|
|
3
2
|
import type { ReaderAuthSnapshot } from "../components/reader-auth-state";
|
|
4
3
|
|
|
5
4
|
export type NewsDeskShellPhase =
|
|
@@ -59,26 +58,6 @@ export function beginDeskLoad(state: NewsDeskShellState, auth: ReaderAuthSnapsho
|
|
|
59
58
|
};
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
export function beginAuthenticatedDeskHydration(
|
|
63
|
-
state: NewsDeskShellState,
|
|
64
|
-
auth: ReaderAuthSnapshot,
|
|
65
|
-
options: { canManageUsers: boolean },
|
|
66
|
-
): NewsDeskShellState {
|
|
67
|
-
const dashboard = state.dashboard ?? {
|
|
68
|
-
...createEmptyCategorySteeringDashboard(),
|
|
69
|
-
isPublicSkeleton: false,
|
|
70
|
-
canManageUsers: options.canManageUsers,
|
|
71
|
-
};
|
|
72
|
-
return {
|
|
73
|
-
...state,
|
|
74
|
-
phase: state.dashboard ? "refreshing" : "loadingDesk",
|
|
75
|
-
auth,
|
|
76
|
-
dashboard,
|
|
77
|
-
stale: false,
|
|
78
|
-
error: null,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
61
|
export function resolveSignedOut(state: NewsDeskShellState, auth: ReaderAuthSnapshot): NewsDeskShellState {
|
|
83
62
|
return {
|
|
84
63
|
...state,
|
|
@@ -143,6 +122,18 @@ export function resolveDashboardFailure(
|
|
|
143
122
|
};
|
|
144
123
|
}
|
|
145
124
|
|
|
125
|
+
/** An unverified session must never leave private records on screen. */
|
|
126
|
+
export function resolveAccessFailure(state: NewsDeskShellState, error: string): NewsDeskShellState {
|
|
127
|
+
return {
|
|
128
|
+
...state,
|
|
129
|
+
phase: "error",
|
|
130
|
+
auth: { status: "signedOut", label: "Signed out" },
|
|
131
|
+
dashboard: null,
|
|
132
|
+
stale: false,
|
|
133
|
+
error,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
146
137
|
export function patchDashboard(
|
|
147
138
|
state: NewsDeskShellState,
|
|
148
139
|
patch: (dashboard: CategorySteeringDashboard) => CategorySteeringDashboard,
|