@anthusai/papyrus 2.0.0-next.10 → 2.0.0-next.101

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.
Files changed (230) hide show
  1. package/amplify/auth/resource.js +16 -1
  2. package/amplify/auth/resource.ts +19 -1
  3. package/amplify/data/function-handler.js +20 -0
  4. package/amplify/data/function-handler.ts +40 -0
  5. package/amplify/data/resource.js +152 -62
  6. package/amplify/data/resource.ts +158 -62
  7. package/amplify/data/schema.js +2102 -1986
  8. package/amplify/data/schema.ts +199 -70
  9. package/amplify/functions/assignment-action/data-access.js +2 -0
  10. package/amplify/functions/assignment-action/data-access.ts +2 -0
  11. package/amplify/functions/assignment-action/handler.ts +1 -1
  12. package/amplify/functions/automation-view/handler.py +17 -0
  13. package/amplify/functions/automation-view/resource.js +24 -0
  14. package/amplify/functions/automation-view/resource.ts +34 -0
  15. package/amplify/functions/automation-view/schema.js +26 -0
  16. package/amplify/functions/automation-view/schema.ts +28 -0
  17. package/amplify/functions/automation-view/wire.js +56 -0
  18. package/amplify/functions/automation-view/wire.ts +76 -0
  19. package/amplify/functions/content-actions/handler.py +64 -3
  20. package/amplify/functions/content-actions/requirements.txt +1 -0
  21. package/amplify/functions/content-actions/resource.js +1 -1
  22. package/amplify/functions/content-actions/resource.ts +1 -1
  23. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +3 -3
  24. package/amplify/functions/editorial-checks/requirements.txt +1 -0
  25. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +7 -7
  26. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +7 -7
  27. package/amplify/functions/editorial-checks/schema.js +122 -0
  28. package/amplify/{plugins → functions}/editorial-checks/schema.ts +40 -12
  29. package/amplify/functions/editorial-checks/wire.js +53 -0
  30. package/amplify/functions/editorial-checks/wire.ts +61 -0
  31. package/amplify/functions/monitoring-view/handler.py +11 -0
  32. package/amplify/functions/monitoring-view/resource.js +24 -0
  33. package/amplify/functions/monitoring-view/resource.ts +34 -0
  34. package/amplify/functions/monitoring-view/schema.js +29 -0
  35. package/amplify/functions/monitoring-view/schema.ts +31 -0
  36. package/amplify/functions/monitoring-view/wire.js +64 -0
  37. package/amplify/functions/monitoring-view/wire.ts +81 -0
  38. package/amplify/functions/public-forms/core.js +247 -0
  39. package/amplify/functions/public-forms/core.ts +310 -0
  40. package/amplify/functions/public-forms/handler.ts +41 -0
  41. package/amplify/functions/public-forms/origins.js +35 -0
  42. package/amplify/functions/public-forms/origins.ts +43 -0
  43. package/amplify/functions/public-forms/resource.js +9 -0
  44. package/amplify/functions/public-forms/resource.ts +10 -0
  45. package/amplify/functions/public-forms/wire.js +25 -0
  46. package/amplify/functions/public-forms/wire.ts +32 -0
  47. package/amplify/functions/shared/editorial-context.ts +63 -4
  48. package/amplify/functions/shared/python-bundle.js +20 -14
  49. package/amplify/functions/shared/python-bundle.ts +25 -18
  50. package/amplify/plugins/newsletter/config.js +245 -0
  51. package/amplify/plugins/newsletter/config.ts +305 -0
  52. package/amplify/plugins/newsletter/function/handler.py +6 -0
  53. package/amplify/plugins/newsletter/function/requirements.txt +5 -0
  54. package/amplify/plugins/newsletter/ops/handler.py +12 -0
  55. package/amplify/plugins/newsletter/ops/resource.js +27 -0
  56. package/amplify/plugins/newsletter/ops/resource.ts +37 -0
  57. package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
  58. package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
  59. package/amplify/plugins/newsletter/schema.js +57 -0
  60. package/amplify/plugins/newsletter/schema.ts +62 -0
  61. package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
  62. package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
  63. package/amplify/plugins/newsletter/sending.js +140 -0
  64. package/amplify/plugins/newsletter/sending.ts +186 -0
  65. package/amplify/plugins/reader-steering/config.js +182 -0
  66. package/amplify/plugins/reader-steering/config.ts +290 -0
  67. package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
  68. package/amplify/plugins/reader-steering/function/handler.py +1567 -0
  69. package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
  70. package/amplify/plugins/reader-steering/function/moderation.py +316 -0
  71. package/amplify/plugins/reader-steering/function/proposals.py +458 -0
  72. package/amplify/plugins/reader-steering/function/resource.js +27 -0
  73. package/amplify/plugins/reader-steering/function/resource.ts +35 -0
  74. package/amplify/plugins/reader-steering/papyrus.plugin.js +186 -0
  75. package/amplify/plugins/reader-steering/papyrus.plugin.ts +212 -0
  76. package/amplify/plugins/reader-steering/schema.js +229 -0
  77. package/amplify/plugins/reader-steering/schema.ts +231 -0
  78. package/amplify/plugins/registry.js +4 -2
  79. package/amplify/plugins/registry.ts +4 -2
  80. package/amplify/plugins/signup/schema.js +2 -1
  81. package/amplify/plugins/signup/schema.ts +2 -1
  82. package/amplify/plugins/types.ts +5 -0
  83. package/amplify/plugins/wire.js +1 -0
  84. package/amplify/plugins/wire.ts +1 -0
  85. package/amplify/publication/projection-lease.js +30 -0
  86. package/amplify/publication/projection-lease.ts +39 -0
  87. package/amplify/publication/redirect-and-index.js +105 -0
  88. package/amplify/publication/redirect-rules.js +73 -0
  89. package/amplify/publication/redirect-rules.ts +93 -0
  90. package/amplify/publication/s3-projection.js +7 -1
  91. package/amplify/publication/s3-projection.ts +9 -1
  92. package/amplify/site-backend-config.js +19 -0
  93. package/amplify/site-backend-config.ts +45 -1
  94. package/amplify/site-backend-names.js +4 -2
  95. package/amplify/site-backend-names.ts +4 -2
  96. package/amplify/site-backend.js +46 -4
  97. package/amplify/site-backend.ts +49 -4
  98. package/app/api/auth/prospectus/sign-in/route.ts +4 -0
  99. package/app/api/newsletter/confirm/route.ts +13 -0
  100. package/app/api/newsletter/preferences/route.ts +13 -0
  101. package/app/api/newsletter/state/route.ts +9 -0
  102. package/app/api/newsletter/subscribe/route.ts +9 -0
  103. package/app/api/newsletter/unsubscribe/route.ts +13 -0
  104. package/app/api/newsroom/chat/[...path]/route.ts +17 -0
  105. package/app/api/newsroom/curation/context/route.ts +9 -0
  106. package/app/api/newsroom/curation/priming/route.ts +9 -0
  107. package/app/api/newsroom/curation/publication/route.ts +9 -0
  108. package/app/api/reader/comment/delete/route.ts +9 -0
  109. package/app/api/reader/comment/edit/route.ts +9 -0
  110. package/app/api/reader/comment/route.ts +9 -0
  111. package/app/api/reader/flag/route.ts +9 -0
  112. package/app/api/reader/handle/route.ts +9 -0
  113. package/app/api/reader/me/route.ts +9 -0
  114. package/app/api/reader/rating/route.ts +9 -0
  115. package/app/api/reader/report/route.ts +9 -0
  116. package/app/api/reader/retired/route.ts +9 -0
  117. package/app/api/reader/submit/route.ts +9 -0
  118. package/app/api/reader/vote/route.ts +9 -0
  119. package/app/layout.tsx +5 -1
  120. package/bin/papyrus-infra.mjs +20 -1
  121. package/components/automation-panel.tsx +49 -0
  122. package/components/automation-run-now.tsx +97 -0
  123. package/components/automation-view.tsx +195 -0
  124. package/components/editorial-checks-admin-panel.tsx +38 -13
  125. package/components/moderation-panel.tsx +248 -0
  126. package/components/monitoring-overview-view.tsx +414 -0
  127. package/components/monitoring-panel.tsx +60 -0
  128. package/components/news-desk-client-provider.tsx +18 -18
  129. package/components/news-desk-taxonomy-client.ts +28 -8
  130. package/components/newsletter-panel.tsx +474 -0
  131. package/components/newsletter-sections.tsx +163 -0
  132. package/components/newsroom-access-gate.tsx +16 -2
  133. package/components/newsroom-article-editor.tsx +11 -11
  134. package/components/newsroom-articles-view.tsx +6 -6
  135. package/components/newsroom-assignment-detail.tsx +6 -6
  136. package/components/newsroom-board-view.tsx +8 -3
  137. package/components/newsroom-curation-bootstrap.tsx +45 -0
  138. package/components/newsroom-ops-overview.tsx +26 -96
  139. package/components/newsroom-ops-shell.tsx +2 -2
  140. package/components/newsroom-organizer.css +33 -0
  141. package/components/newsroom-overview-skeleton.tsx +25 -0
  142. package/components/newsroom-references-view.tsx +192 -49
  143. package/components/notification-center.tsx +281 -0
  144. package/components/papyrus-console-conversation.css +14 -0
  145. package/components/papyrus-console-conversation.tsx +55 -0
  146. package/components/papyrus-console-shell.tsx +62 -420
  147. package/components/papyrus-private-chat-panel.tsx +134 -0
  148. package/components/publication-cyclotron-panel.tsx +15 -10
  149. package/components/reader-auth-state.ts +1 -1
  150. package/components/reference-add-entry.tsx +75 -0
  151. package/components/reference-curation-controls.tsx +31 -22
  152. package/components/reference-cyclotron-priming.tsx +58 -19
  153. package/components/reference-cyclotron-suggestion.tsx +10 -6
  154. package/components/reference-priming.tsx +14 -10
  155. package/components/reference-relevance-review.tsx +3 -3
  156. package/components/reference-section-suggestion.tsx +1 -1
  157. package/components/topic-steering-workspace.tsx +237 -158
  158. package/components/use-editorial-checks.ts +12 -10
  159. package/infra/amplify-app-shell.js +96 -16
  160. package/infra/amplify-app-shell.ts +108 -18
  161. package/infra/cloud-email-config.js +32 -0
  162. package/infra/cloud-email-config.ts +39 -0
  163. package/infra/cloud-email.js +57 -0
  164. package/infra/cloud-email.test.js +69 -0
  165. package/infra/cloud-email.test.ts +66 -0
  166. package/infra/cloud-email.ts +60 -0
  167. package/infra/index.js +1 -0
  168. package/infra/secure-string-parameter-handler.js +41 -0
  169. package/infra/secure-string-parameter-handler.ts +41 -0
  170. package/infra/secure-string-parameter.js +79 -0
  171. package/infra/secure-string-parameter.ts +91 -0
  172. package/infra/site-config.js +92 -6
  173. package/infra/site-config.ts +138 -6
  174. package/lib/automation-client.ts +54 -0
  175. package/lib/automation-fixture.ts +93 -0
  176. package/lib/automation-overview.ts +252 -0
  177. package/lib/category-repository.ts +14 -12
  178. package/lib/console-web-context.ts +1 -1
  179. package/lib/curation-routes.ts +345 -0
  180. package/lib/cyclotron-suggestions.ts +4 -4
  181. package/lib/editorial-checks-backend.ts +1 -1
  182. package/lib/editorial-checks-client.ts +19 -1
  183. package/lib/editorial-checks-demo.ts +2 -2
  184. package/lib/editorial-checks-settings.ts +3 -0
  185. package/lib/editorial-checks.ts +8 -11
  186. package/lib/editorial-preview.ts +5 -5
  187. package/lib/monitoring-client.ts +83 -0
  188. package/lib/monitoring-overview-fixture.ts +51 -0
  189. package/lib/monitoring-overview.ts +188 -0
  190. package/lib/news-desk-session.ts +12 -21
  191. package/lib/newsletter-api.ts +418 -0
  192. package/lib/newsletter-client.ts +46 -0
  193. package/lib/newsletter-fixture.ts +184 -0
  194. package/lib/newsletter-model.ts +202 -0
  195. package/lib/newsroom-demo-dashboard.ts +1 -1
  196. package/lib/newsroom-nav.ts +1 -1
  197. package/lib/newsroom-organizer-nav.ts +2 -1
  198. package/lib/papyrus-chat-admission.ts +65 -0
  199. package/lib/papyrus-hosted-chat.ts +217 -0
  200. package/lib/papyrus-private-chat-client.ts +81 -0
  201. package/lib/prospectus-session.ts +30 -14
  202. package/lib/prospectus-sign-in-routes.ts +106 -31
  203. package/lib/prospectus-site-identity.ts +46 -6
  204. package/lib/reader-api.ts +145 -0
  205. package/lib/reader-moderation.ts +220 -0
  206. package/lib/reader-retirement-route.ts +89 -0
  207. package/lib/reader-subject.md +29 -0
  208. package/lib/reader-subject.ts +53 -0
  209. package/lib/reference-review-details.ts +71 -0
  210. package/lib/reference-review-request.ts +14 -0
  211. package/lib/relevance-decisions.ts +42 -10
  212. package/lib/relevance-status.ts +2 -2
  213. package/lib/review-client.ts +120 -0
  214. package/lib/review-fixture.ts +45 -0
  215. package/lib/review-requests.ts +246 -0
  216. package/lib/semantic-graph.ts +2 -0
  217. package/lib/shared-chat-admission.ts +182 -0
  218. package/lib/site-brand-tenant.ts +11 -6
  219. package/lib/site-brand.ts +6 -3
  220. package/lib/site-origin-value.ts +50 -0
  221. package/lib/site-origin.ts +8 -0
  222. package/lib/use-reference-review-data.ts +44 -0
  223. package/middleware.ts +14 -3
  224. package/package.json +65 -3
  225. package/routes.manifest.json +227 -2
  226. package/src/with-papyrus.mjs +1 -1
  227. package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
  228. package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -71
  229. package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -87
  230. package/amplify/plugins/editorial-checks/schema.js +0 -94
