@anthusai/papyrus 2.0.0-next.9 → 2.0.0-next.90

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 (217) 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 +151 -61
  6. package/amplify/data/resource.ts +157 -61
  7. package/amplify/data/schema.js +2096 -1980
  8. package/amplify/data/schema.ts +198 -69
  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 +61 -2
  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 +17 -0
  35. package/amplify/functions/monitoring-view/schema.ts +19 -0
  36. package/amplify/functions/monitoring-view/wire.js +37 -0
  37. package/amplify/functions/monitoring-view/wire.ts +47 -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.ts +18 -1
  93. package/amplify/site-backend-names.js +4 -2
  94. package/amplify/site-backend-names.ts +4 -2
  95. package/amplify/site-backend.js +45 -3
  96. package/amplify/site-backend.ts +51 -3
  97. package/app/api/auth/prospectus/sign-in/route.ts +4 -0
  98. package/app/api/newsletter/confirm/route.ts +13 -0
  99. package/app/api/newsletter/preferences/route.ts +13 -0
  100. package/app/api/newsletter/state/route.ts +9 -0
  101. package/app/api/newsletter/subscribe/route.ts +9 -0
  102. package/app/api/newsletter/unsubscribe/route.ts +13 -0
  103. package/app/api/newsroom/curation/context/route.ts +9 -0
  104. package/app/api/newsroom/curation/priming/route.ts +9 -0
  105. package/app/api/newsroom/curation/publication/route.ts +9 -0
  106. package/app/api/reader/comment/delete/route.ts +9 -0
  107. package/app/api/reader/comment/edit/route.ts +9 -0
  108. package/app/api/reader/comment/route.ts +9 -0
  109. package/app/api/reader/flag/route.ts +9 -0
  110. package/app/api/reader/handle/route.ts +9 -0
  111. package/app/api/reader/me/route.ts +9 -0
  112. package/app/api/reader/rating/route.ts +9 -0
  113. package/app/api/reader/report/route.ts +9 -0
  114. package/app/api/reader/retired/route.ts +9 -0
  115. package/app/api/reader/submit/route.ts +9 -0
  116. package/app/api/reader/vote/route.ts +9 -0
  117. package/bin/papyrus-infra.mjs +20 -1
  118. package/components/automation-panel.tsx +49 -0
  119. package/components/automation-run-now.tsx +97 -0
  120. package/components/automation-view.tsx +195 -0
  121. package/components/editorial-checks-admin-panel.tsx +38 -13
  122. package/components/moderation-panel.tsx +248 -0
  123. package/components/monitoring-overview-view.tsx +241 -0
  124. package/components/monitoring-panel.tsx +40 -0
  125. package/components/news-desk-client-provider.tsx +18 -18
  126. package/components/news-desk-taxonomy-client.ts +28 -8
  127. package/components/newsletter-panel.tsx +474 -0
  128. package/components/newsletter-sections.tsx +163 -0
  129. package/components/newsroom-access-gate.tsx +16 -2
  130. package/components/newsroom-article-editor.tsx +11 -11
  131. package/components/newsroom-articles-view.tsx +6 -6
  132. package/components/newsroom-assignment-detail.tsx +6 -6
  133. package/components/newsroom-board-view.tsx +8 -3
  134. package/components/newsroom-curation-bootstrap.tsx +45 -0
  135. package/components/newsroom-ops-overview.tsx +26 -96
  136. package/components/newsroom-ops-shell.tsx +2 -2
  137. package/components/newsroom-organizer.css +33 -0
  138. package/components/newsroom-overview-skeleton.tsx +25 -0
  139. package/components/newsroom-references-view.tsx +192 -49
  140. package/components/notification-center.tsx +277 -0
  141. package/components/publication-cyclotron-panel.tsx +15 -10
  142. package/components/reader-auth-state.ts +1 -1
  143. package/components/reference-add-entry.tsx +75 -0
  144. package/components/reference-curation-controls.tsx +31 -22
  145. package/components/reference-cyclotron-priming.tsx +58 -19
  146. package/components/reference-cyclotron-suggestion.tsx +10 -6
  147. package/components/reference-priming.tsx +14 -10
  148. package/components/reference-relevance-review.tsx +3 -3
  149. package/components/reference-section-suggestion.tsx +1 -1
  150. package/components/topic-steering-workspace.tsx +237 -158
  151. package/components/use-editorial-checks.ts +12 -10
  152. package/infra/amplify-app-shell.js +94 -16
  153. package/infra/amplify-app-shell.ts +106 -18
  154. package/infra/cloud-email-config.js +32 -0
  155. package/infra/cloud-email-config.ts +39 -0
  156. package/infra/cloud-email.js +57 -0
  157. package/infra/cloud-email.test.js +69 -0
  158. package/infra/cloud-email.test.ts +66 -0
  159. package/infra/cloud-email.ts +60 -0
  160. package/infra/index.js +1 -0
  161. package/infra/secure-string-parameter-handler.js +41 -0
  162. package/infra/secure-string-parameter-handler.ts +41 -0
  163. package/infra/secure-string-parameter.js +79 -0
  164. package/infra/secure-string-parameter.ts +91 -0
  165. package/infra/site-config.js +88 -6
  166. package/infra/site-config.ts +134 -6
  167. package/lib/automation-client.ts +54 -0
  168. package/lib/automation-fixture.ts +93 -0
  169. package/lib/automation-overview.ts +252 -0
  170. package/lib/category-repository.ts +14 -12
  171. package/lib/console-web-context.ts +1 -1
  172. package/lib/curation-routes.ts +345 -0
  173. package/lib/cyclotron-suggestions.ts +4 -4
  174. package/lib/editorial-checks-backend.ts +1 -1
  175. package/lib/editorial-checks-client.ts +19 -1
  176. package/lib/editorial-checks-demo.ts +2 -2
  177. package/lib/editorial-checks-settings.ts +3 -0
  178. package/lib/editorial-checks.ts +8 -11
  179. package/lib/editorial-preview.ts +5 -5
  180. package/lib/monitoring-client.ts +29 -0
  181. package/lib/monitoring-overview-fixture.ts +51 -0
  182. package/lib/monitoring-overview.ts +145 -0
  183. package/lib/news-desk-session.ts +12 -21
  184. package/lib/newsletter-api.ts +418 -0
  185. package/lib/newsletter-client.ts +46 -0
  186. package/lib/newsletter-fixture.ts +184 -0
  187. package/lib/newsletter-model.ts +202 -0
  188. package/lib/newsroom-demo-dashboard.ts +1 -1
  189. package/lib/newsroom-nav.ts +1 -1
  190. package/lib/newsroom-organizer-nav.ts +2 -1
  191. package/lib/prospectus-session.ts +15 -14
  192. package/lib/prospectus-sign-in-routes.ts +105 -20
  193. package/lib/prospectus-site-identity.ts +46 -6
  194. package/lib/reader-api.ts +145 -0
  195. package/lib/reader-moderation.ts +220 -0
  196. package/lib/reader-retirement-route.ts +89 -0
  197. package/lib/reader-subject.md +29 -0
  198. package/lib/reader-subject.ts +53 -0
  199. package/lib/reference-review-details.ts +71 -0
  200. package/lib/reference-review-request.ts +14 -0
  201. package/lib/relevance-decisions.ts +42 -10
  202. package/lib/relevance-status.ts +2 -2
  203. package/lib/review-client.ts +117 -0
  204. package/lib/review-fixture.ts +45 -0
  205. package/lib/review-requests.ts +244 -0
  206. package/lib/semantic-graph.ts +2 -0
  207. package/lib/site-brand-tenant.ts +11 -6
  208. package/lib/site-brand.ts +6 -3
  209. package/lib/site-origin.ts +67 -0
  210. package/lib/use-reference-review-data.ts +44 -0
  211. package/middleware.ts +14 -3
  212. package/package.json +56 -3
  213. package/routes.manifest.json +215 -2
  214. package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
  215. package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -71
  216. package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -87
  217. package/amplify/plugins/editorial-checks/schema.js +0 -94
