WORKFLOW-APP/scripts
Dave c349998fa1 feat: Task Kanban UI + Python task worker
Adds the visible front-end and the worker back-end for the agent
task system. The PocketBase schema, API routes, and ToastProvider
were already in the previous commit; this commit completes the
picture.

UI
- app/tasks/{page,KanbanBoard,TaskCard,CardModal,TaskDetail,api,columns}
  Full Kanban with six columns (Backlog, Ready, In Progress,
  Blocked, Done, Failed), drag-and-drop between columns, quick-add
  chips, type/priority filters, task detail panel with retry/cancel/
  delete, 5s polling, transition toasts.
- components/NavBar.tsx: 'Tasks' link in primary nav.
- app/bizportal-2/BizPortal2Explorer.tsx: 'Send to Kanban' button
  on the result view — dispatches the current search to the agent
  queue as a 'ready' task.

Back-end
- scripts/task_worker.py: PocketBase polling worker with two
  lanes. Deterministic handles bizportal_search/reserve, pdf_generate,
  email_send. Custom lane writes to /tmp/agent_inbox/ for the
  LLM agent to pick up. Atomic claim via PB filter, auto-retry up
  to 3x, structured log on each task.
- scripts/bizportal_service.py: bugfix — _is_logged_in was using
  a non-CSS selector so session-recovery was a no-op. Also made
  the login flow re-runnable mid-session (handles BizPortal's
  ~20-minute session timeout transparently).
- scripts/init_pb_schema.py: duration_ms no longer capped at 0.

Verified end-to-end:
- CK search via worker: 1 company, 3 directors, ~10s
- ID search via worker: 4 companies, 7 total directors, ~34s
- Auto-retry + status transitions + toast fires
2026-08-17 18:57:02 +02:00
..
.env.example feat: BizPortal 2.0 — Python Playwright sidecar 2026-08-17 18:33:36 +02:00
.gitignore feat: BizPortal 2.0 — Python Playwright sidecar 2026-08-17 18:33:36 +02:00
bizportal_service.py feat: Task Kanban UI + Python task worker 2026-08-17 18:57:02 +02:00
init_pb_schema.py feat: Task Kanban UI + Python task worker 2026-08-17 18:57:02 +02:00
README.md feat: BizPortal 2.0 — Python Playwright sidecar 2026-08-17 18:33:36 +02:00
requirements.txt feat: BizPortal 2.0 — Python Playwright sidecar 2026-08-17 18:33:36 +02:00
task_worker.py feat: Task Kanban UI + Python task worker 2026-08-17 18:57:02 +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).