Skip to content

feat(da): move DA event page creation into EMC browser at runtime#200

Draft
qiyundai wants to merge 6 commits into
devfrom
page-gen-refactor
Draft

feat(da): move DA event page creation into EMC browser at runtime#200
qiyundai wants to merge 6 commits into
devfrom
page-gen-refactor

Conversation

@qiyundai

@qiyundai qiyundai commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Retires the fire-and-forget AIO pipeline (Kinesis → ESL → Hoolihan → hoolihan-da-webhook) as the sole mechanism for DA event page creation. When that pipeline fails, EMC has no visibility — failures only appear in Splunk logs that few people can read.
  • EMC now creates DA pages directly in the browser at save time, using the logged-in user's IMS Bearer token. Success or failure is surfaced to the author immediately.
  • Suppresses the old pipeline via a pageCreatedBy: 'emc' flag in the ESP payload so both paths don't race during the dual-run transition.

This PR is EMC-only. Three other repos (ESL, events-service-platform, events-platform-hh-webhooks) require coordinated changes before the old pipeline can be fully retired — see docs/DA_PAGE_CREATION_MIGRATION.md.

What's in this PR

New: DA service layer (web-src/src/services/da/)

File What it does
daFetch.ts Network primitive for DA/Helix calls — allows text/html responses and FormData bodies; validates hosts via ALLOWED_HOSTS; honours dryRun + ?nonInvasiveTest=true
daClient.ts readFromDA, writeToDA, listDaPath, deleteFromDA against admin.da.live
helixClient.ts helixPreview/Publish/Unpublish/PurgeCache; resolveHelixOperation; bulkHelixOperation sequencing
daPageService.ts Core orchestrator: per-locale loop, dynamic fragments, session/speaker hub pages, picture-tag swapping, full event metadata dump, Helix preview/publish

New: Page engine utilities (web-src/src/utils/daPage/)

File What it does
placeholders.ts [[kebab.path]] and [[scope:path:index]] template tokenizer; resolveArrayPlaceholders; updateFragmentPaths
paths.ts getRelativeEventPagePath, constructFragmentsFolderPath, getLocalizedTemplatePath; shared joinPath
localization.ts mergeLocalization, extractCustomAttributes (reads primaryProductName + promotionalItems), getDisplayDateTime, fillMissingFields
dom.ts Native DOMParser/XMLSerializer port of linkedom DOM operations: fragment metadata extraction, picture tag updates, metadata block append, modified-by ownership marker

New: Config & hook

  • config/daConfig.tsDA_CONFIG, DEFAULT_SP_LOCALES, HELIX_OPERATIONS, getDaSiteForSeries()
  • hooks/useDaPageCreation.ts — React hook wrapping daPageService.createEventPages(); tracks status, localeResults, error