@@ -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, historyComplete: true },
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,188 @@
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 MonitoringEmailSummary = {
36
+ receiptId: string; observedAt: string | null; status: string; subject: string | null;
37
+ envelopeSender: string | null; headerFrom: string | null; warnings: string[];
38
+ verdicts: Record<string, string>; linkCount: number; processedCount: number; unresolvedLinkCount: number;
39
+ reviewRequired: boolean; errorCode: string | null;
40
+ };
41
+
42
+ export type MonitoringEmailDetails = {
43
+ submission: MonitoringEmailSummary & { corpusId: string | null }; safeBodyText: string;
44
+ candidateLinks: { url: string; title: string; resolutionStatus?: "unresolved" }[]; sourceMaterialIsUntrusted: true;
45
+ } | { error: "forbidden" | "bad_request" } | { state: "unavailable" | "not_configured" };
46
+
47
+ export type MonitoringEmailArticleResolution = {
48
+ url: string; title: string; resolved: true;
49
+ } | { error: "forbidden" | "bad_request" | "not_resolvable" | "resolution_failed"; reason?: string } | { state: "unavailable" };
50
+
51
+ export type EmailReferenceImportItem = { candidateIndex: number; source_uri: string };
52
+ export type EmailReferenceImportOutcome = {
53
+ imported: number; references: { id: string; lineageId: string; curationStatus: string }[]; error?: string;
54
+ };
55
+
56
+ /** Split a complete editor selection into API work batches; the batch size is not a total link limit. */
57
+ export function emailReferenceBatches<T>(items: T[], batchSize = 20): T[][] {
58
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1) throw new Error("batchSize must be a positive integer");
59
+ const batches: T[][] = [];
60
+ for (let offset = 0; offset < items.length; offset += batchSize) {
61
+ batches.push(items.slice(offset, offset + batchSize));
62
+ }
63
+ return batches;
64
+ }
65
+
66
+ /** Async email actions may only update the receipt that is still open. */
67
+ export function isCurrentEmailSubmissionAction(activeReceiptId: string | null, actionReceiptId: string): boolean {
68
+ return activeReceiptId === actionReceiptId;
69
+ }
70
+
71
+ export function beginEmailSubmissionAction(inFlight: Set<string>, actionKey: string): boolean {
72
+ if (inFlight.has(actionKey)) return false;
73
+ inFlight.add(actionKey);
74
+ return true;
75
+ }
76
+
77
+ export function endEmailSubmissionAction(inFlight: Set<string>, actionKey: string): void {
78
+ inFlight.delete(actionKey);
79
+ }
80
+
81
+ export type MonitoringRun = {
82
+ runId: string;
83
+ startedAt: string;
84
+ status: string;
85
+ new: number;
86
+ revised: number;
87
+ unchanged: number;
88
+ itemCount: number | null;
89
+ errorCount: number;
90
+ errors: (string | null)[];
91
+ };
92
+
93
+ export type MonitoringRouterRun = {
94
+ runId: string;
95
+ startedAt: string;
96
+ mode: string;
97
+ status: string;
98
+ stoppedBy: string | null;
99
+ waitingForClassifier?: string[];
100
+ counts: Record<string, number>;
101
+ errorCount: number;
102
+ };
103
+
104
+ export type MonitoringFeed = {
105
+ id: string;
106
+ sourceUrl: string | null;
107
+ intervalSeconds: number | null;
108
+ limit: number | null;
109
+ enabled: boolean;
110
+ lastStatus: string | null;
111
+ lastFinishedAt: string | null;
112
+ nextDueAt: string | null;
113
+ };
114
+
115
+ export type MonitoringOverview = {
116
+ state: "ready" | "empty";
117
+ asOf: string;
118
+ notice?: "publication_not_routed";
119
+ scope: { mode: "publication" | "all"; publicationId: string | null; site: string | null; canViewAll: boolean };
120
+ feeds: MonitoringFeed[];
121
+ runs: MonitoringRun[];
122
+ routerRuns: MonitoringRouterRun[];
123
+ router: { mode: string; feedback: boolean; enabled: boolean; maxItemsPerRun: number | null } | null;
124
+ usage: {
125
+ day: string;
126
+ decisionsToday: number;
127
+ decisionsTotal: number;
128
+ historyComplete: boolean;
129
+ } | null;
130
+ items: MonitoringItem[];
131
+ emailSubmissions?: MonitoringEmailSummary[];
132
+ hiddenNotRelevant: number;
133
+ };
134
+
135
+ export type MonitoringResult =
136
+ | MonitoringOverview
137
+ | { state: "not_configured" }
138
+ | { state: "unavailable" }
139
+ | { error: "forbidden" | "bad_request" };
140
+
141
+ const ROUTING_WORDS: Record<string, string> = {
142
+ collected: "Collected, waiting for the router",
143
+ "dry-run-validated": "Checked in a dry run, not sent",
144
+ decided: "Judged, delivery not finished",
145
+ routed: "Routed",
146
+ refused: "Refused by the shared Cyclotron",
147
+ uncertain: "Uncertain, needs a person",
148
+ error: "Error",
149
+ };
150
+
151
+ export function routingLabel(routing: string): string {
152
+ return ROUTING_WORDS[routing] ?? routing;
153
+ }
154
+
155
+ /** Fixed-format UTC time so the page reads the same everywhere. */
156
+ export function formatUtc(iso: string | null | undefined): string {
157
+ if (!iso) return "never";
158
+ const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(iso);
159
+ return match ? `${match[1]} ${match[2]} UTC` : iso;
160
+ }
161
+
162
+ export function confidenceText(confidence: number | null): string {
163
+ return confidence === null ? "confidence not reported" : `${Math.round(confidence * 100)}% confidence`;
164
+ }
165
+
166
+ export function everyText(seconds: number | null): string {
167
+ if (!seconds) return "on a schedule";
168
+ return seconds % 3600 === 0 ? `every ${seconds / 3600} h` : seconds % 60 === 0 ? `every ${seconds / 60} min` : `every ${seconds} s`;
169
+ }
170
+
171
+ export function referenceHref(lineageId: string, demo?: boolean): string {
172
+ return getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
173
+ }
174
+
175
+ export function describeRelevance(entry: MonitoringRelevance): string {
176
+ if (entry.relevant === null) return "Not judged yet";
177
+ return entry.relevant ? "Relevant" : "Not relevant";
178
+ }
179
+
180
+ export function describeDelivery(entry: MonitoringRelevance): string {
181
+ switch (entry.delivery.state) {
182
+ case "delivered": return "In the References queue";
183
+ case "failed": return "Not in the References queue: delivery failed";
184
+ case "pending": return "Not in the References queue yet";
185
+ case "not_applicable": return "Nothing to deliver";
186
+ default: return "";
187
+ }
188
+ }
@@ -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,