The modal's submit handler wrapped every optional field with
"if (value.trim()) fields.x = value.trim()". When the user
deleted the content of a field, the value was empty, the guard
returned false, and the field was OMITTED from the PATCH entirely.
PB then left the existing value in place.
Symptom: edit a field, delete its content, save. The DB still
has the old value.
Fix: always send every field. Empty string ("") clears a value
in PocketBase (verified end-to-end). Required-field validation
(surname, first_names, enterprise_no, enterprise_name) still
guards against empty required values up-front.
Also fixed the modal's React Query invalidation to target the
list pages' actual keys (["companies"], ["directors"]) in
addition to the incorrect ["pb-list"] key. This makes the
list page refetch after save.
Same fix applied to DeleteConfirmModal for consistency.
When the edit modal was opened from /companies or /directors (the
list pages) and the user saved, the modal called
onAfterSave={() => {}} — an empty handler. The modal also fired
qc.invalidateQueries(['pb-list', collection]), but the list
page's queryKey is ['companies', ...] (or ['directors', ...]),
not ['pb-list', ...]. So the list page never re-fetched.
Result: stale row data persisted in the list state. Re-opening
the modal showed the original values, making it look like the
save hadn't taken effect (it had — the data was in PB; the list
just didn't know about it).
Fix: wire onAfterSave to the existing refreshAll() helper in
both list pages so the list refetches from PB and the local
editingCompany/editingDirector state stays in sync.
The /api/pb/records/[id] proxy had an allowlist of editable fields that
was written before the 2026-08-18 onboarding field additions. The new
fields were silently dropped before reaching PocketBase, so the modal
appeared to save successfully but the data never landed in PB.
Added to COMPANY_EDIT_FIELDS:
physical_address_line_1/2, physical_town_city, physical_province,
physical_postal_code, postal_address_line_1/2, postal_town_city,
postal_province, postal_postal_code, company_email, company_cell_phone,
public_officer_name, company_secretary_name, financial_year_end_month,
authorised_shares
Added to DIRECTOR_EDIT_FIELDS:
residential_address_line_1/2, residential_town_city, residential_province,
residential_postal_code, postal_address_line_1/2, postal_town_city,
postal_province, postal_postal_code, date_of_birth
End-to-end tested via proxy PATCH — fields now land in PB.
PB: 3 new fields on ysterbaard_director_appointments
- proceed_anyway (bool) — true if user proceeded with missing emails
- missing_emails (text) — comma-separated keys
- missing_emails_detail (text) — JSON per-recipient validation
App:
- POST /api/appointments: builds recipient report (initial +
distro); returns 422 + report if proceed_anyway is missing
and any recipient is missing/invalid. preview=1 returns the
report without sending.
- PATCH /api/appointments/[id]: tightened idempotency guard to
also short-circuit on signature_received (not just
mail_sent_to_all). Prevents double-send on double-click.
- DirectorAppointmentTab: two-step modal — "Validate recipients"
surfaces the report (status dots + per-recipient email),
then Send / Proceed anyway / Fix records first. New
"company_email" gap surfaced before any email leaves.
- lib/format.ts: isValidEmail() shared helper.
End-to-end tested: 422 enforces server-side, preview returns
the report, double-PATCH signs only once.
PB: 15 fields on ysterbaard_companies (financial_year_end_month,
authorised_shares, structured physical/postal address, company_cell_phone,
public_officer_name, company_secretary_name), 11 fields on
ysterbaard_directors (date_of_birth, structured residential/postal address).
App: types widened in lib/types.ts. EditEntityModal rebuilt with full
address sections (line 1, line 2, town/city, SA province dropdown,
postal code), DOB, company-level fields, financial year end month,
authorised shares, public officer, company secretary.
View pages: structured address blocks now show labeled fields
(line 1, line 2, town/city, province, postal code) on both
/companies/[id] and /directors/[id].
- New /Volumes/KIOXIA/.hermes/profiles/ysterbaard/scripts/pdfd.js
standalone HTML->PDF daemon on localhost:3070, calls soffice via
execSync (proven working pattern from resignation form watcher.js)
- app/api/appointments/route.ts generates acceptance-letter.pdf via
daemon, attaches via MIME multipart/mixed
- app/api/appointments/[id]/route.ts renders signed-letter.pdf with
embedded signature, attaches to distro mail
- Idempotency guard: skip distro if status is already mail_sent_to_all
A task with status='' caused the Kanban byColumn memo to throw
TypeError: undefined is not an object (out[t.status].push).
Defensive UI: bucket unknown statuses into backlog with a console
warning. Defensive API: reject PATCHes with invalid status values
(including empty string) with a 400. Cleaned up two existing
empty-status rows in PB.
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
- DirectorAppointmentTab: table + 'Acceptance Letter' modal with company/director dropdowns
- GET/POST /api/appointments: create record, generate HTML letter, convert to PDF via LibreOffice, email to director via himalaya SMTP
- PATCH /api/appointments/[id]: save signature PNG, embed in signed letter PDF, mail to all company directors
- GET /api/appointments/sign/[token]: public token lookup
- /sign/[token]/page.tsx: public signature canvas (mouse + touch)
- ysterbaard_director_appointments PB collection (requires manual schema setup)
- Section 3 'Director Appointment' now active in ResolutionExplorer
- NEXT_PUBLIC_APP_URL added to .env.local
Uncommitted work in progress at the point of starting the Kanban
project. Captures:
- Registration wizard refactor + multi-step routing
- structured physical/postal address fields on companies and directors
- appointments flow (sign + verify)
- document type manager + upload/panel for companies and directors
- status actions, approve button, action/dropdown buttons
- step2 review page
tsconfig.tsbuildinfo is now ignored (build artefact).
Replaces the brittle agent-driven /api/bizportal/search queue with a
deterministic Python FastAPI service that drives BizPortal via a real
headless Chromium. The old flow is untouched.
What this adds:
- scripts/bizportal_service.py — FastAPI + Playwright service with
/search, /reserve, /health endpoints. Persistent login session
via storage_state.json. Locator-based selectors (vs eval JS).
- app/bizportal-2/ — new /bizportal-2 page, synchronous fetch
(no polling), shows round-trip duration, mirrors the existing
bulk-add + link UX.
- app/api/bizportal-2/{search,reserve,health}/route.ts — thin
proxies to the Python sidecar.
- scripts/README.md, .env.example, .gitignore, requirements.txt.
- .gitignore: ignore tsconfig.tsbuildinfo (build artefact).
Wiring:
- components/NavBar.tsx: 'BizPortal 2.0' nav entry with TEST badge.
- app/bizportal/page.tsx: 'Try BizPortal 2.0' action link in
header so the new option is discoverable from the existing page.
Run: cd scripts && source .venv/bin/activate && uvicorn
bizportal_service:app --port 7001
Add a conditional cleanup step to remove the public directory if it does not exist or contains no files. This reduces the final docker image size since Next.js standalone builds automatically inline public assets when they are present, making the copied public directory often redundant.