Modified: Save flow (hooks/useEventFormSave.ts)

  • Step 7a — injects series data into the hydrated event (fast cache hit; getEventFull doesn't fetch the series record but the DA orchestrator needs series.targetCms and series.templateId)
  • Step 7b — calls createDaPages() after getEventFull hydration so image URLs, speakers, and series data are all present; publish failure throws (blocks save); draft failure sets daWarning on SaveResult instead of setSaveError (the draft DID save to ESP successfully)
  • Best-effort follow-up PUT — sends pageCreatedBy: 'emc' after successful page creation to suppress the kinesis pipeline once the other repos are updated

Modified: EventForm UX (pages/EventForm/EventForm.tsx)

  • Shows toast.info() when result.daWarning is set (DA page creation failed on draft save, ESP save succeeded)

Modified: Shared types & filters

  • ALLOWED_HOSTS in constants.ts — added admin.da.live and admin.hlx.page
  • EventApiResponse in types/domain.ts — added pageCreatedBy?: string
  • EVENT_DATA_FILTER in utils/dataFilters.ts — added pageCreatedBy as submittable: true so it survives prepareEslEventPutPayload

Tests

  • 67 unit tests across placeholders.test.ts, paths.test.ts, localization.test.ts — pure JS, no DOM/network mocks needed

Docs

  • docs/DA_PAGE_CREATION_MIGRATION.md — step-by-step guide for ESL, events-service-platform, and events-platform-hh-webhooks teams; includes rollout order, verification checklists, and full-retirement cleanup tables

What this PR does NOT include

  • AlertDialog for publish failures — currently surfaces as toast.error via the existing saveError path. A blocking per-locale retry dialog is a follow-up.
  • Progress UI during page creationuseDaPageCreation exposes status and localeResults but no component consumes them yet.
  • Auth spike — must verify that admin.da.live and admin.hlx.page accept the user's IMS token with CORS from the EMC origin before enabling for authors (see Risk R0 in the plan).
  • Cross-repo changes — documented in docs/DA_PAGE_CREATION_MIGRATION.md; not implemented here.

Test plan

  • npm run type-check — passes (zero errors)
  • npx jest --testPathPattern="utils/daPage" — 67/67 pass
  • Auth spike: fetch('https://admin.da.live/source/adobecom/da-events/<scratch>/test.html', { method: 'GET', headers: { Authorization: 'Bearer <user IMS token>' }, mode: 'cors' }) from the EMC dev origin — confirm CORS headers and token acceptance
  • Create a DA-series event in dev — confirm page + fragments written at the expected DA path
  • Publish the event — confirm Helix live and pageCreatedBy: 'emc' in the ESP event record
  • Save as draft — confirm only Helix preview (not live)
  • Confirm toast.info appears when DA page creation fails on a draft save
  • Confirm publish is blocked (error toast) when DA page creation fails on publish
  • Confirm modified-by: EMC in the page's .metadata block
  • Confirm a non-DA-series event saves normally with no DA calls made

🤖 Generated with Claude Code

qiyundai and others added 6 commits June 30, 2026 08:52
Ports the DA page creation pipeline from the hoolihan-da-webhook AIO app
into the EMC browser frontend so page creation failures are surfaced to
the author immediately instead of silently disappearing into Splunk logs.

## What's in this commit

### Config (daConfig.ts)
- DA_CONFIG: org='adobecom', branch='main', defaultSite='da-events'
- TARGET_CMS_SITE_MAP: maps series.targetCms.code → DA site name
- DEFAULT_SP_LOCALES: IETF locale → DA folder-prefix map (mirrors
  hoolihan-da-webhook appConfig.js defaultLocales)
- HELIX_OPERATIONS enum and getDaSiteForSeries() helper
- Extended ALLOWED_HOSTS in constants.ts: admin.da.live + admin.hlx.page
- Added pageCreatedBy?: string to EventApiResponse in domain.ts
- Registered pageCreatedBy in EVENT_DATA_FILTER (submittable:true) so it
  survives prepareEslEventPutPayload and reaches ESL for pipeline suppression

### Network layer (services/da/)
- daFetch.ts: sibling to safeFetch for DA/Helix calls; allows text/html
  responses and FormData bodies; validates host via isValidUrl; honors
  dryRun + ?nonInvasiveTest=true
- daClient.ts: readFromDA (GET→text), writeToDA (POST multipart FormData),
  listDaPath (GET list), deleteFromDA (DELETE); all using Accept:text/html
  on reads (not Content-Type)
- helixClient.ts: helixPreview/Publish/Unpublish/DeletePreview/PurgeCache;
  resolveHelixOperation({published, liveUpdate}) mapping; bulkHelixOperation
  sequencing (publish→preview+publish+purge; unpublish→unpublish+purge;
  preview→preview+purge concurrently)

### Page engine (utils/daPage/)
- placeholders.ts: replacePlaceholders ([[kebab.path]] and [[scope:path:index]]
  syntaxes); parseRegularPath; resolveArrayPlaceholders; updateFragmentPaths;
  camelToKebab; getMetadata; isPrimitive. Ported from utils.js — String.replaceAll
  replaced with String.replace(/.../g) for ES2020 compat.
- paths.ts: handleExtension, getRelativeEventPagePath, constructFragmentsFolderPath,
  getLocalizedTemplatePath; local joinPath() replaces node:path.join()
- localization.ts: mergeData, mergeLocalization, removeLocalizationObjects,
  getDisplayDateTime, formatDate, isSameDate, extractCustomAttributes (reads
  primaryProductName + promotionalItems from customAttributes[] with fallback
  to publishingProfile.metadata), fillMissingFields. Pure JS, no DOM/Node deps.
- dom.ts: parseHtmlDocument/serializeDocument via native DOMParser (linkedom
  replaced); extractDynamicFragmentMetadata, updatePictureTags, getHubConfigs,
  createMetaTag, appendEventMetadata, addPageMarker, performDomOperations.
  Serialization: <!DOCTYPE html>${documentElement.outerHTML}

### Orchestrator (daPageService.ts)
- isDocumentAuthoringEvent(): checks series.targetCms.code.startsWith('da-')
  or series.targetCms.provider === 'documentauthoring'
- createEventPages(): per-locale loop using event.ietfLocales or
  event.localizations keys + defaultLocale; per-locale: fetch template via
  _templateCache, populateEventObject (mergeLocalization + extractCustomAttributes
  + fillMissingFields), createHydratedEventFragments (dynamic-fragment-metadata
  block → fetch fragment templates → hydrate + write to DA), hub pages
  (session-hub + speaker-hub rows), picture tag updates, DOM operations
  (strip dynamic-fragment-metadata, append full event metadata dump, add
  modified-by:EMC marker), write final page, run bulkHelixOperation
- DA ownership: modified-by:EMC marker prevents hoolihan-da-webhook from
  overwriting EMC-created pages during dual-run transition period
- Module-level _templateCache Map: caches fetched templates within a session

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d UX fixes

## Save flow integration (useEventFormSave.ts)
- Added step 7b after cachedApi.getEventFull() hydration: if the series is
  DA-targeted, calls useDaPageCreation.createPages() with the fully-hydrated
  event (image URLs, speakers, sessions, series.templateId all present)
- Publish failure (daResult.success=false + publish=true): throws an Error so
  the save is marked failed. A published event with no live page is the worst
  outcome — this surfaces immediately to the author via the existing saveError
  toast rather than silently failing in Splunk.
- Draft failure (daResult.success=false + publish=false): logs to console,
  sets daWarning on SaveResult — the draft WAS saved to ESP so calling
  setSaveError would show a red error toast on a successful save, which is
  misleading. daWarning lets the caller choose the presentation separately.
- Success: fire-and-forget best-effort PUT to persist pageCreatedBy:'emc' on
  the event record so the kinesis-processor can suppress the old webhook path
  once it is updated to check the flag. Non-fatal if this PUT fails.
- Extended SaveResult interface: added daWarning?: string field.

## React hook (useDaPageCreation.ts)
- Thin wrapper over daPageService.createEventPages()
- Guards: skips non-DA events (isDocumentAuthoringEvent check) and events
  with no detailPagePath; returns {success:true} noop in both cases
- Deduplicates concurrent calls via inFlightRef
- Gets auth token via apiService.getAuthTokenForExternalUse() with null guard
- Exposes: createPages(opts), status, localeResults, error, reset

## EventForm.tsx UX
- handleSave and executeSaveWithUrl: check result.daWarning after saveDraft()
  returns; show toast.info() with 10s duration so it is visually distinct
  from the success toast (amber info vs green success). Draft save still
  shows success toast — the warning is additive, not a replacement.

## Bug fixes
- daClient.ts readFromDA: removed incorrect Content-Type:text/html from GET
  request headers (Content-Type describes request body, not response format);
  replaced with Accept:text/html
- daPageService.ts listPagePaths: replaced raw fetch() with listDaPath() from
  daClient so the ALLOWED_HOSTS allowlist and dry-run/nonInvasiveTest guards
  are honoured consistently. listDaPath throws on non-OK; wrapped in try/catch
  to preserve the existing empty-array fallback behaviour.

## Barrel exports
- services/da/index.ts: re-exports all public symbols from daFetch, daClient,
  helixClient, daPageService (including DaPageCreationInput/Result types)
- utils/daPage/index.ts: re-exports all public symbols from placeholders,
  paths, localization, dom

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three test suites covering the pure-JS (no DOM, no Node) utility layer of
the DA page creation engine:

### placeholders.test.ts (37 tests)
- isPrimitive: null, string, number, boolean (true); object, array, function (false)
- camelToKebab: standard conversion, already-kebab input, consecutive-uppercase
  boundary behaviour (only splits at [a-z][A-Z] transitions, not consecutive caps)
- getMetadata: top-level key, deeply-nested key, nested array, missing key
- parseRegularPath: colon-delimited array indexing, dot-delimited object walk,
  out-of-bounds index fallback, missing key fallback
- replacePlaceholders: simple [[key]], nested [[parent.child]], special
  photoURL→photo.imageUrl mapping, missing-key passthrough, colon-syntax
  array placeholder, multiple replacements in one string
- resolveArrayPlaceholders: found with exists:true, no placeholder → null,
  empty array → exists:false
- updateFragmentPaths: link/relativeValue/absoluteValue types, unmatched
  placeholder passthrough, indexed array placeholders

### paths.test.ts (13 tests)
- handleExtension: .docx strip, .xlsx→.json, index.docx→folder-index,
  normalization + lowercase, leading/trailing hyphen strip
- constructFragmentsFolderPath: simple, deep, no-leading-slash paths
- getLocalizedTemplatePath: unchanged when no locale, locale inserted after
  first segment (fr, uk), throws on non-slash-prefixed path
- getRelativeEventPagePath: default locale, missing detailPagePath → null,
  non-default locale includes folder prefix

### localization.test.ts (17 tests)
- mergeData: scalar, override precedence, nested objects, array-by-index
- removeLocalizationObjects: removes localizations/localizationOverrides from
  top-level and from array items and nested objects
- mergeLocalization: merges locale overrides end-to-end; leaves data unchanged
  when no locale match
- isSameDate: same day different times → true, different days → false
- extractCustomAttributes: from customAttributes[], sorted by displayOrder for
  promotionalItems, publishingProfile.metadata fallback, does not mutate original
- fillMissingFields: adds missing photos/series/localizations, preserves existing

All tests pass (67/67). Jest 29 + ts-jest in node environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…te, DRY

Three blocking fixes and three warnings from post-commit review:

## Blocking

**Series data injection (useEventFormSave.ts — step 7a)**
`getEventFull` fetches event + speakers + sponsors + venue + images but never
fetches the series record. `isDocumentAuthoringEvent` reads `series.targetCms.code`;
`daPageService` reads `series.templateId` and `series.contentRoot`. Without this
both values are always `undefined` and the entire DA block was dead code.
Fix: after `getEventFull`, if `hydratedEvent.series` is absent, call
`cachedApi.getSeriesFull(seriesId)` and inject the result. The series is already
cached from form initialisation, so this is a near-zero-cost cache hit.

**UTC gmtOffset=0 (localization.ts — getDisplayDateTime)**
`if (...&& gmtOffset)` is falsy for `0` (UTC). Any event hosted in UTC would
silently return `undefined` from `getDisplayDateTime`, leaving the
`[[display-date-time]]` placeholder unresolved in the DA page template.
Fix: `gmtOffset !== undefined && gmtOffset !== null`.

**helixDelete raw fetch (helixClient.ts)**
`helixDelete` bypassed `daFetch` — and therefore the `isValidUrl` ALLOWED_HOSTS
guard and dry-run gate — with a comment claiming "daFetch throws on non-ok, so
handle separately." HTTP 204 has `response.ok === true`, so daFetch never would
have thrown. The special-case was based on a false premise.
Fix: replace with a plain `daFetch` call.

## Warnings

**speakerHubFragmentMap omitted from event page (daPageService.ts)**
Main event page `createOrUpdateDaPage` received only `{ ...sessionHubFragmentMap }`.
If the event page template references speaker-hub fragments via
`[[fragment.speaker-*]]` placeholders, those cross-references would silently
fail. Fix: `{ ...sessionHubFragmentMap, ...speakerHubFragmentMap }`.

**Dead HELIX_OPERATIONS entries (daConfig.ts)**
`CACHEPURGE: 'Purge'` and `DELETE_PREVIEW: 'deletePreview'` were never used —
`helixPurgeCache` and `helixDeletePreview` hardcode their path segments.
Passing them to `bulkHelixOperation` would silently fall through to PREVIEW
(the else branch). Removed both constants.

**joinPath duplicated across 4 files (paths.ts / daClient.ts / dom.ts / daPageService.ts)**
Exported `joinPath` from `utils/daPage/paths.ts` (updated to accept
`(string | undefined | null)[]` with `filter(Boolean)` so callers with optional
parts continue to work). Removed local copies from `daClient.ts`, `dom.ts`, and
`daPageService.ts`. Removed the now-redundant re-export of `constructFragmentsFolderPath`
from `dom.ts` (it's already in the `daPage/index.ts` barrel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Actionable guide for the three other teams whose repos must be updated
before the old Kinesis→ESL→Hoolihan DA pipeline can be retired.

Covers exact file paths, function names, and before/after code diffs for:
- events-service-layer: accept + persist + forward pageCreatedBy field
- events-service-platform/ServiceProcessor.js: skip DA webhook tagging
  when pageCreatedBy==='emc' (behind SUPPRESS_DA_WEBHOOK_FOR_EMC env gate)
- events-platform-hh-webhooks/index.js: defensive early-return on flag;
  documents that checkFileOwner already guards via modified-by:EMC marker

Includes rollout order (ESL first — hard prerequisite), per-environment
verification checklist, and retirement steps once cutover is stable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers the cleanup pass once backward compatibility is no longer needed —
i.e. after all DA event pages in prod carry modified-by:EMC and the old
webhook has been retired long enough to be confident nothing will write
DA_WEBHOOK pages again.

EMC removals: fire-and-forget pageCreatedBy follow-up PUT, pageCreatedBy
from EVENT_DATA_FILTER and EventApiResponse, modified-by:EMC addPageMarker
call, EMC_MARKER + DA_MARKER constants, series injection step 7a.
Includes a note on the simplification opportunity: moving the
isDocumentAuthoringEvent check to before the save using form context data,
which eliminates the getSeriesFull round-trip entirely.

Cross-repo removals: pageCreatedBy + targetCmsProvider from ESL schema and
kinesis payload; DA-provider forwarding block + SUPPRESS gate + constants
from ServiceProcessor.js; hoolihan-da-webhook action directory + secrets
if not already deleted in the prior retirement phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant