@anthusai/papyrus 5.42.0-next.2 → 5.42.0-next.4
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/schema.js +2 -0
- package/amplify/data/schema.ts +2 -0
- package/amplify/functions/monitoring-view/schema.js +6 -0
- package/amplify/functions/monitoring-view/schema.ts +6 -0
- package/amplify/functions/monitoring-view/wire.js +1 -0
- package/amplify/functions/monitoring-view/wire.ts +1 -0
- package/amplify/functions/proposal-actions/handler.py +137 -0
- package/amplify/functions/proposal-actions/resource.js +15 -0
- package/amplify/functions/proposal-actions/resource.ts +22 -0
- package/amplify/functions/proposal-actions/schema.js +106 -0
- package/amplify/functions/proposal-actions/schema.ts +108 -0
- package/amplify/functions/proposal-actions/wire.js +10 -0
- package/amplify/functions/proposal-actions/wire.ts +13 -0
- package/amplify/plugins/reader-steering/function/handler.py +12 -69
- package/amplify/plugins/reader-steering/papyrus.plugin.js +5 -5
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +5 -5
- package/amplify/plugins/reader-steering/schema.js +0 -95
- package/amplify/plugins/reader-steering/schema.ts +0 -95
- package/amplify/site-backend.js +4 -0
- package/amplify/site-backend.ts +4 -0
- package/app/globals.css +2 -35
- package/app/tailwind.css +2 -0
- package/components/monitoring-overview-view.tsx +7 -5
- package/components/monitoring-panel.tsx +33 -3
- package/components/newsroom-references-view.tsx +2 -3
- package/components/proposals-panel.tsx +110 -0
- package/components/proposals-view.tsx +93 -0
- package/components/topic-steering-workspace.tsx +13 -2
- package/lib/monitoring-client.ts +11 -2
- package/lib/monitoring-overview-fixture.ts +1 -1
- package/lib/monitoring-overview.ts +1 -1
- package/lib/proposals-admin.ts +115 -0
- package/lib/relevance-decisions.ts +9 -0
- package/package.json +4 -1
- package/routes.manifest.json +1 -1
- /package/amplify/{plugins/reader-steering/function → functions/proposal-actions}/proposals.py +0 -0
|
@@ -14,9 +14,8 @@ export const READER_POLL_SECRET_PARAMETER_ENV = "PAPYRUS_READER_POLL_SECRET_PARA
|
|
|
14
14
|
export const readerSteeringManifest = {
|
|
15
15
|
name: "reader-steering",
|
|
16
16
|
contract: 1,
|
|
17
|
-
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"
|
|
18
|
-
mutations: ["moderateReader"
|
|
19
|
-
queries: ["listStoryDrafts"],
|
|
17
|
+
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"],
|
|
18
|
+
mutations: ["moderateReader"],
|
|
20
19
|
guest: { mutations: [] },
|
|
21
20
|
newsroomPanels: [{ id: "moderation", label: "Moderation" }],
|
|
22
21
|
staticAssets: ["_plugins/papyrus-reader@1.js", "_plugins/papyrus-reader@1.css"],
|
|
@@ -119,7 +118,6 @@ export const readerSteeringPlugin = {
|
|
|
119
118
|
["PAPYRUS_READER_COMMENT_TABLE", "ReaderComment"],
|
|
120
119
|
["PAPYRUS_READER_REPORT_TABLE", "ReaderReport"],
|
|
121
120
|
["PAPYRUS_READER_ACTION_TABLE", "ModerationAction"],
|
|
122
|
-
["PAPYRUS_READER_PROPOSAL_TABLE", "Proposal"],
|
|
123
121
|
]) {
|
|
124
122
|
tables[model].grantReadWriteData(lambda);
|
|
125
123
|
lambda.addEnvironment(variable, tables[model].tableName);
|
|
@@ -136,8 +134,10 @@ export const readerSteeringPlugin = {
|
|
|
136
134
|
// The snapshot reads submissions, and the review step writes their signals and moderation state.
|
|
137
135
|
tables.ReaderSubmission.grantReadWriteData(contentActions);
|
|
138
136
|
tables.ReaderComment.grantReadWriteData(contentActions);
|
|
139
|
-
// The snapshot reads proposals and freezes the community result of one that reaches editor review.
|
|
137
|
+
// The snapshot reads proposals and freezes the community result of one that reaches editor review. The Proposal model and the
|
|
138
|
+
// functions that file and move proposals are core (amplify/functions/proposal-actions); this plugin only reads the same table.
|
|
140
139
|
tables.Proposal.grantReadWriteData(contentActions);
|
|
140
|
+
contentActions.addEnvironment("PAPYRUS_READER_PROPOSAL_TABLE", tables.Proposal.tableName);
|
|
141
141
|
lambda.addEnvironment("PAPYRUS_READER_SETTINGS", JSON.stringify(readerActionsSettings(config, interimSubjectsAllowedAtSynth())));
|
|
142
142
|
if (config.reservedConcurrency !== undefined) {
|
|
143
143
|
lambda.node.defaultChild.addPropertyOverride("ReservedConcurrentExecutions", config.reservedConcurrency);
|
|
@@ -21,9 +21,8 @@ export const READER_POLL_SECRET_PARAMETER_ENV = "PAPYRUS_READER_POLL_SECRET_PARA
|
|
|
21
21
|
export const readerSteeringManifest: PluginManifest = {
|
|
22
22
|
name: "reader-steering",
|
|
23
23
|
contract: 1,
|
|
24
|
-
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"
|
|
25
|
-
mutations: ["moderateReader"
|
|
26
|
-
queries: ["listStoryDrafts"],
|
|
24
|
+
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"],
|
|
25
|
+
mutations: ["moderateReader"],
|
|
27
26
|
guest: { mutations: [] },
|
|
28
27
|
newsroomPanels: [{ id: "moderation", label: "Moderation" }],
|
|
29
28
|
staticAssets: ["_plugins/papyrus-reader@1.js", "_plugins/papyrus-reader@1.css"],
|
|
@@ -138,7 +137,6 @@ export const readerSteeringPlugin: PapyrusPlugin<ReaderSteeringConfig, { readerA
|
|
|
138
137
|
["PAPYRUS_READER_COMMENT_TABLE", "ReaderComment"],
|
|
139
138
|
["PAPYRUS_READER_REPORT_TABLE", "ReaderReport"],
|
|
140
139
|
["PAPYRUS_READER_ACTION_TABLE", "ModerationAction"],
|
|
141
|
-
["PAPYRUS_READER_PROPOSAL_TABLE", "Proposal"],
|
|
142
140
|
] as const) {
|
|
143
141
|
tables[model].grantReadWriteData(lambda);
|
|
144
142
|
lambda.addEnvironment(variable, tables[model].tableName);
|
|
@@ -155,8 +153,10 @@ export const readerSteeringPlugin: PapyrusPlugin<ReaderSteeringConfig, { readerA
|
|
|
155
153
|
// The snapshot reads submissions, and the review step writes their signals and moderation state.
|
|
156
154
|
tables.ReaderSubmission.grantReadWriteData(contentActions);
|
|
157
155
|
tables.ReaderComment.grantReadWriteData(contentActions);
|
|
158
|
-
// The snapshot reads proposals and freezes the community result of one that reaches editor review.
|
|
156
|
+
// The snapshot reads proposals and freezes the community result of one that reaches editor review. The Proposal model and the
|
|
157
|
+
// functions that file and move proposals are core (amplify/functions/proposal-actions); this plugin only reads the same table.
|
|
159
158
|
tables.Proposal.grantReadWriteData(contentActions);
|
|
159
|
+
contentActions.addEnvironment("PAPYRUS_READER_PROPOSAL_TABLE", tables.Proposal.tableName);
|
|
160
160
|
|
|
161
161
|
lambda.addEnvironment("PAPYRUS_READER_SETTINGS", JSON.stringify(readerActionsSettings(config, interimSubjectsAllowedAtSynth())));
|
|
162
162
|
if (config.reservedConcurrency !== undefined) {
|
|
@@ -135,101 +135,6 @@ export function readerSteeringSchemaDefinitions() {
|
|
|
135
135
|
.disableOperations(["subscriptions"])
|
|
136
136
|
.secondaryIndexes((index) => [index("target").sortKeys(["at"]).name("actionsByTarget").queryField("listModerationActionsByTargetAndAt")])
|
|
137
137
|
.authorization(staffOnly),
|
|
138
|
-
Proposal: a
|
|
139
|
-
.model({
|
|
140
|
-
id: a.id().required(),
|
|
141
|
-
kind: a.string().required(),
|
|
142
|
-
title: a.string().required(),
|
|
143
|
-
summary: a.string(),
|
|
144
|
-
body: a.string().required(),
|
|
145
|
-
targets: a.json().required(),
|
|
146
|
-
evidence: a.json(),
|
|
147
|
-
proposer: a.string().required(),
|
|
148
|
-
state: a.string().required(),
|
|
149
|
-
history: a.json().required(),
|
|
150
|
-
communityResult: a.json(),
|
|
151
|
-
pinned: a.boolean(),
|
|
152
|
-
held: a.boolean(),
|
|
153
|
-
override: a.json(),
|
|
154
|
-
exclusions: a.json(),
|
|
155
|
-
editorLog: a.json(),
|
|
156
|
-
needsLook: a.json(),
|
|
157
|
-
createdAt: a.datetime().required(),
|
|
158
|
-
})
|
|
159
|
-
.disableOperations(["subscriptions"])
|
|
160
|
-
.secondaryIndexes((index) => [index("state").sortKeys(["createdAt"]).name("proposalsByState").queryField("listProposalsByStateAndCreatedAt")])
|
|
161
|
-
.authorization(staffOnly),
|
|
162
|
-
ProposalReceipt: a.customType({
|
|
163
|
-
status: a.string().required(),
|
|
164
|
-
id: a.string(),
|
|
165
|
-
error: a.string(),
|
|
166
|
-
message: a.string(),
|
|
167
|
-
}),
|
|
168
|
-
// What an operator may read of a story draft: these six fields and nothing else (no body, evidence, history, proposer, vote or log).
|
|
169
|
-
StoryDraftSummary: a.customType({
|
|
170
|
-
id: a.string().required(),
|
|
171
|
-
title: a.string().required(),
|
|
172
|
-
summary: a.string(),
|
|
173
|
-
targets: a.json(),
|
|
174
|
-
state: a.string().required(),
|
|
175
|
-
createdAt: a.string().required(),
|
|
176
|
-
}),
|
|
177
|
-
StoryDraftList: a.customType({
|
|
178
|
-
status: a.string().required(),
|
|
179
|
-
id: a.string(),
|
|
180
|
-
error: a.string(),
|
|
181
|
-
message: a.string(),
|
|
182
|
-
drafts: a.ref("StoryDraftSummary").array(),
|
|
183
|
-
}),
|
|
184
|
-
// Editors and admins file as themselves. AppSync also admits an IAM principal whose own policy allows the field (the
|
|
185
|
-
// schema accepts IAM callers for the functions that call it); the function files an AI proposal for an IAM principal
|
|
186
|
-
// of its own account only, and refuses an identity-pool user and any other account. No IAM rule is declared here.
|
|
187
|
-
fileProposal: a
|
|
188
|
-
.mutation()
|
|
189
|
-
.arguments({
|
|
190
|
-
kind: a.string().required(),
|
|
191
|
-
title: a.string().required(),
|
|
192
|
-
body: a.string().required(),
|
|
193
|
-
summary: a.string(),
|
|
194
|
-
targets: a.json(),
|
|
195
|
-
evidence: a.json(),
|
|
196
|
-
})
|
|
197
|
-
.returns(a.ref("ProposalReceipt"))
|
|
198
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
199
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
200
|
-
// The same IAM rule as fileProposal, narrower: an operator may move only draft to in_steering or declined.
|
|
201
|
-
// Approving, deciding after review and overriding are refused for IAM in the function (`operator_not_allowed`).
|
|
202
|
-
transitionProposal: a
|
|
203
|
-
.mutation()
|
|
204
|
-
.arguments({
|
|
205
|
-
proposalId: a.string().required(),
|
|
206
|
-
state: a.string().required(),
|
|
207
|
-
reason: a.string(),
|
|
208
|
-
result: a.string(),
|
|
209
|
-
})
|
|
210
|
-
.returns(a.ref("ProposalReceipt"))
|
|
211
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
212
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
213
|
-
// Read only, and the same caller rule as fileProposal: editors and admins, or an IAM principal of the function's own account. The function
|
|
214
|
-
// answers the story drafts' id, title, summary, targets, state and createdAt so an unattended trend run can dedupe; nothing else.
|
|
215
|
-
listStoryDrafts: a
|
|
216
|
-
.query()
|
|
217
|
-
.returns(a.ref("StoryDraftList"))
|
|
218
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
219
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
220
|
-
// Editors and admins only, and the function refuses an IAM operator: pin, unpin, hold, release, exclude_votes and
|
|
221
|
-
// undo_exclusion. `rule` is a batch rule for exclude_votes; `exclusionId` names the exclusion undo_exclusion reverses.
|
|
222
|
-
editProposal: a
|
|
223
|
-
.mutation()
|
|
224
|
-
.arguments({
|
|
225
|
-
proposalId: a.string().required(),
|
|
226
|
-
action: a.string().required(),
|
|
227
|
-
rule: a.string(),
|
|
228
|
-
exclusionId: a.string(),
|
|
229
|
-
})
|
|
230
|
-
.returns(a.ref("ProposalReceipt"))
|
|
231
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
232
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
233
138
|
ModerationReceipt: a.customType({
|
|
234
139
|
status: a.string().required(),
|
|
235
140
|
id: a.string(),
|
|
@@ -137,101 +137,6 @@ export function readerSteeringSchemaDefinitions() {
|
|
|
137
137
|
.disableOperations(["subscriptions"])
|
|
138
138
|
.secondaryIndexes((index) => [index("target").sortKeys(["at"]).name("actionsByTarget").queryField("listModerationActionsByTargetAndAt")])
|
|
139
139
|
.authorization(staffOnly),
|
|
140
|
-
Proposal: a
|
|
141
|
-
.model({
|
|
142
|
-
id: a.id().required(),
|
|
143
|
-
kind: a.string().required(),
|
|
144
|
-
title: a.string().required(),
|
|
145
|
-
summary: a.string(),
|
|
146
|
-
body: a.string().required(),
|
|
147
|
-
targets: a.json().required(),
|
|
148
|
-
evidence: a.json(),
|
|
149
|
-
proposer: a.string().required(),
|
|
150
|
-
state: a.string().required(),
|
|
151
|
-
history: a.json().required(),
|
|
152
|
-
communityResult: a.json(),
|
|
153
|
-
pinned: a.boolean(),
|
|
154
|
-
held: a.boolean(),
|
|
155
|
-
override: a.json(),
|
|
156
|
-
exclusions: a.json(),
|
|
157
|
-
editorLog: a.json(),
|
|
158
|
-
needsLook: a.json(),
|
|
159
|
-
createdAt: a.datetime().required(),
|
|
160
|
-
})
|
|
161
|
-
.disableOperations(["subscriptions"])
|
|
162
|
-
.secondaryIndexes((index) => [index("state").sortKeys(["createdAt"]).name("proposalsByState").queryField("listProposalsByStateAndCreatedAt")])
|
|
163
|
-
.authorization(staffOnly),
|
|
164
|
-
ProposalReceipt: a.customType({
|
|
165
|
-
status: a.string().required(),
|
|
166
|
-
id: a.string(),
|
|
167
|
-
error: a.string(),
|
|
168
|
-
message: a.string(),
|
|
169
|
-
}),
|
|
170
|
-
// What an operator may read of a story draft: these six fields and nothing else (no body, evidence, history, proposer, vote or log).
|
|
171
|
-
StoryDraftSummary: a.customType({
|
|
172
|
-
id: a.string().required(),
|
|
173
|
-
title: a.string().required(),
|
|
174
|
-
summary: a.string(),
|
|
175
|
-
targets: a.json(),
|
|
176
|
-
state: a.string().required(),
|
|
177
|
-
createdAt: a.string().required(),
|
|
178
|
-
}),
|
|
179
|
-
StoryDraftList: a.customType({
|
|
180
|
-
status: a.string().required(),
|
|
181
|
-
id: a.string(),
|
|
182
|
-
error: a.string(),
|
|
183
|
-
message: a.string(),
|
|
184
|
-
drafts: a.ref("StoryDraftSummary").array(),
|
|
185
|
-
}),
|
|
186
|
-
// Editors and admins file as themselves. AppSync also admits an IAM principal whose own policy allows the field (the
|
|
187
|
-
// schema accepts IAM callers for the functions that call it); the function files an AI proposal for an IAM principal
|
|
188
|
-
// of its own account only, and refuses an identity-pool user and any other account. No IAM rule is declared here.
|
|
189
|
-
fileProposal: a
|
|
190
|
-
.mutation()
|
|
191
|
-
.arguments({
|
|
192
|
-
kind: a.string().required(),
|
|
193
|
-
title: a.string().required(),
|
|
194
|
-
body: a.string().required(),
|
|
195
|
-
summary: a.string(),
|
|
196
|
-
targets: a.json(),
|
|
197
|
-
evidence: a.json(),
|
|
198
|
-
})
|
|
199
|
-
.returns(a.ref("ProposalReceipt"))
|
|
200
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
201
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
202
|
-
// The same IAM rule as fileProposal, narrower: an operator may move only draft to in_steering or declined.
|
|
203
|
-
// Approving, deciding after review and overriding are refused for IAM in the function (`operator_not_allowed`).
|
|
204
|
-
transitionProposal: a
|
|
205
|
-
.mutation()
|
|
206
|
-
.arguments({
|
|
207
|
-
proposalId: a.string().required(),
|
|
208
|
-
state: a.string().required(),
|
|
209
|
-
reason: a.string(),
|
|
210
|
-
result: a.string(),
|
|
211
|
-
})
|
|
212
|
-
.returns(a.ref("ProposalReceipt"))
|
|
213
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
214
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
215
|
-
// Read only, and the same caller rule as fileProposal: editors and admins, or an IAM principal of the function's own account. The function
|
|
216
|
-
// answers the story drafts' id, title, summary, targets, state and createdAt so an unattended trend run can dedupe; nothing else.
|
|
217
|
-
listStoryDrafts: a
|
|
218
|
-
.query()
|
|
219
|
-
.returns(a.ref("StoryDraftList"))
|
|
220
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
221
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
222
|
-
// Editors and admins only, and the function refuses an IAM operator: pin, unpin, hold, release, exclude_votes and
|
|
223
|
-
// undo_exclusion. `rule` is a batch rule for exclude_votes; `exclusionId` names the exclusion undo_exclusion reverses.
|
|
224
|
-
editProposal: a
|
|
225
|
-
.mutation()
|
|
226
|
-
.arguments({
|
|
227
|
-
proposalId: a.string().required(),
|
|
228
|
-
action: a.string().required(),
|
|
229
|
-
rule: a.string(),
|
|
230
|
-
exclusionId: a.string(),
|
|
231
|
-
})
|
|
232
|
-
.returns(a.ref("ProposalReceipt"))
|
|
233
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
234
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
235
140
|
ModerationReceipt: a.customType({
|
|
236
141
|
status: a.string().required(),
|
|
237
142
|
id: a.string(),
|
package/amplify/site-backend.js
CHANGED
|
@@ -24,6 +24,8 @@ import { defineSiteData } from "./data/resource.js";
|
|
|
24
24
|
import { editorialChecks } from "./functions/editorial-checks/resource.js";
|
|
25
25
|
import { automationView } from "./functions/automation-view/resource.js";
|
|
26
26
|
import { wireAutomationView } from "./functions/automation-view/wire.js";
|
|
27
|
+
import { proposalActions } from "./functions/proposal-actions/resource.js";
|
|
28
|
+
import { wireProposalActions } from "./functions/proposal-actions/wire.js";
|
|
27
29
|
import { monitoringView } from "./functions/monitoring-view/resource.js";
|
|
28
30
|
import { wireEmailReferenceIntake, wireMonitoringView } from "./functions/monitoring-view/wire.js";
|
|
29
31
|
import { wireEditorialChecks } from "./functions/editorial-checks/wire.js";
|
|
@@ -110,6 +112,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
110
112
|
modelAttachmentUpload,
|
|
111
113
|
newsroomSummary,
|
|
112
114
|
procedureAction,
|
|
115
|
+
proposalActions,
|
|
113
116
|
publicForms,
|
|
114
117
|
readerSettings,
|
|
115
118
|
emailSubmissionProcessor,
|
|
@@ -631,6 +634,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
631
634
|
wireEditorialChecks(backend);
|
|
632
635
|
wireMonitoringView(backend, emailMonitorId);
|
|
633
636
|
wireAutomationView(backend);
|
|
637
|
+
wireProposalActions(backend);
|
|
634
638
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
635
639
|
return backend;
|
|
636
640
|
}
|
package/amplify/site-backend.ts
CHANGED
|
@@ -31,6 +31,8 @@ import { defineSiteData } from "./data/resource";
|
|
|
31
31
|
import { editorialChecks } from "./functions/editorial-checks/resource";
|
|
32
32
|
import { automationView } from "./functions/automation-view/resource";
|
|
33
33
|
import { wireAutomationView } from "./functions/automation-view/wire";
|
|
34
|
+
import { proposalActions } from "./functions/proposal-actions/resource";
|
|
35
|
+
import { wireProposalActions } from "./functions/proposal-actions/wire";
|
|
34
36
|
import { monitoringView } from "./functions/monitoring-view/resource";
|
|
35
37
|
import { wireEmailReferenceIntake, wireMonitoringView } from "./functions/monitoring-view/wire";
|
|
36
38
|
import { wireEditorialChecks } from "./functions/editorial-checks/wire";
|
|
@@ -131,6 +133,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
131
133
|
modelAttachmentUpload,
|
|
132
134
|
newsroomSummary,
|
|
133
135
|
procedureAction,
|
|
136
|
+
proposalActions,
|
|
134
137
|
publicForms,
|
|
135
138
|
readerSettings,
|
|
136
139
|
emailSubmissionProcessor,
|
|
@@ -777,6 +780,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
777
780
|
wireEditorialChecks(backend);
|
|
778
781
|
wireMonitoringView(backend, emailMonitorId);
|
|
779
782
|
wireAutomationView(backend);
|
|
783
|
+
wireProposalActions(backend);
|
|
780
784
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
781
785
|
|
|
782
786
|
return backend;
|
package/app/globals.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@import "./papyrus-theme.css";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/* Tailwind (import, @source globs, @theme tokens) lives only in ./tailwind.css. Importing it again here
|
|
4
|
+
emitted a second full Tailwind build that loaded later and whose `.hidden` overrode `md:flex`. */
|
|
5
5
|
@import "@radix-ui/colors/slate.css";
|
|
6
6
|
@import "@radix-ui/colors/slate-alpha.css";
|
|
7
7
|
@import "@radix-ui/colors/slate-dark.css";
|
|
@@ -15,39 +15,6 @@
|
|
|
15
15
|
@import "@radix-ui/colors/tomato-dark.css";
|
|
16
16
|
@import "@radix-ui/colors/tomato-dark-alpha.css";
|
|
17
17
|
|
|
18
|
-
@theme inline {
|
|
19
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
20
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
21
|
-
--radius-lg: var(--radius);
|
|
22
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
23
|
-
--color-background: var(--background);
|
|
24
|
-
--color-foreground: var(--foreground);
|
|
25
|
-
--color-card: var(--card);
|
|
26
|
-
--color-card-foreground: var(--card-foreground);
|
|
27
|
-
--color-popover: var(--popover);
|
|
28
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
29
|
-
--color-primary: var(--primary);
|
|
30
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
31
|
-
--color-secondary: var(--secondary);
|
|
32
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
33
|
-
--color-muted: var(--muted);
|
|
34
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
35
|
-
--color-accent: var(--accent);
|
|
36
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
37
|
-
--color-destructive: var(--destructive);
|
|
38
|
-
--color-destructive-foreground: var(--destructive-foreground);
|
|
39
|
-
--color-border: var(--border);
|
|
40
|
-
--color-input: var(--input);
|
|
41
|
-
--color-ring: var(--ring);
|
|
42
|
-
--color-danger: var(--danger);
|
|
43
|
-
--color-danger-foreground: var(--danger-foreground);
|
|
44
|
-
--color-surface: var(--surface);
|
|
45
|
-
--color-surface-foreground: var(--surface-foreground);
|
|
46
|
-
--color-foreground-strong: var(--foreground-strong);
|
|
47
|
-
--color-foreground-muted: var(--foreground-muted);
|
|
48
|
-
--color-foreground-subtle: var(--foreground-subtle);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
18
|
:root {
|
|
52
19
|
--radius: 0.875rem;
|
|
53
20
|
--background: var(--slate-2);
|
package/app/tailwind.css
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
--color-accent: var(--accent);
|
|
25
25
|
--color-accent-foreground: var(--accent-foreground);
|
|
26
26
|
--color-destructive: var(--destructive);
|
|
27
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
28
|
+
--color-foreground-subtle: var(--foreground-subtle);
|
|
27
29
|
--color-danger: var(--danger);
|
|
28
30
|
--color-danger-foreground: var(--danger-foreground);
|
|
29
31
|
--color-surface: var(--surface);
|
|
@@ -97,8 +97,8 @@ function QueueItem({ item, ownId, demo }: { item: MonitoringItem; ownId: string
|
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImportCandidates }: {
|
|
101
|
-
items: MonitoringEmailSummary[]; detail: MonitoringEmailDetails | null; onOpen
|
|
100
|
+
export function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImportCandidates }: {
|
|
101
|
+
items: MonitoringEmailSummary[]; detail: MonitoringEmailDetails | null; onOpen?: (receiptId: string) => void;
|
|
102
102
|
onResolveArticleLink?: (receiptId: string, candidateIndex: number) => Promise<MonitoringEmailArticleResolution>;
|
|
103
103
|
onImportCandidates?: (receiptId: string, items: EmailReferenceImportItem[]) => Promise<EmailReferenceImportOutcome>;
|
|
104
104
|
}) {
|
|
@@ -181,7 +181,7 @@ function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImpor
|
|
|
181
181
|
if (result.error) {
|
|
182
182
|
setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: `${result.imported} of ${candidates.length} selected candidates were registered or matched. ${result.error}` }));
|
|
183
183
|
} else {
|
|
184
|
-
setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: `${result.imported} selected candidates are available as References. Import does not approve or publish them.` }));
|
|
184
|
+
setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: `${result.imported} selected candidates are available as References. Predictions are queued in the cloud; review them when ready. Import does not approve or publish them.` }));
|
|
185
185
|
}
|
|
186
186
|
} catch {
|
|
187
187
|
if (isCurrentEmailSubmissionAction(activeReceiptId.current, requestReceiptId)) {
|
|
@@ -207,7 +207,7 @@ function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImpor
|
|
|
207
207
|
<span>SES signals — SPF {item.verdicts.spf}, DKIM {item.verdicts.dkim}, DMARC {item.verdicts.dmarc}; spam {item.verdicts.spam}, virus {item.verdicts.virus}</span>
|
|
208
208
|
{item.warnings.length ? <span>Warnings: {item.warnings.join(", ")}</span> : null}
|
|
209
209
|
{item.errorCode ? <span>State: {item.status} ({item.errorCode})</span> : <span>State: {item.status}</span>}
|
|
210
|
-
<button type="button" onClick={() => onOpen(item.receiptId)}>Read email and review candidate links</button>
|
|
210
|
+
{onOpen ? <button type="button" onClick={() => onOpen(item.receiptId)}>Read email and review candidate links</button> : null}
|
|
211
211
|
</li>)}
|
|
212
212
|
</ul> : <p>No email submissions are available for this publication.</p>}
|
|
213
213
|
{detail && "submission" in detail ? (
|
|
@@ -238,7 +238,9 @@ function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink, onImpor
|
|
|
238
238
|
{busyImport ? "Adding References…" : "Add selected links to References"}
|
|
239
239
|
</button>
|
|
240
240
|
{notice ? <p role="status" data-email-import-result>{notice}</p> : null}
|
|
241
|
-
{importedReferences.length ? <
|
|
241
|
+
{importedReferences.length ? <ul>{importedReferences.map((reference, index) => (
|
|
242
|
+
<li key={`${reference.id}-${index}`}><Link href={getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(reference.lineageId)}`, false)}>Review imported Reference {index + 1}</Link></li>
|
|
243
|
+
))}</ul> : null}
|
|
242
244
|
</section>
|
|
243
245
|
) : detail && "error" in detail ? <p role="alert">You cannot read this submission.</p>
|
|
244
246
|
: detail && "state" in detail ? <p role="alert">Submission details are unavailable.</p> : null}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
+
import { useSearchParams } from "next/navigation";
|
|
4
5
|
import {
|
|
5
6
|
importMonitoringEmailCandidates,
|
|
6
7
|
loadMonitoringEmailSubmission,
|
|
@@ -9,12 +10,41 @@ import {
|
|
|
9
10
|
} from "../lib/monitoring-client";
|
|
10
11
|
import { FIXTURE_ALL_PIPELINE_OVERVIEW, FIXTURE_MONITORING_OVERVIEW } from "../lib/monitoring-overview-fixture";
|
|
11
12
|
import type { EmailReferenceImportItem, MonitoringEmailDetails, MonitoringResult } from "../lib/monitoring-overview";
|
|
12
|
-
import { MonitoringOverviewView, type MonitoringMode } from "./monitoring-overview-view";
|
|
13
|
+
import { EmailSubmissions, MonitoringOverviewView, type MonitoringMode } from "./monitoring-overview-view";
|
|
13
14
|
|
|
14
15
|
const REFRESH_MILLISECONDS = 60_000;
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
export function MonitoringPanel(
|
|
17
|
+
/** A receipt URL mounts only the single-submission reader, never the overview. */
|
|
18
|
+
export function MonitoringPanel(props: { isAdmin: boolean; isDemo: boolean }) {
|
|
19
|
+
const searchParams = useSearchParams();
|
|
20
|
+
if (searchParams.has("receipt")) {
|
|
21
|
+
return <MonitoringEmailReceipt key={searchParams.get("receipt")} receiptId={searchParams.get("receipt") ?? ""} isDemo={props.isDemo} />;
|
|
22
|
+
}
|
|
23
|
+
return <MonitoringOverviewPanel {...props} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function MonitoringEmailReceipt({ receiptId, isDemo }: { receiptId: string; isDemo: boolean }) {
|
|
27
|
+
const [detail, setDetail] = useState<MonitoringEmailDetails | null>(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!receiptId || isDemo) return;
|
|
30
|
+
let live = true;
|
|
31
|
+
void loadMonitoringEmailSubmission(receiptId).then(
|
|
32
|
+
(next) => { if (live) setDetail(next); },
|
|
33
|
+
() => { if (live) setDetail({ state: "unavailable" }); },
|
|
34
|
+
);
|
|
35
|
+
return () => { live = false; };
|
|
36
|
+
}, [receiptId, isDemo]);
|
|
37
|
+
if (!receiptId) return <p role="alert">A receipt is required to open a submission.</p>;
|
|
38
|
+
if (isDemo) return <p role="status">Single email submissions require an editor session.</p>;
|
|
39
|
+
return <div data-news-desk-admin-panel="monitoring">
|
|
40
|
+
{detail === null ? <p role="status">Loading email submission…</p> :
|
|
41
|
+
<EmailSubmissions items={"submission" in detail ? [detail.submission] : []} detail={detail}
|
|
42
|
+
onResolveArticleLink={resolveMonitoringEmailArticleLink} onImportCandidates={importMonitoringEmailCandidates} />}
|
|
43
|
+
</div>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Overview refreshes apply only when no receipt was requested. */
|
|
47
|
+
function MonitoringOverviewPanel({ isAdmin, isDemo }: { isAdmin: boolean; isDemo: boolean }) {
|
|
18
48
|
const [mode, setMode] = useState<MonitoringMode>("publication");
|
|
19
49
|
const [result, setResult] = useState<MonitoringResult | null>(null);
|
|
20
50
|
const [emailDetail, setEmailDetail] = useState<MonitoringEmailDetails | null>(null);
|
|
@@ -75,7 +75,7 @@ import type { PrimingFormInput } from "./reference-cyclotron-priming";
|
|
|
75
75
|
import { ReferenceCyclotronStatus, type ReviewRateChoice } from "./reference-cyclotron-status";
|
|
76
76
|
import type { PrimingRequestView } from "../lib/relevance-status";
|
|
77
77
|
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
78
|
-
import type
|
|
78
|
+
import { awaitsEditorReview, type RelevanceCuration, type RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
79
79
|
import {
|
|
80
80
|
loadReferenceAttachmentsForLineageId,
|
|
81
81
|
loadStoragePathUrl,
|
|
@@ -252,8 +252,7 @@ function useReferencePreviewAttachments(
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
function needsReview(reference: ReferenceRecord, decision: RelevanceDecisionView | undefined): boolean {
|
|
255
|
-
return
|
|
256
|
-
&& normalizeReferenceCurationStatus(reference.curationStatus) === "pending";
|
|
255
|
+
return awaitsEditorReview(reference.curationStatus, decision);
|
|
257
256
|
}
|
|
258
257
|
|
|
259
258
|
function ReferenceDetailPanel({
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { generateClient } from "aws-amplify/data";
|
|
4
|
+
import amplifyOutputs from "papyrus-amplify-outputs";
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { ProposalsView } from "./proposals-view";
|
|
7
|
+
import { pluginSettings, type PluginOutputs } from "../lib/plugin-settings";
|
|
8
|
+
import {
|
|
9
|
+
EDIT_PROPOSAL,
|
|
10
|
+
LIST_PROPOSALS,
|
|
11
|
+
TRANSITION_PROPOSAL,
|
|
12
|
+
proposalReceiptMessage,
|
|
13
|
+
type ProposalFilter,
|
|
14
|
+
type ProposalMove,
|
|
15
|
+
type ProposalReceipt,
|
|
16
|
+
type ProposalRow,
|
|
17
|
+
} from "../lib/proposals-admin";
|
|
18
|
+
|
|
19
|
+
type GraphResult = { data?: Record<string, { items?: ProposalRow[]; nextToken?: string | null } | ProposalReceipt | null>; errors?: { message?: string }[] };
|
|
20
|
+
const MAXIMUM_PAGES = 20;
|
|
21
|
+
|
|
22
|
+
async function loadProposals(): Promise<ProposalRow[]> {
|
|
23
|
+
const client = generateClient({ authMode: "userPool" });
|
|
24
|
+
const rows: ProposalRow[] = [];
|
|
25
|
+
let nextToken: string | null | undefined;
|
|
26
|
+
for (let page = 0; page < MAXIMUM_PAGES; page += 1) {
|
|
27
|
+
const result = (await client.graphql({ query: LIST_PROPOSALS, variables: { nextToken } })) as GraphResult;
|
|
28
|
+
if (result.errors?.length) throw new Error(result.errors[0]?.message ?? "Could not load proposals.");
|
|
29
|
+
const connection = result.data?.listProposals as { items?: ProposalRow[]; nextToken?: string | null } | undefined;
|
|
30
|
+
rows.push(...(connection?.items ?? []).filter(Boolean));
|
|
31
|
+
nextToken = connection?.nextToken;
|
|
32
|
+
if (!nextToken) break;
|
|
33
|
+
}
|
|
34
|
+
return rows;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function run(query: string, field: string, variables: Record<string, string | undefined>): Promise<ProposalReceipt> {
|
|
38
|
+
const client = generateClient({ authMode: "userPool" });
|
|
39
|
+
const result = (await client.graphql({ query, variables })) as GraphResult;
|
|
40
|
+
if (result.errors?.length) return { status: "refused", message: result.errors[0]?.message ?? "The action failed." };
|
|
41
|
+
return (result.data?.[field] as ProposalReceipt | null) ?? { status: "refused", message: "The action did not answer." };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One button: a path of states is one `transitionProposal` call per state, stopping at the first refusal; pin and hold are `editProposal`. */
|
|
45
|
+
async function perform(proposalId: string, move: ProposalMove, reason: string): Promise<ProposalReceipt> {
|
|
46
|
+
if (move.edit) return run(EDIT_PROPOSAL, "editProposal", { proposalId, action: move.edit });
|
|
47
|
+
let receipt: ProposalReceipt = { status: "refused", message: "Nothing to do." };
|
|
48
|
+
for (const state of move.path ?? []) {
|
|
49
|
+
receipt = await run(TRANSITION_PROPOSAL, "transitionProposal", { proposalId, state, ...(reason ? { reason } : {}) });
|
|
50
|
+
if (receipt.status !== "ok") break;
|
|
51
|
+
}
|
|
52
|
+
return receipt;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Core to every site: it does not depend on the reader-steering plugin. */
|
|
56
|
+
export function ProposalsPanel() {
|
|
57
|
+
const [rows, setRows] = useState<ProposalRow[]>([]);
|
|
58
|
+
const [filter, setFilter] = useState<ProposalFilter>("open");
|
|
59
|
+
const [status, setStatus] = useState("Loading...");
|
|
60
|
+
const [busy, setBusy] = useState(false);
|
|
61
|
+
const [reason, setReason] = useState("");
|
|
62
|
+
const readers = useMemo(() => pluginSettings(amplifyOutputs as PluginOutputs, "reader-steering") !== null, []);
|
|
63
|
+
|
|
64
|
+
const refresh = useCallback(async (quiet = false) => {
|
|
65
|
+
setBusy(true);
|
|
66
|
+
try {
|
|
67
|
+
setRows(await loadProposals());
|
|
68
|
+
if (!quiet) setStatus("");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
setStatus(`Error: ${error instanceof Error ? error.message : "Could not load proposals."}`);
|
|
71
|
+
} finally {
|
|
72
|
+
setBusy(false);
|
|
73
|
+
}
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
void refresh();
|
|
78
|
+
}, [refresh]);
|
|
79
|
+
|
|
80
|
+
const onMove = useCallback(
|
|
81
|
+
async (proposalId: string, move: ProposalMove) => {
|
|
82
|
+
setBusy(true);
|
|
83
|
+
setStatus("Working...");
|
|
84
|
+
try {
|
|
85
|
+
const receipt = await perform(proposalId, move, reason.trim());
|
|
86
|
+
setStatus(proposalReceiptMessage(receipt));
|
|
87
|
+
if (receipt.status === "ok") setReason("");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
setStatus(`Error: ${error instanceof Error ? error.message : "The action failed."}`);
|
|
90
|
+
}
|
|
91
|
+
await refresh(true);
|
|
92
|
+
},
|
|
93
|
+
[reason, refresh],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<ProposalsView
|
|
98
|
+
rows={rows}
|
|
99
|
+
readers={readers}
|
|
100
|
+
filter={filter}
|
|
101
|
+
busy={busy}
|
|
102
|
+
status={status}
|
|
103
|
+
reason={reason}
|
|
104
|
+
onFilter={setFilter}
|
|
105
|
+
onReason={setReason}
|
|
106
|
+
onMove={(proposalId, move) => void onMove(proposalId, move)}
|
|
107
|
+
onRefresh={() => void refresh()}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|