Skip to content

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 until RepairLine: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 ​

KindWhatOwner
ProducerBuilt: 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
Schedulen8n "Repairline dates and schedules - Enqueue", every 3 hn8n
Queuesbq-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 — readWebAPI GET {WebApi:BaseUrl}/Prime/job/{id}/GetJobDates → jobId, jobNumber, clientReference, clientId + value and …Changed for contractSent, contractReturned, expectedStartDate, expectedEndDate, actualStartDate, actualEndDateBlazorApps WebAPI
Data — writeWebAPI POST {WebApi:BaseUrl}/Prime/job/{id}/jobdatehistory, body keys ContractSent, ContractReturned, ExpectedStartDate, ExpectedEndDate, ActualStartDate, ActualEndDate (ISO)BlazorApps WebAPI
CredentialsPortals:RepairLine:Username/:Password, WebApi:*Key Vault
MFADelivered 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)
BrowserContext Portals:RepairLine:ContextId; fixed AU IP required via Portals:RepairLine:ProxyBrowserbase + proxy

Flow ​

  1. Resolve (no browser). GET …/GetJobDates. No record → attempt/no_job_dates_record; no clientReference → 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.
  2. Sign in (https://repairline.com.au, Blazor Server + Telerik). Signed in = dashboard tabs Home, New Jobs, Current Quote Requests, Current Authorities. Else Portals: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.
  3. 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).
  4. 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.
  5. 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. Never FillAsync, 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.
    • (dryRun stops 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.
  6. Witness. Fresh load, find the job again (step 3), re-read the rows. Not as written → attempt/date_not_persisted.
  7. History. POST …/jobdatehistory with the dates now on the portal → complete/reconciled; failure → attempt/history_post_failed.

Deadline 14 min; session timeout 15 min.

Outcomes ​

OutcomeReasons
completereconciled, save_refused, duplicate_authority_rows, already_correct, nothing_to_write, missing_required_date, no_active_authority
rejectinvalid_payload
attemptrows_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-dates return 202 + a run id and run the job in the background; GET /actions/runs/{id} returns the outcome (natural home: the planned dbo.BrowserActionsRun table).
  • [ ] 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 Browserbase keepAlive: true and 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:ConsumerEnabled a slot-sticky setting, false in 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 twin BNES_…) 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_date before 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) — RepairLineBrowser keeps 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_date before 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/Sedgwick works 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.