Workflow-App/Workflow/scripts
sjcoetzeesj-coder 92dcae5122 fix(cipc): stop deadlocking wrapper on agent-browser daemon start
runAb() in scripts/_cipc_service.js resolved only on proc.on('close'),
but agent-browser is a daemon-per-session CLI. When it has to START the
session browser daemon (first call after a reboot, after the daemon died,
or after the app restarts), the new daemon inherits the spawned CLI
stdout pipe. The pipe then never reaches EOF, so Node 'close' event
never fires - even though the CLI exited cleanly with code 0. The 30s
timeout in runAb called proc.kill(SIGTERM), but the process had already
exited, so the kill was a no-op and the promise never settled. The
wrapper serialised mutex then held inFlight forever, every later request
queued behind it, and CIPC search silently 500-ed for days with no log
line to show why - which is what made this look like a flaky / restart-
only issue.

Fixes:
  - runAb now settles on 'exit' (with a 250ms flush grace for stdio to
    drain), escalates the timeout kill to SIGKILL, and guarantees an
    unconditional forced settle if neither fires. The promise can no
    longer remain pending.
  - app/api/cipc/search/route.ts: ensureWrapperService() now probes port
    6002 on every search and respawns the wrapper (rate-limited to once
    per 30s) if it is down. Hot reloads still no-op once a wrapper is up,
    but a dead wrapper now self-heals instead of 500-ing forever.

Verified end to end: killed the browser daemon and the wrapper, started
a fresh wrapper, its first isWarm() spawned a new daemon (the previous
deadlock path) and returned in under 1s. A real enterprise search of
K2018435959 came back in 31.7s on the cold path and 50.9s on the warm
path with full company + director data.
2026-10-19 13:40:36 +02:00
..
__test_capture.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_add_field.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_capture_periods.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_check_apostrophe.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_check_deems.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_check_signature_images.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_cipc_service.js fix(cipc): stop deadlocking wrapper on agent-browser daemon start 2026-10-19 13:40:36 +02:00
_clear_ufc_documents.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_compare_doc_text.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_compare_docs.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_create_vat_collection.js feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_create_vat_v2.js feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_date_test.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_dump_doc_text.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_inspect_collection.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_inspect_tesh.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_null_test.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_patch_vat_fields.js feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_patch_vat_schema.js feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_probe_res_docs.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_probe_res_docs2.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_regex_test.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_schema_check.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_schema_inspect.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_schema_test.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_show_table.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_upload_signed_resolution.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_verify.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_verify_pb.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
_verify_signed_now.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
backfill_client_status.mjs feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
BACKUP_SARS login Company spesific_20261008-181711.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
Bizportal Directopr id no search.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
Bizportal Enterprise No. Search.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
Bizportal Search.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
bizportal_service.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
build_resolution_master.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
build_shareholder_master.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
check-mail.ps1 feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
cleanup_ab.ps1 feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
init_pb_schema.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
manage_sidecar.sh feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
mark_otp_required.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
otp_fill.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
pb_migration_shareholder_certificates.mjs feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
pb_migration_shareholder_change.mjs feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
README.md feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
requirements.txt feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
SARS login Company spesific.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
SARS retun EMP201 push to PB.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
SARS Retun EMP201 single or all pty.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
SARS Retun History Period.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
SARS retun VAT201 push to PB.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
SARS Retun VAT201 single or all pty.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
SARS Return EMP201 data.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
sars_login.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
SARS_login_Select_Company.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
sars_login_service.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
SARS_retun_History_select.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
SARS_Return_Search.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
screenshot_dashboard.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
smoke_phase6.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
subst.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
subst_shareholder.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00
task_worker.py feat: commit Workflow app source; move CIPC credentials to env 2026-10-18 15:21:45 +02:00

BizPortal 2.0 — Python Playwright service

Sidecar that drives https://www.bizportal.gov.za via a real headless Chromium. The Next.js side talks to it over HTTP — no LLM agent, no file queue, no polling. Sits next to the existing /bizportal page without touching it.

Why this exists

The existing /bizportal page queues searches to /tmp/bizportal_queue/ and an LLM agent reads the page snapshot, decides what to click, and writes the result back. Fast enough when it works, but the agent is the brittle bit — it misreads the page, times out, or clicks the wrong button. This service replaces that with a deterministic Python script.

Architecture

Browser  ─►  /bizportal-2  ─►  /api/bizportal-2/*  ─►  this service  ─►  BizPortal
                                                            │
                                                            └─►  PocketBase (via service)

The Python service holds a persistent storage_state.json so the user is logged in once and every subsequent request reuses the session.

Setup

cd scripts
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium
cp .env.example .env   # fill in real credentials

Run

# Dev
uvicorn bizportal_service:app --host 127.0.0.1 --port 7001 --reload

# Production (Coolify / systemd)
uvicorn bizportal_service:app --host 127.0.0.1 --port 7001 --workers 1

Keep --workers 1 — the script uses a single shared browser context to preserve the logged-in session.

Endpoints

GET /health

{
  "ok": true,
  "service": "bizportal-2",
  "playwright_ready": true,
  "logged_in_cached": true,
  "headless": true
}

POST /search

{ "type": "ck", "query": "2022/808158/08" }
{
  "ok": true,
  "query": "2022/808158/08",
  "type": "ck",
  "results": [
    {
      "enterprise_no": "2022/808158/08",
      "enterprise_name": "AMPLEFORTH TRADING (PTY) LTD",
      "enterprise_type": "Private Company",
      "enterprise_status": "Business is in Business",
      "directors": [
        {
          "id_number": "870922XXXX08X",
          "names": "DAVID",
          "surname": "ELS",
          "status": "Active",
          "type": "Director"
        }
      ]
    }
  ],
  "log": ["Navigating to login", "..."]
}

POST /reserve

{ "names": ["AMPLEFORTH PTY LTD", "AMPLEFORTH TRADING", "AMPLEFORTH SA", "AMPLEFORTH HOLDINGS"], "dry_run": false }
{
  "ok": true,
  "balance_before": 412.50,
  "status": "bizportal_done",
  "log": ["..."],
  "message": "Submitted via Bizportal. The R50 fee will be debited later."
}

Tunable selectors

BizPortal uses ASP.NET WebForms (cntMain_* IDs). The known-stable IDs for login and name reservation are baked in. The search field IDs are not 100% verified — try a search after starting the service once and tune the DIRECTOR_SEARCH_FIELD_CANDIDATES / ENTERPRISE_SEARCH_FIELD_CANDIDATES lists at the top of bizportal_service.py if you get a "Could not find …" error.

The page URL is logged in the error so you can inspect the live page in your browser and grab the right field ID.

Resetting the session

rm scripts/storage_state.json
# Restart the service — it will log in once and re-cache the session.

Running as a systemd service

[Unit]
Description=BizPortal 2.0 Python service
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/workflow/scripts
EnvironmentFile=/var/www/workflow/scripts/.env
ExecStart=/var/www/workflow/scripts/.venv/bin/uvicorn bizportal_service:app --host 127.0.0.1 --port 7001 --workers 1
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

What this does NOT replace

  • The existing /bizportal page (kept untouched)
  • The /api/bizportal/search/* queue routes (kept untouched)
  • The /api/register/[id]/apply-bizportal route (kept untouched)
  • The PocketBase collections and bulk-add / link logic

If you decide to retire the old flow, see scripts/README.rollback.md (TODO if you want one).