@@ -0,0 +1,163 @@
1
+ import {
2
+ countsText,
3
+ formatInZone,
4
+ formatUtc,
5
+ issueEditorHref,
6
+ statusLabel,
7
+ statusMark,
8
+ type NewsletterDelivery,
9
+ type NewsletterEvent,
10
+ type NewsletterIssue,
11
+ type NewsletterOverview,
12
+ type NewsletterSubscriber,
13
+ } from "../lib/newsletter-model";
14
+
15
+ /** Presentation only: the panel fetches and holds state, these render what they are given, so every state is testable from fixtures. */
16
+
17
+ export function ListsCard({ overview }: { overview: NewsletterOverview }) {
18
+ const { totals } = overview;
19
+ return (
20
+ <article className="news-desk-doctrine-card" data-newsletter-section="lists">
21
+ <div className="news-desk-doctrine-card__header">
22
+ <div><h3>Lists</h3><span>Subscribers per list. A person on two lists counts on each.</span></div>
23
+ </div>
24
+ {overview.lists.length ? (
25
+ <dl className="news-desk-monitoring__usage">
26
+ {overview.lists.map((list) => (
27
+ <div key={list.id} data-newsletter-list={list.id}>
28
+ <dt>{list.name} ({list.kind === "roundup" ? "roundup" : list.kind === "alert" ? "alerts" : list.kind})</dt>
29
+ <dd>{list.confirmed}</dd>
30
+ <span className="news-desk-newsletter__quiet">confirmed &middot; {list.pending} pending &middot; {list.unsubscribed} unsubscribed</span>
31
+ </div>
32
+ ))}
33
+ </dl>
34
+ ) : <p>No list is configured.</p>}
35
+ <p className="news-desk-delegation-note" data-newsletter-totals>
36
+ Across the site: {totals.confirmed} confirmed, {totals.pending} waiting to confirm, {totals.unsubscribed} unsubscribed.
37
+ {" "}{overview.suppressed} {overview.suppressed === 1 ? "address is" : "addresses are"} suppressed after a bounce or complaint.
38
+ </p>
39
+ </article>
40
+ );
41
+ }
42
+
43
+ export function SiteStatusCard({ paused, isAdmin, busy, onResume }: { paused: NewsletterOverview["paused"]; isAdmin: boolean; busy: boolean; onResume: () => void }) {
44
+ return (
45
+ <article className="news-desk-doctrine-card" data-newsletter-section="site-status">
46
+ <div className="news-desk-doctrine-card__header">
47
+ <div><h3>Site status</h3><span>Sending turns itself off when bounces or complaints run high</span></div>
48
+ </div>
49
+ {paused.paused ? (
50
+ <div className="category-steering-alert" role="alert" data-newsletter-paused>
51
+ <strong><span aria-hidden="true">■ </span>Sending is paused.</strong>{" "}
52
+ {paused.reason ? <>Reason: {paused.reason}. </> : null}
53
+ {paused.pausedAt ? <>Paused {formatUtc(paused.pausedAt)}. </> : null}
54
+ Issues due now are blocked until sending resumes; approve a blocked issue again afterwards.
55
+ <div className="news-desk-doctrine-card__actions">
56
+ {isAdmin ? (
57
+ <button type="button" disabled={busy} data-newsletter-resume onClick={onResume}>Resume sending</button>
58
+ ) : <span>An admin can resume sending.</span>}
59
+ </div>
60
+ </div>
61
+ ) : (
62
+ <p data-newsletter-running><span aria-hidden="true">✓ </span>Sending is on.{paused.resumedAt ? ` Resumed ${formatUtc(paused.resumedAt)}.` : ""}</p>
63
+ )}
64
+ </article>
65
+ );
66
+ }
67
+
68
+ /** What an issue row says about the issue; the buttons beside it belong to the panel. */
69
+ export function IssueSummary({ issue, zone, demo }: { issue: NewsletterIssue; zone: string; demo: boolean }) {
70
+ return (
71
+ <>
72
+ <header>
73
+ <strong>{issue.subject || "(no subject)"}</strong>
74
+ <span data-newsletter-status={issue.status}><span aria-hidden="true">{statusMark(issue.status)} </span>{statusLabel(issue.status)}</span>
75
+ </header>
76
+ <p>{issue.listName ?? issue.listId}{issue.kind ? ` · ${issue.kind}` : ""}</p>
77
+ {issue.sendAt ? (
78
+ <p data-newsletter-send-at>
79
+ {issue.status === "sent" || issue.status === "sending" ? "Scheduled for " : "Send at "}
80
+ <time dateTime={issue.sendAt} title={formatUtc(issue.sendAt)}>{formatInZone(issue.sendAt, zone)}</time>
81
+ {issue.approvedBy ? ` · approved by ${issue.approvedBy}` : ""}
82
+ </p>
83
+ ) : null}
84
+ <p data-newsletter-counts>{countsText(issue.counts ?? {})}</p>
85
+ {issue.status === "blocked" ? (
86
+ <p className="category-steering-alert" data-newsletter-blocked>
87
+ Blocked: {issue.note ?? "see the issue"}. Fix the cause, then approve it again.
88
+ </p>
89
+ ) : issue.note ? <p data-newsletter-note>Note: {issue.note}</p> : null}
90
+ {issue.editedSinceApproval ? (
91
+ <p className="category-steering-alert" data-newsletter-edited>
92
+ Edited since approval. When it comes due it will go back to draft and send nothing. Approve it again to send this version.
93
+ </p>
94
+ ) : null}
95
+ <p className="news-desk-newsletter__links">
96
+ {issue.itemId ? <a href={issueEditorHref(issue.itemId, demo)} data-newsletter-editor-link>Open in the editor</a> : <span>No item</span>}
97
+ {issue.archiveUrl ? <> &middot; <a href={issue.archiveUrl} target="_blank" rel="noreferrer noopener" data-newsletter-archive-link>Archive page</a></> : null}
98
+ </p>
99
+ </>
100
+ );
101
+ }
102
+
103
+ export function DeliveriesCard({
104
+ issue, rows, events, hasMore, loading, isAdmin, onMore,
105
+ }: {
106
+ issue: NewsletterIssue; rows: NewsletterDelivery[]; events: NewsletterEvent[]; hasMore: boolean; loading: boolean; isAdmin: boolean; onMore: () => void;
107
+ }) {
108
+ return (
109
+ <article className="news-desk-doctrine-card" data-newsletter-section="deliveries" data-newsletter-deliveries-for={issue.issueId}>
110
+ <div className="news-desk-doctrine-card__header">
111
+ <div><h3>Delivery history</h3><span>{issue.subject} &middot; {countsText(issue.counts ?? {})}</span></div>
112
+ </div>
113
+ {!isAdmin ? <p className="news-desk-delegation-note">Addresses are masked for editors.</p> : null}
114
+ {rows.length ? (
115
+ <table className="news-desk-newsletter__table">
116
+ <thead><tr><th scope="col">Address</th><th scope="col">Result</th><th scope="col">Queued (UTC)</th><th scope="col">Last event (UTC)</th></tr></thead>
117
+ <tbody>
118
+ {rows.map((row, index) => (
119
+ <tr key={`${row.address}-${index}`} data-newsletter-delivery={row.status}>
120
+ <td>{row.address}</td>
121
+ <td>{row.status}{row.error ? ` (${row.error})` : ""}</td>
122
+ <td>{formatUtc(row.at)}</td>
123
+ <td>{row.eventAt ? formatUtc(row.eventAt) : "none yet"}</td>
124
+ </tr>
125
+ ))}
126
+ </tbody>
127
+ </table>
128
+ ) : <p>{loading ? "Loading deliveries..." : "Nothing has been sent for this issue."}</p>}
129
+ {hasMore ? <div className="news-desk-doctrine-card__actions"><button type="button" disabled={loading} onClick={onMore}>Load more deliveries</button></div> : null}
130
+ <h4>Recent events for this issue</h4>
131
+ {events.length ? (
132
+ <ul className="news-desk-monitoring__list" data-newsletter-events>
133
+ {events.map((event, index) => (
134
+ <li key={`${event.at}-${index}`}>
135
+ <strong>{event.type}{event.detail ? ` (${event.detail})` : ""}</strong>
136
+ <span>{event.list ?? ""}</span>
137
+ <span>{formatUtc(event.at)}</span>
138
+ </li>
139
+ ))}
140
+ </ul>
141
+ ) : <p>No recent events for this issue. Events are kept for 90 days and the panel shows the latest 100 for the whole site.</p>}
142
+ </article>
143
+ );
144
+ }
145
+
146
+ export function SubscribersTable({ rows, zone }: { rows: NewsletterSubscriber[]; zone: string }) {
147
+ return (
148
+ <table className="news-desk-newsletter__table" data-newsletter-subscribers>
149
+ <thead><tr><th scope="col">Address</th><th scope="col">Status</th><th scope="col">Lists</th><th scope="col">Signed up</th><th scope="col">Confirmed</th></tr></thead>
150
+ <tbody>
151
+ {rows.map((row) => (
152
+ <tr key={row.email} data-newsletter-subscriber={row.status}>
153
+ <td>{row.email}</td>
154
+ <td>{row.status}</td>
155
+ <td>{row.lists.length ? row.lists.map((entry) => `${entry.id} (${entry.status})`).join(", ") : "none"}</td>
156
+ <td>{formatInZone(row.createdAt, zone)}</td>
157
+ <td>{row.confirmedAt ? formatInZone(row.confirmedAt, zone) : "not yet"}</td>
158
+ </tr>
159
+ ))}
160
+ </tbody>
161
+ </table>
162
+ );
163
+ }
@@ -6,11 +6,13 @@ import { SITE_BRAND } from "../lib/site-brand";
6
6
  import type { NewsDeskShellState } from "../lib/news-desk-session";
7
7
  import { cn } from "../lib/utils";
8
8
  import { NewsroomOpsShell } from "./newsroom-ops-shell";
9
+ import { NewsroomOverviewSkeleton } from "./newsroom-overview-skeleton";
9
10
  import { ReaderAuthControl } from "./reader-auth-control";
10
11
  import { buttonVariants } from "./ui/button";
11
12
 
12
13
  export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell: NewsDeskShellState | null; showSectionTabs?: boolean }) {
13
14
  const accessPhase = shell?.phase ?? "checkingAccess";
15
+ const pending = accessPhase === "checkingAccess" || accessPhase === "loadingDesk";
14
16
 
15
17
  return (
16
18
  <NewsroomOpsShell
@@ -21,9 +23,20 @@ export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell:
21
23
  headerActions={(
22
24
  <Link className={cn(buttonVariants({ variant: "ghost", size: "sm" }))} href="/settings">Settings</Link>
23
25
  )}
24
- pageTitle="Newsroom"
26
+ pageTitle={showSectionTabs ? "Notification Center" : "Newsroom"}
25
27
  showNavigation={showSectionTabs}
26
28
  >
29
+ {pending ? (showSectionTabs ? <NewsroomOverviewSkeleton /> : (
30
+ <div aria-label="Loading newsroom" className="space-y-4" data-newsroom-page-skeleton role="status">
31
+ <div className="h-6 w-48 rounded bg-muted" />
32
+ <div className="h-4 w-3/4 rounded bg-muted" />
33
+ <div className="grid gap-3 sm:grid-cols-2">
34
+ {Array.from({ length: 4 }, (_, index) => <div className="h-32 rounded-xl border bg-card p-4" key={index}>
35
+ <div className="h-4 w-2/3 rounded bg-muted" /><div className="mt-4 h-3 w-1/2 rounded bg-muted" />
36
+ </div>)}
37
+ </div>
38
+ </div>
39
+ )) : (
27
40
  <section aria-live="polite" data-news-desk-access={accessPhase} data-news-desk-access-phase={accessPhase}>
28
41
  <div className="space-y-3 rounded-xl border border-border bg-card p-6">
29
42
  <p className="m-0 text-[0.7rem] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Access</p>
@@ -38,12 +51,13 @@ export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell:
38
51
  ) : null}
39
52
  </div>
40
53
  </section>
54
+ )}
41
55
  </NewsroomOpsShell>
