From 331316122db0fc042f4b5a06918421416c6bcade Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Wed, 4 Mar 2026 10:29:41 -0500 Subject: [PATCH 01/15] feat: coar notify US4, announce ingest --- core/playwright/coar-notify.spec.ts | 132 +++++++++++++++++- .../fixtures/coar-notify-payloads.ts | 3 + core/prisma/seeds/coar-notify.ts | 62 +++++++- .../app/components/SendNotificationForm.tsx | 60 +++++++- mock-notify/src/lib/payloads.ts | 3 + 5 files changed, 246 insertions(+), 14 deletions(-) diff --git a/core/playwright/coar-notify.spec.ts b/core/playwright/coar-notify.spec.ts index fd412dc5c..89589da5c 100644 --- a/core/playwright/coar-notify.spec.ts +++ b/core/playwright/coar-notify.spec.ts @@ -82,6 +82,17 @@ const seed = createSeed({ config: { path: WEBHOOK_PATH }, }, ], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: + "'Offer' in $.json.type or ('Announce' in $.json.type and 'coar-notify:IngestAction' in $.json.type)", + }, + ], + }, actions: [ { action: Action.createPub, @@ -97,7 +108,7 @@ const seed = createSeed({ }, ], }, - "Create Review for Notification": { + "Create Review for Offer": { triggers: [ { event: AutomationEvent.pubEnteredStage, @@ -112,6 +123,11 @@ const seed = createSeed({ type: "jsonata", expression: "$.pub.pubType.name = 'Notification'", }, + { + kind: "condition", + type: "jsonata", + expression: "'Offer' in $eval($.pub.values.payload).type", + }, ], }, actions: [ @@ -122,8 +138,6 @@ const seed = createSeed({ formSlug: "review-default-editor", pubValues: { title: "Review for: {{ $.pub.values.title }}", - // Copy sourceurl from Notification to Review for use in Announce - // TODO: Investigate why relationship traversal ($.pub.out.relatedpub.values.sourceurl) isn't working sourceurl: "{{ $.pub.values.sourceurl }}", }, relationConfig: { @@ -136,8 +150,89 @@ const seed = createSeed({ }, ], }, + "Accept Request": { + triggers: [{ event: AutomationEvent.manual, config: {} }], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: "$.pub.pubType.name = 'Notification'", + }, + ], + }, + actions: [{ action: Action.move, config: { stage: STAGE_IDS.Accepted } }], + }, + "Reject Request": { + triggers: [{ event: AutomationEvent.manual, config: {} }], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: "$.pub.pubType.name = 'Notification'", + }, + ], + }, + actions: [{ action: Action.move, config: { stage: STAGE_IDS.Rejected } }], + }, }, }, + Accepted: { + id: STAGE_IDS.Accepted, + automations: { + "Create Review for Ingest": { + triggers: [ + { + event: AutomationEvent.pubEnteredStage, + config: {}, + }, + ], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: "$.pub.pubType.name = 'Notification'", + }, + { + kind: "condition", + type: "jsonata", + expression: + "'Announce' in $eval($.pub.values.payload).type and 'coar-notify:IngestAction' in $eval($.pub.values.payload).type", + }, + ], + }, + resolver: `$.pub.id = {{ $replace($replace($eval($.pub.values.payload).object["as:inReplyTo"], $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pubs/", ""), $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pub/", "") }}`, + actions: [ + { + action: Action.createPub, + config: { + stage: STAGE_IDS.ReviewInbox, + formSlug: "review-default-editor", + pubValues: { + title: + "Review from aggregator: {{ $eval($.pub.values.payload).object.id }}", + }, + relationConfig: { + fieldSlug: `${COMMUNITY_SLUG}:relatedpub`, + relatedPubId: "{{ $.pub.id }}", + value: "Submission", + direction: "source", + }, + }, + }, + ], + }, + }, + }, + Rejected: { + id: STAGE_IDS.Rejected, + automations: {}, + }, ReviewInbox: { id: STAGE_IDS.ReviewInbox, automations: { @@ -250,7 +345,10 @@ const seed = createSeed({ ], stageConnections: { Inbox: { - to: ["ReviewRequested", "Published"], + to: ["ReviewRequested", "Published", "Accepted", "Rejected"], + }, + Accepted: { + to: ["ReviewInbox"], }, ReviewInbox: { to: ["Reviewing"], @@ -444,12 +542,17 @@ test.describe("User Story 4: Review Group Aggregation Announcement to Repositori await loginPage.goto() await loginPage.loginAndWaitForNavigation(community.users.admin.email, "password") - const webhookUrl = `${process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000"}/api/v0/c/${community.community.slug}/site/webhook/${WEBHOOK_PATH}` + const baseUrl = process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000" + const webhookUrl = `${baseUrl}/api/v0/c/${community.community.slug}/site/webhook/${WEBHOOK_PATH}` + + const submissionPub = community.pubs[0] + const workUrl = `${baseUrl}/c/${community.community.slug}/pub/${submissionPub.id}` const ingestionAnnouncement = createAnnounceIngestPayload({ reviewId: "review-123", serviceUrl: "https://review-group.org", aggregatorUrl: mockPreprintRepo.url, + workUrl, }) await mockPreprintRepo.sendNotification(webhookUrl, ingestionAnnouncement) @@ -463,5 +566,24 @@ test.describe("User Story 4: Review Group Aggregation Announcement to Repositori ).toBeVisible({ timeout: 15000, }) + + // Accept the notification to trigger Create Review for Ingest + const stagesManagePage = new StagesManagePage(page, community.community.slug) + await stagesManagePage.goTo() + await stagesManagePage.openStagePanelTab("Inbox", "Pubs") + await page.getByRole("button", { name: "Inbox" }).first().click() + await page.getByText("Move to Accepted").click() + + // Verify Review was created and linked to the Submission (Pre-existing Pub) + await expect + .poll( + async () => { + await page.goto(`/c/${community.community.slug}/stages`) + const reviewList = page.getByText("Review from aggregator:", { exact: false }) + return (await reviewList.count()) > 0 + }, + { timeout: 15000 } + ) + .toBe(true) }) }) diff --git a/core/playwright/fixtures/coar-notify-payloads.ts b/core/playwright/fixtures/coar-notify-payloads.ts index 361848c55..5d7ccf695 100644 --- a/core/playwright/fixtures/coar-notify-payloads.ts +++ b/core/playwright/fixtures/coar-notify-payloads.ts @@ -231,10 +231,12 @@ export function createAnnounceIngestPayload({ reviewId, serviceUrl, aggregatorUrl, + workUrl, }: { reviewId: string serviceUrl: string aggregatorUrl: string + workUrl?: string }): CoarNotifyPayload { const reviewUrl = `${serviceUrl}/review/${reviewId}` return { @@ -249,6 +251,7 @@ export function createAnnounceIngestPayload({ object: { id: reviewUrl, type: ["Page", "sorg:Review"], + ...(workUrl && { "as:inReplyTo": workUrl }), }, target: { id: serviceUrl, diff --git a/core/prisma/seeds/coar-notify.ts b/core/prisma/seeds/coar-notify.ts index f4f520463..2d0e061aa 100644 --- a/core/prisma/seeds/coar-notify.ts +++ b/core/prisma/seeds/coar-notify.ts @@ -115,14 +115,15 @@ export async function seedCoarNotify(communityId?: CommunitiesId) { config: { path: WEBHOOK_PATH }, }, ], - // Only process incoming Offer notifications (review requests from external services) + // Process incoming Offer (review requests) or Announce Ingest (aggregator notifications) condition: { type: AutomationConditionBlockType.AND, items: [ { kind: "condition", type: "jsonata", - expression: "'Offer' in $.json.type", + expression: + "'Offer' in $.json.type or ('Announce' in $.json.type and 'coar-notify:IngestAction' in $.json.type)", }, ], }, @@ -597,8 +598,8 @@ pre { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow-x: aut }, ], }, - // Create a Review pub after accepting the request - "Create Review for Notification": { + // Create a Review pub after accepting an Offer (links to Notification) + "Create Review for Offer": { icon: { name: "plus-circle", color: "#10b981", @@ -617,6 +618,11 @@ pre { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow-x: aut type: "jsonata", expression: "$.pub.pubType.name = 'Notification'", }, + { + kind: "condition", + type: "jsonata", + expression: "'Offer' in $eval($.pub.values.Payload).type", + }, ], }, actions: [ @@ -638,6 +644,54 @@ pre { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow-x: aut }, ], }, + // Create a Review pub after accepting an Announce Ingest (links to resolved local article) + "Create Review for Ingest": { + icon: { + name: "plus-circle", + color: "#10b981", + }, + triggers: [ + { + event: AutomationEvent.pubEnteredStage, + config: {}, + }, + ], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: "$.pub.pubType.name = 'Notification'", + }, + { + kind: "condition", + type: "jsonata", + expression: + "'Announce' in $eval($.pub.values.Payload).type and 'coar-notify:IngestAction' in $eval($.pub.values.Payload).type", + }, + ], + }, + resolver: `$.pub.id = {{ $replace($replace($eval($.pub.values.Payload).object["as:inReplyTo"], $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pubs/", ""), $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pub/", "") }}`, + actions: [ + { + action: Action.createPub, + config: { + stage: STAGE_IDS.ReviewInbox, + formSlug: "review-default-editor", + pubValues: { + Title: "Review from aggregator: {{ $eval($.pub.values.Payload).object.id }}", + }, + relationConfig: { + fieldSlug: "coar-notify:relatedpub", + relatedPubId: "{{ $.pub.id }}", + value: "Submission", + direction: "source", + }, + }, + }, + ], + }, }, }, Rejected: { diff --git a/mock-notify/src/app/components/SendNotificationForm.tsx b/mock-notify/src/app/components/SendNotificationForm.tsx index 3435342f2..edec482dc 100644 --- a/mock-notify/src/app/components/SendNotificationForm.tsx +++ b/mock-notify/src/app/components/SendNotificationForm.tsx @@ -65,6 +65,9 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr const [serviceName, setServiceName] = useState("Mock Review Service") const [inReplyTo, setInReplyTo] = useState(prefill?.inReplyTo ?? "") const [inReplyToUrl, setInReplyToUrl] = useState(prefill?.inReplyToObjectUrl ?? "") + const [workUrl, setWorkUrl] = useState( + "http://localhost:3000/c/coar-notify/pub/{pubId}" + ) const generatePayload = (): CoarNotifyPayload => { switch (templateType) { @@ -95,6 +98,7 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr reviewUrl, originUrl, targetUrl: targetServiceUrl, + workUrl: workUrl || undefined, }) case "Accept": return createAcceptPayload({ @@ -297,7 +301,6 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr ) case "Offer Ingest": - case "Announce Ingest": return ( <>
@@ -307,18 +310,65 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr type="text" value={reviewUrl} onChange={(e) => setReviewUrl(e.target.value)} - placeholder="http://localhost:4000/review/..." + placeholder="http://localhost:4001/review/..." + className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" + /> + +
+
+ +
+
+ setReviewUrl(e.target.value)} - placeholder="http://localhost:4001/review/..." + value={targetServiceUrl} + onChange={(e) => setTargetServiceUrl(e.target.value)} className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
+ + ) + case "Announce Ingest": + return ( + <> +
+ +
+
+ +
From 5239af947d907213bdc7876c89ed19a7bd0b86f3 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Thu, 5 Mar 2026 15:15:29 -0500 Subject: [PATCH 03/15] proper syntax for seeding action configs w/ jsonata --- core/prisma/seeds/coar-notify.ts | 42 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/core/prisma/seeds/coar-notify.ts b/core/prisma/seeds/coar-notify.ts index 0c18d5f30..fc1c8d61b 100644 --- a/core/prisma/seeds/coar-notify.ts +++ b/core/prisma/seeds/coar-notify.ts @@ -144,33 +144,33 @@ export async function seedCoarUS1(communityId?: CommunitiesId) { config: { url: REMOTE_INBOX_URL, method: "POST", - body: { + body: `<<< { "@context": [ "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net", + "https://coar-notify.net" ], - type: ["Offer", "coar-notify:ReviewAction"], - id: "urn:uuid:{{ $.pub.id }}", - actor: { - id: "{{ $.env.PUBPUB_URL }}/c/{{ $.community.slug }}", - type: "Service", - name: "{{ $.community.name }}", - }, - object: { - id: "{{ $.env.PUBPUB_URL }}/c/{{ $.community.slug }}/pub/{{ $.pub.id }}", - type: ["Page", "sorg:AboutPage"], + "type": ["Offer", "coar-notify:ReviewAction"], + "id": "urn:uuid:" & $.pub.id, + "actor": { + "id": $.env.PUBPUB_URL & "/c/" & $.community.slug, + "type": "Service", + "name": $.community.name }, - target: { - id: REMOTE_INBOX_URL.replace("/inbox", ""), - inbox: REMOTE_INBOX_URL, - type: "Service", + "object": { + "id": $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pub/" & $.pub.id, + "type": ["Page", "sorg:AboutPage"] }, - origin: { - id: "{{ $.env.PUBPUB_URL }}/c/{{ $.community.slug }}", - inbox: `{{ $.env.PUBPUB_URL }}/api/v0/c/{{ $.community.slug }}/site/webhook/${WEBHOOK_PATH}`, - type: "Service", + "target": { + "id": "${REMOTE_INBOX_URL.replace("/inbox", "")}", + "inbox": "${REMOTE_INBOX_URL}", + "type": "Service" }, - }, + "origin": { + "id": $.env.PUBPUB_URL & "/c/" & $.community.slug, + "inbox": $.env.PUBPUB_URL & "/api/v0/c/" & $.community.slug & "/site/webhook/${WEBHOOK_PATH}", + "type": "Service" + } + } >>>`, }, }, ], From 9d3792511e4fbce4397d3390cf938aa121cab465 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Wed, 11 Mar 2026 10:58:36 -0400 Subject: [PATCH 04/15] wip --- .gitignore | 3 + core/actions/buildSite/action.ts | 8 +- core/actions/buildSite/run.tsx | 182 ++++++- core/playwright/coar-notify.spec.ts | 4 +- core/prisma/seeds/coar-notify.ts | 444 +++++++++++++++++- .../src/app/components/NotificationCard.tsx | 87 +++- .../app/components/SendNotificationForm.tsx | 9 +- mock-notify/src/app/page.tsx | 4 +- .../reviews/sample-review/content/route.ts | 25 + .../app/reviews/sample-review/docmap/route.ts | 51 ++ .../src/app/reviews/sample-review/route.ts | 102 ++++ mock-notify/src/lib/payloads.ts | 16 + mock-notify/src/lib/store.ts | 45 +- .../contracts/src/resources/site-builder-2.ts | 3 +- site-builder-2/server/server.ts | 76 ++- 15 files changed, 973 insertions(+), 86 deletions(-) create mode 100644 mock-notify/src/app/reviews/sample-review/content/route.ts create mode 100644 mock-notify/src/app/reviews/sample-review/docmap/route.ts create mode 100644 mock-notify/src/app/reviews/sample-review/route.ts diff --git a/.gitignore b/.gitignore index ccc4f294c..38a05eafe 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ storybook-static ./playwright .local_data + +# mock-notify store +.notifications.json diff --git a/core/actions/buildSite/action.ts b/core/actions/buildSite/action.ts index 93e18a49d..a3a7e371c 100644 --- a/core/actions/buildSite/action.ts +++ b/core/actions/buildSite/action.ts @@ -91,11 +91,17 @@ const schema = z.object({ transform: z .string() .describe("JSONata expression that outputs content for the page"), + headExtra: z + .string() + .optional() + .describe( + "JSONata expression for additional HTML to inject into (e.g. tags). Only applies to HTML pages." + ), extension: z .string() .default("html") .describe( - "File extension for the generated output (e.g., 'html', 'json', 'xml'). Only 'html' pages are wrapped in an HTML shell." + "File extension for the generated output (e.g., 'html', 'json', 'xml'). Only 'html' pages are wrapped in an HTML shell. If content starts with return interpolate(expression, data) } +/** + * Browser script injected into submission pages that dynamically fetches + * review content by following signposting links: + * review page → → DocMap JSON → web-content URL → content + */ +const SIGNPOSTING_FETCH_SCRIPT = `