Appearance
Action: Nexus — update estimated completion date
Status: proposed. Port of stagehand-automations/apps/new-nexus-automation (its README records why most steps are the way they are — read it before building). Shared rules (queue, outcomes, MFA, credentials) are in README.md.
Sets the Completion Date on every in-scope sub-job of a project in Auto & General's supplier portal to Prime's estimated completion date (ECD), verifies it from the timeline, then records the watermark in Prime.
Entry points
- Queue
nexus,Subjectnone. Message body = payload. POST /actions/nexus/update-ecd— payload +dryRun?.
Payload
json
{ "primeJobId": "<guid>", "primeJobNumber": "<optional>" }primeJobId required, trimmed, non-empty; primeJobNumber optional, display only. Anything else → reject/invalid_payload.
Dependencies
| Kind | What | Owner |
|---|---|---|
| Producer | spEthNexusECDQueueTasks — one message per job whose normalised EndDate differs from LastECD. Today ends in spEthWorkQueueEnqueue_Preview; needs to SELECT rows instead (see README). | BlazorApps |
| Schedule | n8n "Nexus ECD Update - Enqueue" (o9JbWWZXjN67NJJj), every 3 h at :11 | n8n |
| Queue | nexus, duplicate detection window 12 h, MessageId = primeJobId (matches BlazorApps migration 0034_NexusQueueDeduplication12Hours). At-least-once is fine: the write is idempotent (read before, skip if already correct). | this repo |
| Data — read | WebAPI GET {WebApi:BaseUrl}/Prime/job/{primeJobId}/ecd → { jobID, clientReference, endDate }; 404 or empty body = no record | BlazorApps WebAPI |
| Data — write | WebAPI POST {WebApi:BaseUrl}/Prime/job/{primeJobId}/ecdhistory, body { "EcdDate": "<iso>" }, Content-Type: application/json (else 415). Upserts via spPrimeUpsertECDHistory. | BlazorApps WebAPI |
| Credentials | Portals:Nexus:Username/:Password, WebApi:* | Key Vault |
| MFA | Delivered SMS to one phone. Portals:Nexus:MfaUrl = n8n mfa-as-a-service/get-mfa-latest-email/unity ("Get Latest MFA Code For Service", iTrH8w5mgxZWrlxQ). Send budget account nexus, 3/hour, shared with A&G staff — so the new budget must leave headroom. | n8n |
| Browser | Context Portals:Nexus:ContextId. AU residential proxy (rotating is OK today). The old lane also used a fixed egress IP for the saved login — prove whether Unity cares. | Browserbase |
Flow
- Resolve (no browser).
GET …/ecd.- No record / no
endDate/ noclientReference→attempt(no_ecd_record,no_usable_date,no_project_reference). endDate≥ 2099-12-31 (Prime's "no date") →POST …/ecdhistorywith that date →complete/sentinel. ~70% of messages end here.- Else target =
{ projectReference: clientReference, date: endDate (date only) }.
- No record / no
- Sign in (
https://unity.autogeneral.com.au). Route-block{origin}/graphql/v2/clear-site-data*before navigating (the app wipes its own storage otherwise). Go to/pulse/suppliers/projects; signed in = projects list or account name visible. Otherwise the auth0 login at/login: supplier login option → (remembered-account screen: continue) → email → password (maybe separate screens) → reserve send → submit → "remember this browser" → code → submit. Decline passkey enrolment if offered. - Find project. Projects search field: type reference, Enter. Open the row whose reference matches exactly; confirm the reference on the project page (
wrong_project). - List sub-jobs. "Jobs" tab; columns Job Type, Job Reference, Assigned to, Last Updated, Status, Actions. Walk pages (cap 25; stop when a page repeats). Keep rows with Job Type "Assess and Repair" or "Quote Only" and Status "Repairs in Progress" (case-insensitive). None →
complete/no_targets. - Per sub-job: open via the row's "VIEW" button (not the caret beside it). Page is
/pulse/suppliers/jobs/<id>/detailsand paints blank until reloaded — reload, re-read after 6 s if still blank. Confirm it's the expected sub-job (wrong_sub_job_opened→complete, nothing written). - Write. "OVERVIEW" tab → "Completion Date" (
input[type=date]— not "Appointment Date"). Already equal → skip. ElseFillAsync("yyyy-MM-dd"), read the value back, (dryRunstops here) press the header "SAVE". - Verify. "TIMELINE" tab; if the table is empty, turn on "SHOW SYSTEM-GENERATED RECORDS" (its own on/off state is unreliable — judge by the rows). Columns Date, Type, Summary, Description, User, Action. Newest "Job field updated" row → its "VIEW" → "View Field Updates" panel shows the new value as
yyyy-MM-dd→ "CLOSE". Mismatch →attempt/date_not_persisted. - Watermark. Only when every in-scope sub-job is verified or already correct:
POST …/ecdhistory→complete/reconciled. Failure →attempt/watermark_unavailable.
Whole-run deadline 14 min (deadline_exceeded); Browserbase session timeout 15 min.
Outcomes
| Outcome | Reasons |
|---|---|
| complete | sentinel, no_targets, wrong_sub_job_opened, reconciled |
| reject | invalid_payload |
| attempt | irreversible_write_refused (dryRun), wrong_project, date_not_persisted, coverage_incomplete, watermark_unavailable, no_ecd_record, no_usable_date, no_project_reference, send_budget_exhausted, mfa_code_stale, mfa_code_rejected, sign_in_failed, bot_challenge, rate_limited, portal_step_failed, deadline_exceeded |
Open / to prove
- Saved login survives in a Browserbase context. The token that matters is in IndexedDB (
localforage, ~9 h life). The old worker seeded IndexedDB from/robots.txtbefore the app booted. If the context keeps the full profile this all disappears; if not, restore viaAddInitScript. Prove it first — every failed restore spends an SMS. - Capture real locators for every label above from one signed-in session.
- Order: first action to port.

