18-Apr-2026 — Ramblers-upload-worker → Integration-worker #114
build 614 — commit 40e8d62
Rename the Ramblers upload worker to a generic integration worker, cut every
in-process browser session out of the website, and replace the buffered
Serenity StageCrewMember step-delivery path with a Playwright-native reporter
that posts each test.step as it ends. Clean up env-var wiring, settings
surface, and scripts along the way.
Rename: ramblers-upload-worker → integration-worker
The worker now handles Ramblers walks upload, Flickr album scraping,
arbitrary HTML fetches and full static-site migration - "upload worker"
no longer describes it. Rename every piece of the worker plumbing;
keep walks-upload-specific files (dispatcher, queue, session registry,
audit notifier, execution model) named ramblers-upload-* because they
describe one type of integration job.
- Fly app:
ngx-ramblers-upload-worker→ngx-ramblers-integration-worker - Fly config:
fly.worker.toml→fly.integration-worker.toml - Manual deploy workflow:
build-and-deploy-ramblers-upload-worker.yml→build-and-deploy-integration-worker.yml - Docker image tags:
ngx-ramblers-worker:N→ngx-ramblers-integration-worker:N; push tagngx-ramblers:worker-N→ngx-ramblers:integration-worker-N; local tagngx-worker:local→ngx-integration-worker:local; local containerngx-worker-local→ngx-integration-worker-local - Env vars:
RAMBLERS_UPLOAD_WORKER_*→INTEGRATION_WORKER_* - Types:
RamblersUploadWorker*→IntegrationWorker* - Files: 12 files in
server/lib/ramblers/(integration-worker-{client,server,routes,runner,callback-client, execution-state,crypto,crypto.spec}) plusserver/deploy/deploy-integration-worker.tsandprojects/.../integration-worker.model.ts - Route prefix:
/api/ramblers-upload-worker/*→/api/integration-worker/*
Sync browser ops moved to the worker
POST /api/integration-worker/browser/html-fetch- launches Chromium,
navigates, returns
{ html, finalUrl, baseHref }. Used byhtml-from-url,fetchHtmlFromUrlandsearch-venue-websiteon the website. POST /api/integration-worker/browser/flickr-user-albums- scrapes a
Flickr albums page (TrustArc consent handled via Playwright
frameLocator) and extractsFlickrScrapedUserAlbumsData. - New
integration-worker-browser-client.tson the website: signs withsignRamblersUploadBody, POSTs to the worker, falls back to plainfetchif the worker errors out. flickr-provider.scrapeUserAlbumsPagebecomes a thin wrapper callingscrapeFlickrUserAlbumsViaIntegrationWorker; the ~180-line in-process page scrape + cookie-consent iframe dance moved to the worker.migration-routes.tsdropslaunchPlainBrowser()entirely. TheUSE_SERENITY_FOR_MIGRATIONfeature flag and its env entry go with it.
Full site migration moved to the worker (async job + callbacks)
POST /api/integration-worker/migration/jobsaccepts{ jobId, siteConfig, persistData, uploadTos3, callback }, runsmigrateStaticSite()on the worker, streams progress back via signed callbacks. Refuses a second job while one is running.- New model types:
IntegrationWorkerMigrationJobRequest,IntegrationWorkerMigrationProgressCallback,IntegrationWorkerMigrationResultCallback. - New backend callback endpoints at
/api/integration-worker/migration/progressand/result. Each verifies the signature, looks up the active migration session, and forwards{ level, message }or the final{ status, result, errorMessage }to the client websocket. - New
migration-session-registry.ts - in-memory map of
jobId → { ws, siteIdentifier, siteName, historyId, startedAt }. - New
migration-notifier.ts - websocket send + audit-log / history-status persistence, split from the old ws handler so the callback route and initial handler share it.
site-migration-ws-handler.tsno longer importsmigrate-static-site-engineor callsmigrateStaticSitedirectly. It resolves the site config, creates themigrationHistorydoc, generates ajobIdviacrypto.randomUUID(), registers a migration session, and submits the job to the worker viasubmitMigrationJobToIntegrationWorker.
Real-time step streaming: fix and DRY up the delivery path
The original DomainEventPublisher StageCrewMember (48aca026) worked
under WebdriverIO because WDIO fires InteractionFinished /
TaskFinished synchronously as each action executes. Under
@serenity-js/playwright-test those domain events are buffered by
PlaywrightEventBuffer and only announced to the Stage at onTestEnd
- so our crew member only ever saw them in one end-of-run batch, which is why the audit view only populated when the browser closed.
Replace the whole per-step delivery path with a Playwright-native
Reporter (bypasses Serenity's buffer):
server/lib/serenity-js/reporters/realtime-step-reporter.ts- listens toReporter.onStepEnd, filterscategory === "test.step", and POSTs each step to/api/integration-worker/progresswith the existingtest-stepenvelope (eventData.timestamp/details.name/ outcome.code). Registered inplaywright.config.tsalongside the Serenity reporter.
Backend handler, session registry and audit notifier tightened so the real-time stream behaves correctly:
/progresshandler now responds200 OKimmediately, then processes the audit asynchronously. Eliminates the 1-3 s per-request latency that was queueing test-step POSTs and bunching them at end-of-run./progressfortest-stepnow unwraps the{ eventData, finished }envelope before handing the inner event toparseTestStepEvent, so the parser sees realtimestamp/details.name/outcome.codeagain (the old path was passing the envelope, so every event came out with a NaN timestamp and got clustered into one write-time window).sendAuditis now serialised per-session via a promise chain. Each background callback runs strictly after the previous one, sosession.recordincrements monotonically instead of racing (no more runs ofrec=8, rec=8, rec=8).updateRamblersUploadSessionmutates the session object in place (Object.assign) instead of replacing it in the map, so all capturedsessionreferences stay in sync with the incremented record.
DRY: drop the old WDIO-era event transport entirely
With RealtimeStepReporter as the single per-step path, the old
WebSocket-based transport and its wiring are dead weight:
- Deleted:
server/lib/serenity-js/screenplay/crew/DomainEventPublisher.tsserver/lib/ramblers/process-test-step-event.tsserver/lib/ramblers/integration-worker-websocket-server.tsserver/lib/websockets/websocket-client.ts- Removed
EventType.TEST_STEP_REPORTERfrom the sharedwebsocket.model.ts. - Dropped the
TEST_STEP_REPORTERhandler from the backendwebsocket-server.tsand thecreateIntegrationWorkerWebSocketServercall from the worker server.
Environment enum: move to shared model, prune, enforce
Environmentenum moved toprojects/ngx-ramblers/src/app/models/environment.model.tsso the Angular app can use it too; deleted the server shim atserver/lib/env-config/environment-model.ts; all 28 server importers rewritten to the shared path.- Added
ADMIN_MONGODB_URIandPLAYWRIGHT_HEADLESSto the enum. - Every literal
process.env.X/env.X/secrets.Xaccess in the server now goes throughEnvironment.X- deploy script, CLI commands, worker routes. - Frontend
environment-settings.tsnow uses[Environment.X]computed keys instead of string literals.
Local CLI ergonomics
- New
--no-headlessflag onngx-cli local dev. Runs the worker headed (visible browser) and implies--no-docker-worker(Docker always headless). Existing--no-docker-workerstill works for fast-feedback headless iteration without an image rebuild. docker-workerandheadlessthreaded throughLocalRunConfig.- Force-kills stale processes on backend / frontend / worker ports
before each start (
SIGTERMwith 4 s grace, thenSIGKILL); tears down any stale worker container.
Walk export UI
autoTitleadded to the<app-page>wrapper so the tab view has an H1 above the tabs, via the same mechanism used by Bookings / Member Bulk Load / Environment Setup.- Audit sort state persisted in the URL as `?sort=
&sort-order=
<asc|desc>`:
- New
StoredValueentriesAUDIT_MESSAGE,AUDIT_TIME,DURATION,STATUS(kebab-case values in the URL). - Reuse existing
StoredValue.SORT+SORT_ORDERas the query keys andSortDirection.ASC/DESCfromsort.model.tsfor the values (no duplicate"asc"/"desc"constants). sortAuditsBy(field: StoredValue)now flips order on the same column and resets toDESCon a new column, then persists viaupdateUrl().AUDIT_SORT_FIELD_MAPPINGmaps the URL/StoredValuekebab-case to the internal camelCase field thesortByhelper reads.- Duration column:
durationMsis now computed on every audit unconditionally (theshowDetailgate was stopping the field from being added when the checkbox was off, which was why the Duration header sort did nothing).dateUtils.formatDurationno longer short-circuits to "0 secs" whenfromTime === 0- that falsy-zero guard was wrong for millisecond-delta inputs and was making every row render "0 secs" regardless of the real duration.applyFilterhandles the oldest row (no previous) cleanly instead of computingauditTime - 0.
Scripts cleanup
server/package.json:
- Dropped
test: "failsafe serenity report" - duplicated
serenity. - Dropped
lint/lintfix/lintversionand thetslintdep +server/tslint.json. tslint has been unmaintained since 2019 and none of the hooks or CI workflows invoke them.
server/test-setenv.sh: dropped stale CHROME_BIN /
CHROMEDRIVER_PATH exports.
server/.env: local key names renamed to INTEGRATION_WORKER_*;
stale Chrome paths removed. (Local values only; remote Fly secrets
re-imported under the new names separately.)
Dead code removal
server/lib/migration/migrate-static-site.ts(standalone NWK script, unreferenced).server/lib/migration/serenity-migration-engine.ts,serenity-migration-utils.ts,serenity-utils.tsand thescreenplay/scaffolding (parallel implementation, not imported by any live route).server/lib/migration/puppeteer-utils.tsreplaced byserver/lib/migration/browser-utils.ts(only compiled into the worker image - never called from the website live path).
Fly secrets
Staged but not deployed on the two apps available to the current token:
ngx-ramblers-upload-worker: newINTEGRATION_WORKER_SHARED_SECRET/INTEGRATION_WORKER_ENCRYPTION_KEYstaged with matching digests to the old names.ngx-ramblers: newINTEGRATION_WORKER_SHARED_SECRET/INTEGRATION_WORKER_ENCRYPTION_KEY/INTEGRATION_WORKER_URL/INTEGRATION_WORKER_CALLBACK_BASE_URLstaged.
Staging central DB config.environments for staging, ekwg and
ekwg-dev-2-staging renamed the per-env secret keys from
RAMBLERS_UPLOAD_WORKER_* to INTEGRATION_WORKER_* so future
deploys push the new names consistently.
Old secrets remain Deployed on both apps until the next deploy activates the new staged names.
worker: switch upload worker to Serenity/JS Playwright stack and cut the website off WebdriverIO/Puppeteer (#114)
Migrate the Ramblers upload worker off the WebdriverIO + Chrome/Chromedriver runtime onto the officially supported Serenity/JS Playwright image, harden the end-to-end upload path, and remove every WebdriverIO/Puppeteer usage from the codebase so the website image no longer ships a browser stack.
Worker image
Dockerfileworkerstage now builds fromghcr.io/serenity-js/playwright:v1.58.1-noble(Node 24 pinned vian, npm 11, OpenJDK, Playwright browsers pre-installed on Ubuntu 24.04). Chrome/Chromedriver/@puppeteer/browsersinstalls are removed - the Playwright image already ships the browsers the worker needs.npm run serenity-bdd-updateis pre-run so runtime doesn't re-download the Serenity-BDD JAR..dockerignoreexcludesserver/node_modules,cli-output/,target/,ts-gen/anddist/so worker images stay lean.
Website image
- Renamed
--target originto--target websiteto reflect its role as the pure web tier. Build argCHROME_VERSION, OpenJDK 21 install andserenity-bdd-updateare removed from this stage - the website no longer runs Serenity or any browser. - CI workflow now builds with
--target websiteand no longer passes--build-arg CHROME_VERSION. - Dockerfile header comment updated to state the website stage ships neither a browser nor a JDK.
Test runner migration (WebdriverIO → Playwright)
server/package.jsonserenity-runnow invokesplaywright test -c ./playwright.config.ts. Adds@playwright/test,playwright,@serenity-js/playwrightand@serenity-js/playwright-test.- New
server/playwright.config.tswires the Serenity crew, BDD reporter,ArtifactArchiverandPhotographer, usingSerenityFixtures/SerenityWorkerFixtures. HonoursPLAYWRIGHT_HEADLESS,RAMBLERS_FEATUREandBASE_URL. - Feature specs (
contacts,process-command-args,walks-and-events- manager,walks-programme,walks-upload) migrated off mocha /actorCalledstatics to the@serenity-js/playwright-testactorCalledfixture;beforeEachengagement is replaced with per-scenarioactorCalled(...). SaveBrowserSourcetask replaces the WDIO@wdio/globalscall withBrowseTheWeb.currentPage().executeScript(...)and now writes totarget/browser-source/with a fallback page on capture failure.- New
SetFileInputtask drives file uploads via Playwright's nativesetInputFiles, replacingEnter.theValue(...)and fixing the "Input of type file cannot be filled" failure on the Ramblers upload modal. - Login wait restored to
Wait.upTo(DEFAULT_WAIT_TIMEOUT).until(...)sourcing the 15 s budget from a single shared constant inserver/lib/serenity-js/config/serenity-timeouts.ts(also consumed byplaywright.config.ts). Fixes the 5 sTimeoutExpiredErroron the post-login Create-menu dropdown check.
Bulk-action selector hardening
WalksPageElementstightens theDelete/Unpublish/Publish/Uncancelselectors to#vbo-action-form-wrapper input[data-vbo='vbo-action'][value='...'], eliminating ambiguous matches when the VBO action bar renders twice.- Adds a
loggedInWalksManagerPageelement (body CSS class check) used insideAuthErrorCookieBannerOrCreateMenuDropdownso post-login detection succeeds even before the Create menu renders.
Live audit streaming (in progress)
- Server spawns the Serenity subprocess with
RAMBLERS_UPLOAD_WORKER_CALLBACK_BASE_URL,_CALLBACK_PROGRESS_PATH,_CALLBACK_RESULT_PATH,_CALLBACK_SECRETand_JOB_IDin the environment (newEnvironmentenum entries). DomainEventPublisherpostsTEST_STEPevents directly to the worker callback endpoint when that env is present, with diagnostic logging on the transport branch actually taken, instead of going through the Serenity subprocess websocket (which intermittently dropped with close code 1006 and caused audit rows to bunch at end-of-run).processTestStepEventnow writes step events straight into the active local session so the Walk upload audit tab updates while the browser is still running.- Frontend
WebSocketClientServicewrapsonopen/onmessage/onclose/onerrorinNgZone.run(...)so progress events trigger change detection.walk-export.tscallsChangeDetectorRef.detectChanges()on updates, broadcastsNamedEventType.REFRESHon completion, and runs a 2 s audit refresh loop while the export is in progress.
Audit parser & timing clean-up
ramblers-audit-parserstrips ANSI escape sequences via module-levelRegExps built fromString.fromCharCode(27)(no inline control characters, passesno-control-regexwithout a disable) and filters separator / noise lines such asExecution Summary,Scenarios:,Real time:,Total time:,1 passed,Succeeded with exit code 0 as all scripts passed,> playwright,@playwright/test,@serenity-js/playwright/-test,Running 1 test using 1 worker, themkdir -p target/site/archiveline and all-=separator lines.walk-export.tstiming()no longer has a special SUMMARY case that returned the whole-session duration; summary and step rows now both show true per-step durations derived fromdurationMsor the adjacent-row delta.ramblers-upload-walks.tsprogress line reportsPLAYWRIGHT_HEADLESSandBASE_URLinstead of the removedCHROME_BIN/CHROMEDRIVER_PATH.
Report storage (zipped artefacts)
ramblers-upload-worker-runner.tsuploads the Serenity report as a single<keyPrefix>.zipviaadm-zip, emitting lifecycle progress events for archive start, completion (with file count, byte size and elapsed ms) and failure.recordReportLocationstores the summary message ass3://<bucket>/<keyPrefix>.zip.- New
GET /api/aws/report/:bucket/*route inaws-routes.tsstreams assets from the archive:aws-controllers.tslazy-extracts each report undertarget/report-cache/<sha1>/, guards against directory traversal, dedupes concurrent extractions via an in-memory promise cache, and falls back to a chunkedstreamToBufferfor SDK response bodies that aren't alreadyBuffers. walk-export.tsopenReport()uses the new route withbucket+keyPrefix+/_/+index.htmlas the split marker; sessions without a stored bucket are no-ops.
WebdriverIO / Puppeteer removal across the codebase
@serenity-js/webdriverio,webdriverio,puppeteerand every@wdio/*runner/CLI package removed fromserver/package.json; the lockfile shrunk by ~4 200 lines.- Deleted
server/wdio.conf.ts,server/lib/serenity-js/features/config/ actors.ts,server/lib/migration/puppeteer-utils.tsand the parallelserenity-migration-engine.ts/serenity-migration-utils.ts/serenity-utils.tsstack plus theirscreenplay/scaffolding and the standalonemigrate-static-site.tsscript (none were imported by any live route). - New
server/lib/migration/browser-utils.tsprovides a Playwrightchromium.launch(...)replacement for the old puppeteer helper. migrate-static-site-engine.tsnow importsBrowser/Pagefromplaywright, swapsnetworkidle2fornetworkidle, and restructures everypage.evaluate(fn, arg1, arg2, ...)call for Playwright's single-argument contract (object-payload pattern).migration-routes.tscollapses the dual WebdriverIO / Puppeteer branches, drops theUSE_SERENITY_FOR_MIGRATIONfeature flag and its env entry, and uses Playwright-only paths.flickr-provider.tsis on Playwright:browser.url/waitUntil/execute/getPageSource/$becomepage.goto/waitForFunction/evaluate/content/locator, and cookie-consent iframe handling is reimplemented withpage.frameLocator(...)rather thanswitchToFrame.Environmentenum dropsCHROME_BINandCHROMEDRIVER_PATH; theramblers-audit-parsernoise list replaces the webdriverio marker with@serenity-js/playwright.
Local dev ergonomics
bin/ngx-cli local devforce-kills stale processes holding the backend / frontend / worker ports (SIGTERMwith a 4 s grace, thenSIGKILL), and tears down any old worker container before each run.- Docker worker rebuild trigger now watches
server/playwright.config.ts,server/lib/ramblers,server/lib/serenity-jsandserver/lib/env-configrecursively, via a newnewestMtimeMshelper, so nested source edits trigger a rebuild. - When
PLAYWRIGHT_HEADLESS=falseis set, the in-process worker path is used (headed debugging) instead of the Docker worker, andnpm exec playwright install chromiumis ensured with output streamed to both stdout and the worker log. - Backend env injects
RAMBLERS_UPLOAD_WORKER_CALLBACK_BASE_URL=http://host.docker.internal:<port>when the Docker worker is used. - Worker Docker build logs are streamed to the terminal as well as the worker log file.
CI
build-push-and-deploy-ngx-ramblers-docker-image.ymlbuilds a separate worker image taggedworker-${run_number}whenever worker-relevant files changed, deploys it with--image-tag worker-${run_number}, and now builds the website image with--target website(no Chrome build arg).- Manual
build-and-deploy-ramblers-upload-worker.ymlpoints at theworkerbuild stage. server/deploy/detect-worker-changes.tstreatsserver/playwright.config.tsas worker-relevant and dropsserver/wdio.conf.tsfrom its watch list.