Appearance
Action: RepairLine — sync job dates
Status: built, in testing (dry runs on the live portal, 11 Oct 2026). What it does now is in BrowserActions-Spec.md "RepairLine sync-dates (consumer)"; this file keeps the history, the rules' reasons and what is still open. Port of stagehand-automations/apps/new-repair-line-dates-and-schedules. Shared rules are in README.md; shares the RepairLine account and send budget with repairline-submit-job.md.
Writes Prime's changed job dates onto the job's Key Dates in Sedgwick's RepairLine portal, re-reads them after a fresh load, then posts the date history back to Prime.
Entry points
- Queue
sbq-repairlinedates-prod-australiasoutheast-001,Subject = "sync-dates"— producer and consumer built (consumer off untilRepairLine:SyncDates:ConsumerEnabled). POST /actions/repairline/sync-dates— payload +dryRun?.
Payload
json
{ "primeJobId": "<guid>", "primeJobNumber": "<optional>" }primeJobId required, a GUID. Else dead-lettered invalid_payload.
Dependencies
| Kind | What | Owner |
|---|---|---|
| Producer | Built: spRepairlineGetSyncDatesJobTasks (this repo) — same eligibility as BlazorApps' spEthRepairlineJobDatesQueueTasks, returns messageId, subject, body rows; n8n calls POST /producers/repairline-sync-dates every 3 h, which runs the proc and queues the rows. The BlazorApps proc and its SQL work queue stay untouched until cutover. | this repo |
| Schedule | n8n "Repairline dates and schedules - Enqueue", every 3 h | n8n |
| Queue | sbq-repairlinedates-prod-australiasoutheast-001, read with MaxConcurrentCalls = 1. Sync-dates only — submit-job shares the RepairLine account, so it must go through this queue (branch on Subject) or both need a shared lock. Old dedupe: Prime job id, 24 h. MessageId = sync-dates:<primeJobId>; duplicate-detection window 1 day. At-least-once is fine (reads before writing). | this repo |
| Data — read | WebAPI GET {WebApi:BaseUrl}/Prime/job/{id}/GetJobDates → jobId, jobNumber, clientReference, clientId + value and …Changed for contractSent, contractReturned, expectedStartDate, expectedEndDate, actualStartDate, actualEndDate | BlazorApps WebAPI |
| Data — write | WebAPI POST {WebApi:BaseUrl}/Prime/job/{id}/jobdatehistory, body keys ContractSent, ContractReturned, ExpectedStartDate, ExpectedEndDate, ActualStartDate, ActualEndDate (ISO) | BlazorApps WebAPI |
| Credentials | Portals:RepairLine:Username/:Password, WebApi:* | Key Vault |
| MFA | Delivered email, 6 digits. n8n ingests it into dbo.EthMFAAsAService row service_provider_name = 'Sedgwick' (Portals:RepairLine:MfaServiceProviderName); the API reads it from WebAPI GET api/Mfa/Sedgwick (floor = updatedAt before submit). Send budget account repairline, portal cap 3/hour shared with submit (old split: dates 2, submit 1). | n8n (ingest), BlazorApps WebAPI (read) |
| Browser | Context Portals:RepairLine:ContextId; fixed AU IP required via Portals:RepairLine:Proxy | Browserbase + proxy |
Flow
- Resolve (no browser).
GET …/GetJobDates. No record →attempt/no_job_dates_record; noclientReference→attempt/no_job_reference. Keep only changed rows that can be written; none →complete/nothing_to_write. A required date missing →complete/missing_required_date. - Sign in (
https://repairline.com.au, Blazor Server + Telerik). Signed in = dashboard tabs Home, New Jobs, Current Quote Requests, Current Authorities. ElsePortals:RepairLine:LoginUrl(/Account/Login?ReturnUrl=%2F): "Contractor Login" → email → password → reserve send → submit → 6-digit code → submit. Decline MFA enrolment if offered. No teardown URL to block. - Find job. Search tab: type the reference in "Search:", then press the separate Search button (two actions). Columns: reference, Contractor Ref, Request Type, Status, Date Requested, Date Accepted. Exactly one row with Request Type "Repair Authority" and Status "Incomplete": none →
complete/no_active_authority; several →complete/duplicate_authority_rows. Open it ("Open"), confirm the reference on the page (wrong_job). - Read Key Dates. Expand "Key Dates" (sections arrive collapsed differently per job and re-collapse on every reload). Rows: "Date Contract Sent", "Contract Returned", "Estimated Commencement", "Estimated Completion", "Commencement Date", "Date Completed". Never touch "Date Due for Completion". All equal →
complete/already_correct. - Write at most 2 differing rows per run, inside the deadline (rest →
attempt/rows_remaining, the next message finishes them).- Picker recipe (Telerik masked
dd/MM/yyyy, one server round trip per key, drops keys typed during one): click the field,Control+A,PressSequentiallyAsync("ddMMyyyy", Delay = 600 ms),Tab, wait 1500 ms. NeverFillAsync, never press Home — a corrupted row's picker stops opening. - "Commencement Date" with an existing date: "Modify original start date" → "New date" field (same recipe) + "Reason for the date change" =
Date change from Prime. - Read back via the row's "Open" button: popup header shows the month as a name, read the selected day,
Escape. - (
dryRunstops here.) "Save and Exit". Banner "You need to add … before submitting" →complete/save_refused. Ignore "An unhandled error has occurred" — it appears whether or not the save landed.
- Picker recipe (Telerik masked
- Witness. Fresh load, find the job again (step 3), re-read the rows. Not as written →
attempt/date_not_persisted. - History.
POST …/jobdatehistorywith the dates now on the portal →complete/reconciled; failure →attempt/history_post_failed.
Deadline 14 min; session timeout 15 min.
Outcomes
| Outcome | Reasons |
|---|---|
| complete | reconciled, save_refused, duplicate_authority_rows, already_correct, nothing_to_write, missing_required_date, no_active_authority |
| reject | invalid_payload |
| attempt | rows_remaining, date_not_persisted, history_post_failed, job_not_found, wrong_job, no_job_dates_record, no_job_reference, prime_error, irreversible_write_refused (dryRun), send_budget_exhausted, mfa_code_stale, mfa_code_rejected, sign_in_failed, bot_challenge, rate_limited, portal_step_failed, deadline_exceeded |
Open / blockers
TODO — hosting the long-lived session in an Azure web app (from 11 Oct 2026 review; the queue worker is a hosted service, so no HTTP timeout applies to it):
- [ ] Manual endpoint is synchronous and will time out. App Service's front end cuts requests at ~230 s; the dry runs took 230–250 s and a run that writes takes up to ~15 min. Make
POST /actions/repairline/sync-datesreturn202+ a run id and run the job in the background;GET /actions/runs/{id}returns the outcome (natural home: the planneddbo.BrowserActionsRuntable). - [ ] Idle CDP connection may be dropped. Between jobs the connection to Browserbase can sit idle for up to 10 min; Azure drops idle outbound TCP at ~4 min by default, and a non-keep-alive Browserbase session ends when its client disconnects (→ next job fails, new session, another MFA code). Add a heartbeat every 1–2 min while idle that doesn't touch the portal (e.g. a no-op
EvaluateAsync), and/or open the session with BrowserbasekeepAlive: trueand reconnect to the same browser (same IP, same login) on a dropped connection. - [ ] App Service: Always On. Without it the app unloads after ~20 min with no HTTP traffic, stopping the worker and killing the session.
- [ ] App Service: exactly one instance, no autoscale. Two instances = two sessions, double MFA codes, two writers persisting one Browserbase context.
- [ ] Deployment slots: make
RepairLine:SyncDates:ConsumerEnableda slot-sticky setting,falsein staging, so a swap never runs two consumers. - [ ] Restarts (deploys, platform recycling) lose the session; the next job spends a code. A job interrupted mid-run is safe — before Save nothing was saved and the message is redelivered; after Save the retry finds the dates already there and posts history. Confirm graceful shutdown closes the session (
RepairLineBrowser.DisposeAsync) within the host's shutdown timeout. - [ ] Portal-side login expiry while the session idles is caught by the per-job sign-in check (costs a code). Measure how long RepairLine keeps an idle login; jobs arrive in bursts (producer every 3 h) so expect ~1 session/code per burst with the 10-min idle close.
Findings from the port (11 Oct 2026):
Selectors captured from a read-only Browserbase session (keep-alive, one sign-in) on references 60333168, 60335072, 60333583, 60335847 — see BrowserActions-Spec.md. The old notes' column list was out of date (grid: Status, Contractor, Request Type, Contractor Ref, Sedgwick Ref, Date Requested, Date Accepted, Date Due, Date Completed, Job Location); Commencement Date is an ordinary editable date until a change is recorded.
The proxy IP held for a ~25-minute keep-alive session with no dropped Blazor connection.
The login cookie
.RepairLine.Auth.Application.v2(and its WAF twinBNES_…) is session-only, so the persisted Browserbase context comes back signed out. Restoring the cookies into a new session does not work either (tested 20:36 and 20:40 on 11 Oct 2026: the second session, on a new proxy IP, was signed out and spent a second code) — the login is bound to the client IP. So on Browserbase's rotating proxy every session costs one MFA code, and the account allows 3 an hour.Live dry run works end to end (J015212/60333583, session e6fd00cf…, 253 s): sign-in with emailed code, search, open, six-row read; settled
complete/missing_required_datebefore typing. Contract Sent read 17/09/2026, matching Prime's 2026-09-17 04:37 UTC converted to Melbourne.Throughput: one code per session made one code per job (≤ 2–3 jobs an hour). Built: option (a) —
RepairLineBrowserkeeps one signed-in Browserbase session (60 min) open and runs every job in it. A fixed AU IP / allowlist (b) would still help: the in-memory cookie restore was removed because it can't survive a new IP.The send limit is tighter than it looks: sign-ins at 20:36 and 20:40 delivered codes within a minute; a third at 20:54 delivered none within 150 s (or by 20:57). Treat 2 codes per rolling hour as the practical ceiling until Sedgwick says otherwise; the in-process cap is 2/hour.
Most eligible jobs on dev have an empty Estimated Commencement, so they settle
missing_required_datebefore typing — as the old lane learnt.Still to prove on the live portal: a real write + Save & Exit + witness on a job someone picks, and the audited Commencement change.
Fixed AU IP (blocker). RepairLine binds two login cookies to the client IP and its firewall blocked the fixed Azure egress address. On a rotating residential proxy the Blazor connection drops mid-run (seen ~2.5 min in, code spent, nothing written). Browserbase's residential proxy rotates the same way. Needs a dedicated AU residential IP or a Sedgwick allowlist, then
Portals:RepairLine:Proxy.Tested 2026-10-10 on Browserbase (AU residential proxy, Singapore): login page reachable, sign-in + emailed code via WebAPI
api/Mfa/Sedgwickworks in one session (proxy IP held steady within the session). Selectors:input[name='Input.Email'],input[name='Input.Password'], button "Contractor Login" →/Account/LoginWith2faEmail:#two-factor-code(Input.TwoFactorCode), button "Log in". Leftover cookies from an earlier session made the code post fail with HTTP 400. Rule: whenever signed out, clear cookies before filling the form; if a sign-in post still returns 400, clear cookies and sign in again (once, and only while the send budget allows — each attempt sends a code). The form has no "Remember me" (its checkbox is "Show Password"). A new session reusing the persisted context comes back signed out — not yet known whether because the login cookie is session-only (dropped when the browser closes) or because it is bound to the IP (each session gets a new one). Next test: sign in, list cookie expiries, copy the cookies into a session on another IP. If that stays signed in, save and re-add the cookies ourselves and no fixed IP is needed for saved logins.bot_challenge: a WAF challenge. Browserbase captcha solving is on; the old lane gave it 45 s then failed and paused both queues.