42
56
  );
43
57
  }
44
58
 
45
59
  function formatAccessTitle(state: NewsDeskShellState | null): string {
46
- if (!state || state.phase === "checkingAccess") return "Checking Desk Credentials";
60
+ if (!state || state.phase === "checkingAccess") return "Checking Credentials";
47
61
  if (state.phase === "loadingDesk") return "Loading Newsroom Records";
48
62
  if (state.phase === "forbidden") return "Editor Role Required";
49
63
  if (state.phase === "error") return "Newsroom Unavailable";
@@ -48,11 +48,9 @@ import {
48
48
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
49
49
  import { SITE_BRAND } from "../lib/site-brand";
50
50
  import { cn } from "../lib/utils";
51
- import amplifyOutputs from "papyrus-amplify-outputs";
52
- import { applyReplacement, editorialChecksEnabled, inlineFindings, visibleFindings } from "../lib/editorial-checks";
51
+ import { applyReplacement, inlineFindings, visibleFindings } from "../lib/editorial-checks";
53
52
  import { graphqlEditorialBackend } from "../lib/editorial-checks-client";
54
53
  import { demoEditorialBackend } from "../lib/editorial-checks-demo";
55
- import type { PluginOutputs } from "../lib/plugin-settings";
56
54
  import type { FindingActions } from "./editorial-finding-card";
57
55
  import { EditorialFindingsPanel } from "./editorial-findings-panel";
58
56
  import { EditorialPreviewPopover, type PreviewPopoverTarget } from "./editorial-preview-popover";
@@ -219,7 +217,7 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
219
217
  const frontMatterYaml = editor?.frontMatterYaml ?? null;
220
218
  const bodyMarkus = editor?.bodyMarkus ?? null;
221
219
  const editorialBackend = useMemo(
222
- () => (demo ? demoEditorialBackend : !previewMode && editorialChecksEnabled(amplifyOutputs as PluginOutputs) ? graphqlEditorialBackend : null),
220
+ () => (demo ? demoEditorialBackend : !previewMode ? graphqlEditorialBackend : null),
223
221
  [demo, previewMode],
224
222
  );
225
223
  const editorial = useEditorialChecks({
@@ -515,8 +513,10 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
515
513
  }
516
514
  if (!editor) {
517
515
  return (
518
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
519
- <Spinner /> Loading article…
516
+ <div aria-label="Loading article" className="space-y-5" role="status">
517
+ <div className="h-8 w-2/3 rounded bg-muted" />
518
+ <div className="grid gap-4 sm:grid-cols-2"><div className="h-12 rounded bg-muted" /><div className="h-12 rounded bg-muted" /></div>
519
+ <div className="h-64 rounded-xl border bg-card p-4"><div className="h-4 w-5/6 rounded bg-muted" /><div className="mt-3 h-4 w-3/4 rounded bg-muted" /></div>
520
520
  </div>
521
521
  );
522
522
  }
@@ -670,21 +670,21 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
670
670
  </Select>
671
671
  </label>
672
672
  <label className="space-y-1 text-sm font-medium">
673
- <span>Desk</span>
673
+ <span>Section</span>
674
674
  <Select
675
675
  onValueChange={(value) => {
676
676
  if (typeof value === "string") patchEditor({ section: value === NO_DESK_VALUE ? "" : value });
677
677
  }}
678
678
  value={deskOptions.some(option => option.value === editor.section) ? editor.section : NO_DESK_VALUE}
679
679
  >
680
- <SelectTrigger aria-label="Desk" className="w-full" data-newsroom-article-section>
681
- <SelectValue>{deskOptions.find(option => option.value === editor.section)?.label ?? "No desk"}</SelectValue>
680
+ <SelectTrigger aria-label="Section" className="w-full" data-newsroom-article-section>
681
+ <SelectValue>{deskOptions.find(option => option.value === editor.section)?.label ?? "No section"}</SelectValue>
682
682
  </SelectTrigger>
683
683
  <SelectContent>
684
- <SelectItem value={NO_DESK_VALUE}>No desk</SelectItem>
684
+ <SelectItem value={NO_DESK_VALUE}>No section</SelectItem>
685
685
  {(["core", "rotating"] as const).map((kind) => deskOptions.some(option => option.kind === kind) ? (
686
686
  <SelectGroup key={kind}>
687
- <SelectLabel>{kind === "core" ? "Core desks" : "Rotating desks"}</SelectLabel>
687
+ <SelectLabel>{kind === "core" ? "Core sections" : "Rotating sections"}</SelectLabel>
688
688
  {deskOptions.filter(option => option.kind === kind).map((option) => (
689
689
  <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
690
690
  ))}
@@ -5,7 +5,6 @@ import Link from "next/link";
5
5
  import { useEffect, useMemo, useState } from "react";
6
6
  import { Badge } from "./ui/badge";
7
7
  import { buttonVariants } from "./ui/button";
8
- import { Spinner } from "./ui/spinner";
9
8
  import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
10
9
  import { SITE_BRAND } from "../lib/site-brand";
11
10
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
@@ -89,15 +88,16 @@ export function NewsroomArticlesView({ demo }: { demo: boolean }) {
89
88
  </TabsList>
90
89
  </Tabs>
91
90
  {loadError ? <NewsroomOpsStatusBanner tone="error">{loadError}</NewsroomOpsStatusBanner> : null}
92
- {rows === null && !loadError ? (
93
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
94
- <Spinner /> Loading articles…
95
- </div>
96
- ) : null}
97
91
  {rows !== null && visibleRows.length === 0 ? (
98
92
  <p className="text-sm text-muted-foreground">Nothing here yet.</p>
99
93
  ) : null}
100
94
  <ul className="divide-y divide-border rounded-xl border border-border bg-card" data-newsroom-articles-list>
95
+ {rows === null && !loadError ? Array.from({ length: 5 }, (_, index) => (
96
+ <li aria-hidden="true" className="flex items-center justify-between px-4 py-3" key={index}>
97
+ <span className="w-2/3"><span className="block h-4 w-3/4 rounded bg-muted" /><span className="mt-2 block h-3 w-1/2 rounded bg-muted" /></span>
98
+ <span className="h-5 w-16 rounded bg-muted" />
99
+ </li>
100
+ )) : null}
101
101
  {visibleRows.map((row) => (
102
102
  <li key={row.id}>
103
103
  <Link
@@ -36,7 +36,7 @@ export function NewsroomAssignmentDetail({ assignment, deskOptions = [], onDeskC
36
36
  const changeDesk = (value: string) => {
37
37
  if (!onDeskChange) return;
38
38
  setSavingDesk(true); setDeskError(null);
39
- onDeskChange(value === NO_DESK_VALUE ? null : value).catch((reason: unknown) => setDeskError(reason instanceof Error ? reason.message : "Could not save the desk.")).finally(() => setSavingDesk(false));
39
+ onDeskChange(value === NO_DESK_VALUE ? null : value).catch((reason: unknown) => setDeskError(reason instanceof Error ? reason.message : "Could not save the section.")).finally(() => setSavingDesk(false));
40
40
  };
41
41
  const facts: DetailField[] = [
42
42
  { label: "Status", value: assignment.status },
@@ -58,14 +58,14 @@ export function NewsroomAssignmentDetail({ assignment, deskOptions = [], onDeskC
58
58
  {facts.map(field => <div key={field.label}><dt>{field.label}</dt><dd>{field.value}</dd></div>)}
59
59
  </dl>
60
60
  {onDeskChange && <label className="newsroom-assignment-detail-desk">
61
- <span>Desk</span>
61
+ <span>Section</span>
62
62
  <Select onValueChange={(value) => { if (typeof value === "string") changeDesk(value); }} value={deskChoices.some(option => option.value === currentDesk) ? currentDesk : NO_DESK_VALUE} disabled={savingDesk}>
63
- <SelectTrigger aria-label="Desk" className="w-full" data-newsroom-assignment-desk>
64
- <SelectValue>{deskChoices.find(option => option.value === currentDesk)?.label ?? "No desk"}</SelectValue>
63
+ <SelectTrigger aria-label="Section" className="w-full" data-newsroom-assignment-desk>
64
+ <SelectValue>{deskChoices.find(option => option.value === currentDesk)?.label ?? "No section"}</SelectValue>
65
65
  </SelectTrigger>
66
66
  <SelectContent>
67
- <SelectItem value={NO_DESK_VALUE}>No desk</SelectItem>
68
- {(["core", "rotating"] as const).map(kind => deskChoices.some(option => option.kind === kind) ? <SelectGroup key={kind}><SelectLabel>{kind === "core" ? "Core desks" : "Rotating desks"}</SelectLabel>{deskChoices.filter(option => option.kind === kind).map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectGroup> : null)}
67
+ <SelectItem value={NO_DESK_VALUE}>No section</SelectItem>
68
+ {(["core", "rotating"] as const).map(kind => deskChoices.some(option => option.kind === kind) ? <SelectGroup key={kind}><SelectLabel>{kind === "core" ? "Core sections" : "Rotating sections"}</SelectLabel>{deskChoices.filter(option => option.kind === kind).map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectGroup> : null)}
69
69
  </SelectContent>
70
70
  </Select>
71
71
  {currentDesk && !deskChoices.some(option => option.value === currentDesk) ? <small className="newsroom-desk-legacy-value">Old section value: {currentDesk}</small> : null}
@@ -160,7 +160,7 @@ export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardS
160
160
  if (!allowed) return <NewsDeskAccessGate shell={session?.shell ?? null} />;
161
161
  return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : subject === "desk" ? "desks" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
162
162
  headerActions={<>
163
- <span role="status" className="organizer-board-note">{loading ? "Loading records…" : `${issues.length} records loaded.`}</span>
163
+ {!loading ? <span role="status" className="organizer-board-note">{issues.length} records loaded.</span> : null}
164
164
  <div role="group" aria-label="Record presentation" className="organizer-board-view-toggle"><button type="button" className="organizer-header-button" aria-pressed={view === "board"} onClick={() => setView("board")}>Board</button><button type="button" className="organizer-header-button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button></div>
165
165
  {showsContentItems && (
166
166
  <Link className="organizer-header-button" data-newsroom-article-new href={getNewsroomNavHref(newsroomHref("articles", "new"))}>
@@ -174,11 +174,16 @@ export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardS
174
174
  {error && <NewsroomOpsStatusBanner tone="error">{error}</NewsroomOpsStatusBanner>}
175
175
  {subject === "desk" && <IssueTypeFilter config={config} value={typeFilter} onChange={setTypeFilter} name="Record type" className="organizer-board-type-filter" />}
176
176
  <div className="organizer-board-body" data-detail-open={Boolean(issue)} ref={boardBodyRef} onScrollCapture={handleBoardScroll}>
177
- <IssueCollection issues={issues} config={config} query={{ type: subject === "desk" ? issueTypeFromFilter(typeFilter) : boardType }} view={view}
177
+ {loading && !issues.length ? <div aria-label="Loading board records" className="grid w-full grid-cols-1 gap-3 sm:grid-cols-3" role="status">
178
+ {Array.from({ length: 3 }, (_, column) => <div className="min-h-64 space-y-3 rounded-xl border bg-card p-4" key={column}>
179
+ <div className="h-4 w-1/2 rounded bg-muted" />
180
+ {Array.from({ length: 2 }, (_, row) => <div className="h-24 rounded-lg border bg-background p-3" key={row}><div className="h-4 w-3/4 rounded bg-muted" /><div className="mt-3 h-3 w-1/2 rounded bg-muted" /></div>)}
181
+ </div>)}
182
+ </div> : <IssueCollection issues={issues} config={config} query={{ type: subject === "desk" ? issueTypeFromFilter(typeFilter) : boardType }} view={view}
178
183
  motion={lowMotion ? STATIC_MOTION : undefined}
179
184
  collapsedColumns={columnState.collapsedColumns} onToggleCollapse={columnState.toggleColumn} initialScroll="start"
180
185
  selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel={pageTitle}
181
- emptyMessage={loading ? "Loading records…" : error ? "Records could not be loaded." : "No records in this selection."} />
186
+ emptyMessage={error ? "Records could not be loaded." : "No records in this selection."} />}
182
187
  {issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[]}>
183
188
  {issue.kind === "contentItem"
184
189
  ? <>
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import {useEffect,useState,type ReactNode} from 'react';
4
+ import {loadProspectusAccountState} from '../lib/prospectus-account-client';
5
+ import type {CurationBootstrap} from '../lib/curation-client';
6
+ import {NewsroomCurationProvider} from './newsroom-curation-provider';
7
+
8
+ type Fetcher=(input:string,init?:RequestInit)=>Promise<Response>;
9
+ const CONTEXT_PATH='/api/newsroom/curation/context';
10
+
11
+ function validBootstrap(value:unknown):value is CurationBootstrap{
12
+ const body=value as Partial<CurationBootstrap>|null;
13
+ const scope=body?.scope;
14
+ return !!scope&&[scope.accountId,scope.publicationId,scope.bindingRevision].every(item=>typeof item==='string'&&item.trim()!=='')
15
+ &&(body!.mode==='embedded'||body!.mode==='remote')&&Array.isArray(body!.capabilities);
16
+ }
17
+
18
+ /**
19
+ * Ask the server for the curation bootstrap, or null. Only a signed-in person with a selected
20
+ * account causes a context read; every other outcome (no Prospectus, signed out, no selection,
21
+ * any refusal or failure) is null, so the released CMS is unchanged.
22
+ */
23
+ export async function loadCurationBootstrap(returnPath:string,fetcher:Fetcher=fetch):Promise<CurationBootstrap|null>{
24
+ try{
25
+ const account=await loadProspectusAccountState(returnPath,fetcher);
26
+ if(account.status!=='signedIn'||!account.selectedAccountId)return null;
27
+ const response=await fetcher(CONTEXT_PATH,{credentials:'same-origin',cache:'no-store'});
28
+ if(!response.ok)return null;
29
+ const body=await response.json().catch(()=>null);
30
+ if(!validBootstrap(body)||body.scope.accountId!==account.selectedAccountId)return null;
31
+ return body;
32
+ }catch{return null;}
33
+ }
34
+
35
+ /** Mounts the curation provider for the References tab. Fails closed to no provider content. */
36
+ export function NewsroomCurationBootstrap({enabled=true,children}:{enabled?:boolean;children:ReactNode}){
37
+ const [bootstrap,setBootstrap]=useState<CurationBootstrap|null>(null);
38
+ useEffect(()=>{
39
+ if(!enabled)return;
40
+ let current=true;
41
+ void loadCurationBootstrap(window.location.pathname).then(value=>{if(current)setBootstrap(value);});
42
+ return()=>{current=false;setBootstrap(null);};
43
+ },[enabled]);
44
+ return <NewsroomCurationProvider bootstrap={bootstrap}>{children}</NewsroomCurationProvider>;
45
+ }
@@ -1,24 +1,13 @@
1
1
  "use client";
2
2
 
3
- import {
4
- BookOpenIcon,
5
- ClipboardListIcon,
6
- FileTextIcon,
7
- LayersIcon,
8
- MessageSquareIcon,
9
- NetworkIcon,
10
- SearchIcon,
11
- SettingsIcon,
12
- } from "lucide-react";
13
3
  import Link from "next/link";
14
4
  import { usePathname } from "next/navigation";
15
5
  import { useMemo } from "react";
16
6
  import { Badge } from "./ui/badge";
17
7
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
18
- import type { AssignmentRecord, MessageRecord, NewsroomSectionRecord, ReferenceRecord } from "../lib/category-repository";
19
- import { getNewsroomNavHref, NEWSROOM_OPS_NAV, type NewsroomNavTabId } from "../lib/newsroom-nav";
8
+ import type { AssignmentRecord, NewsroomSectionRecord, ReferenceRecord } from "../lib/category-repository";
9
+ import { getNewsroomNavHref } from "../lib/newsroom-nav";
20
10
  import {
21
- countReferencesByStatus,
22
11
  filterReferencesByStatus,
23
12
  formatReferenceListDate,
24
13
  referenceDisplayTitle,
@@ -33,57 +22,33 @@ import {
33
22
  sortNewsroomSections,
34
23
  } from "../lib/newsroom-sections";
35
24
  import { newsroomInboxRowClassName } from "../lib/newsroom-list-selection";
25
+ import type { RelevanceDecisionView } from "../lib/relevance-decisions";
26
+ import { NotificationCenter } from "./notification-center";
36
27
 
37
28
  type NewsroomOpsOverviewProps = {
38
29
  assignments: AssignmentRecord[];
30
+ actorLabel?: string;
31
+ decisions?: ReadonlyMap<string, RelevanceDecisionView>;
39
32
  demo?: boolean;
40
- messages: MessageRecord[];
33
+ humanCuratedReferenceLineageIds?: ReadonlySet<string>;
41
34
  newsroomSections: NewsroomSectionRecord[];
42
35
  references: ReferenceRecord[];
43
36
  };
44
37
 
45
- type DestinationKey = Exclude<NewsroomNavTabId, "overview" | "desks">;
46
-
47
- const DESTINATION_ICONS: Record<DestinationKey, typeof BookOpenIcon> = {
48
- articles: FileTextIcon,
49
- assignments: ClipboardListIcon,
50
- references: BookOpenIcon,
51
- messages: MessageSquareIcon,
52
- topics: LayersIcon,
53
- concepts: NetworkIcon,
54
- administration: SettingsIcon,
55
- search: SearchIcon,
56
- };
57
-
58
- const DESTINATIONS = NEWSROOM_OPS_NAV.filter((item) => item.id !== "overview").map((item) => ({
59
- key: item.id as DestinationKey,
60
- label: item.label,
61
- detail: item.detail,
62
- href: item.href,
63
- icon: DESTINATION_ICONS[item.id as DestinationKey],
64
- }));
65
-
66
- function openAssignmentCount(assignments: AssignmentRecord[]): number {
67
- return assignments.filter((assignment) => {
68
- const status = assignment.status?.trim().toLowerCase() ?? "";
69
- return status === "open" || status === "claimed" || status === "pending";
70
- }).length;
71
- }
72
-
73
- function recentMessageCount(messages: MessageRecord[]): number {
74
- return messages.filter((message) => message.status !== "archived").length;
75
- }
38
+ const NO_DECISIONS: ReadonlyMap<string, RelevanceDecisionView> = new Map();
39
+ const NO_IDS: ReadonlySet<string> = new Set();
76
40
 
77
41
  export function NewsroomOpsOverview({
42
+ actorLabel = "",
78
43
  assignments,
44
+ decisions = NO_DECISIONS,
79
45
  demo = false,
80
- messages,
46
+ humanCuratedReferenceLineageIds = NO_IDS,
81
47
  newsroomSections,
82
48
  references,
83
49
  }: NewsroomOpsOverviewProps) {
84
50
  const pathname = usePathname();
85
51
  const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
86
- const referenceCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
87
52
  const pendingReferences = useMemo(
88
53
  () => filterReferencesByStatus(canonicalReferences, "pending").slice(0, 5),
89
54
  [canonicalReferences],
@@ -104,12 +69,6 @@ export function NewsroomOpsOverview({
104
69
  const coreDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "core"), [desks]);
105
70
  const rotatingDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "rotating"), [desks]);
106
71
 
107
- const destinationCounts: Partial<Record<DestinationKey, number | null>> = {
108
- references: referenceCounts.pending ?? 0,
109
- assignments: openAssignmentCount(assignments),
110
- messages: recentMessageCount(messages),
111
- };
112
-
113
72
  return (
114
73
  <div
115
74
  className="space-y-8 font-sans"
@@ -117,44 +76,15 @@ export function NewsroomOpsOverview({
117
76
  data-newsroom-ops-home="true"
118
77
  >
119
78
  <div className="space-y-8" data-newsroom-overview-feeds>
120
- <section aria-label="Destinations" className="space-y-3">
121
- <h2 className="text-sm font-medium text-muted-foreground">Go to</h2>
122
- <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
123
- {DESTINATIONS.map((meta) => {
124
- const key = meta.key;
125
- const Icon = meta.icon;
126
- const count = destinationCounts[key];
127
- const href = getNewsroomNavHref(meta.href, demo);
128
- return (
129
- <Link
130
- className="group block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
131
- data-newsroom-ops-destination={key}
132
- data-newsroom-overview-section={key}
133
- href={href}
134
- key={key}
135
- >
136
- <Card className="h-full transition-colors group-hover:border-primary/40">
137
- <CardHeader className="space-y-3 p-4">
138
- <div className="flex items-center justify-between gap-2">
139
- <span className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
140
- <Icon className="size-4" aria-hidden="true" />
141
- </span>
142
- {count != null && count > 0 ? (
143
- <Badge variant="default">{count}</Badge>
144
- ) : null}
145
- </div>
146
- <div className="space-y-0.5">
147
- <CardTitle className="text-base font-semibold">{meta.label}</CardTitle>
148
- <CardDescription>{meta.detail}</CardDescription>
149
- </div>
150
- </CardHeader>
151
- </Card>
152
- </Link>
153
- );
154
- })}
155
- </div>
156
- </section>
157
-
79
+ <div aria-label="Notification Center feed" className="newsroom-overview-feed-frame rounded-xl border bg-card p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" data-newsroom-overview-notifications role="region" tabIndex={0}>
80
+ <NotificationCenter
81
+ actorLabel={actorLabel}
82
+ decisions={decisions}
83
+ demo={demo}
84
+ humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
85
+ references={canonicalReferences}
86
+ />
87
+ </div>
158
88
  <section aria-label="Needs attention" className="space-y-3">
159
89
  <h2 className="text-sm font-medium text-muted-foreground">Needs attention</h2>
160
90
  {pendingReferences.length || attentionAssignments.length ? (
@@ -208,16 +138,16 @@ export function NewsroomOpsOverview({
208
138
  ) : (
209
139
  <Card>
210
140
  <CardContent className="p-4 text-sm text-muted-foreground">
211
- Nothing needs attention right now. Check References for new intake or open Assignments from the destinations above.
141
+ No pending references or open assignments. Check those destinations for other work.
212
142
  </CardContent>
213
143
  </Card>
214
144
  )}
215
145
  </section>
216
146
 
217
147
  {desks.length ? (
218
- <section aria-label="Desks" className="space-y-3" data-newsroom-ops-desks>
219
- <h2 className="text-sm font-medium text-muted-foreground">Desks</h2>
220
- {coreDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="core">Core desks</h3> : null}
148
+ <section aria-label="Sections" className="space-y-3" data-newsroom-ops-desks>
149
+ <h2 className="text-sm font-medium text-muted-foreground">Sections</h2>
150
+ {coreDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="core">Core sections</h3> : null}
221
151
  <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
222
152
  {coreDesks.map((section) => (
223
153
  <Link
@@ -237,7 +167,7 @@ export function NewsroomOpsOverview({
237
167
  </Link>
238
168
  ))}
239
169
  </div>
240
- {rotatingDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="rotating">Rotating desks <small>appear for a while, then rotate out</small></h3> : null}
170
+ {rotatingDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="rotating">Rotating sections <small>appear for a while, then rotate out</small></h3> : null}
241
171
  {rotatingDesks.length ? <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
242
172
  {rotatingDesks.map((section) => (
243
173
  <Link
@@ -8,7 +8,7 @@ import { cn } from "../lib/utils";
8
8
  import { useEffect, useId, useState, type ReactNode } from "react";
9
9
  import Link from "next/link";
10
10
  import { usePathname, useRouter, useSearchParams } from "next/navigation";
11
- import { BookOpen, ClipboardList, Menu, Search, MessagesSquare, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
11
+ import { BookOpen, ClipboardList, House, Menu, Search, MessagesSquare, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
12
12
  import { ORGANIZER_NAV_GROUPS, ORGANIZER_PRIMARY_NAV, ORGANIZER_SETTINGS_DESTINATION } from "../lib/newsroom-organizer-nav";
13
13
  import { browserPathToInternalNewsroomPath } from "../lib/newsroom-base-path";
14
14
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
@@ -55,7 +55,7 @@ function OrganizerSearch({ demo }: { demo: boolean }) {
55
55
  </form>;
56
56
  }
57
57
 
58
- const ICONS = { articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
58
+ const ICONS = { overview: House, articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
59
59
 
60
60
  /** Navigation is a presentation layer; existing route and server authorization still own access. */
61
61
  function Navigation({ activePath, demo, admin, surface, onNavigate }: { activePath: string; demo: boolean; admin: boolean; surface: string; onNavigate?: () => void }) {