Running "npm run dev" as a tracked agent task gets the process killed
when the runtime cap is reached, which leaves the machine with no dev
server on port 6001. This script starts it with Start-Process so it
survives session churn, and it refuses to start a second server when
the port is already taken.
The older .run-dev.ps1 in the same folder is broken and is replaced by
this one: it invokes the Next binary without its extension, which
PowerShell cannot execute, so node.exe is called explicitly instead.
Reconcile CIPC BizPortal against the Workflow database for every
director ID number we hold, and report only the two kinds of
difference that need a decision:
1. new appointment - CIPC lists the person as a director of company
X, the database does not.
2. no longer a director - the database lists the person at company X,
CIPC no longer shows them there.
Everything that is in sync is not reported, so the list stays short
enough to work through by hand.
- New page /cipc-report with an explorer that runs the check, polls
the run and lists the exceptions.
- New API route /api/cipc/report: POST starts a background batch run
of the existing "Bizportal Directopr id no search.py" (invoked
exactly as it already behaves, including its own login rotation),
GET returns run status and the exception lists. ?demo=1 returns a
fixed sample so the screen can be shown without a live run.
- New action route /api/cipc/report/action for the per-exception
follow-up actions.
- scripts/cipc-report/build_report.py renders the run into a PDF with
the exceptions marked, plus the sample report used by ?demo=1.
The BizPortal tooling only exists on the Windows dev machine, so like
the other CIPC routes this tab cannot run the live search from the
Coolify Linux image.
The mobile bottom bar rendered all 15 routes into a single row. On a
390px iPhone that works out at ~17px per tab: the icons overlap, every
label truncates to a single letter and the tap targets sit far below
the 44px minimum, so the tabs could not be read or hit reliably.
Mobile now uses a hamburger in the header that opens a full-screen menu
listing every section, grouped (Main / CIPC & BizPortal / SARS /
Practice). The panel locks background scroll, closes on Escape or on
navigation, and carries the theme toggle so the mobile chrome keeps
parity with the desktop cluster.
Desktop had the same problem in a different shape. The full 15-link row
needed 1752px inside a 1280px container, so the page scrolled sideways
even at 1920px (measured scrollWidth 2065 vs a 1920 viewport) and the
right-hand cluster was pushed off screen. The inline nav now carries
six primary links plus a "More" dropdown, which fits exactly.
- NavBar: mobile tab bar replaced by the header menu panel; desktop nav
reduced to primary links + More dropdown; grouped list shared by both
so the two surfaces cannot drift apart.
- RouteChrome: dropped the pb-24 md:pb-0 padding that only existed to
clear the old bottom bar.
- .gitignore / Workflow/.dockerignore: ignore .next-stale* so a stale
build directory cannot bloat the Docker build context.
Add a simple yes/no tick box on the far right of every table in the
SARS registrations tab, so SJ can tick off each client as the period's
return is filed and confirm nothing was missed at end of month.
- New PB collection sars_submission_flags (pbc_3539276419): company
relation, return_type select, period (YYYYMM), submitted bool,
submitted_at date. One row per (company, return_type, period).
- New route /api/sars-submission-flags with GET (list), POST (upsert,
server owns submitted_at) and DELETE (Clear all for a period).
Hard-allowlisted fields and a fixed collection id.
- Explorer gains a period banner, a submitted/total counter, a
Submitted column on desktop and a tick box on the mobile cards,
and a Clear all button guarded by a confirm.
The counter is scoped to the rows actually on screen. A flag can
outlive the filter that surfaced its company (a client can lose its
PAY/E number), and counting those made the banner read 4/18 while
only 3 boxes were ticked.
Period is the current month and rolls over on its own; Clear all is
what resets a type between periods. Per-month history is deliberately
out of scope for this first pass.
CIPC/BizPortal search drives a real Chrome session through agent-browser
to log into BizPortal and scrape the result pane. That toolchain exists
only on the Windows dev machine; the Coolify image is a Linux container
with no browser tooling (the Dockerfile header already says so).
Nothing detected that up front, so a search on the deployed URL fell
through to the legacy spawnSync fallback, which builds a Windows-style
interpreter path even on Linux (.venv/Scripts/ plus python). The shell
answered 'not found' with exit 127, extractError() found no marker line
in stdout and returned 'Unknown error', and the UI rendered that over
the raw shell message - a dead end with no action in it.
Now the route checks for the interpreter it needs before doing any
work, and returns 503 with a sentence naming the real cause and where
to run the search instead. Checked per request so it cannot go stale.
Verified: typecheck clean; a real enterprise search of K2022808158 on
the local dev server still returns 200 in 87s with 3 directors, so the
guard does not touch the working Windows path.
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.
middleware.ts previously 404'd every external request, which meant the
app was unusable once hosted. There are no per-user accounts, so this
adds a single-password gate:
- /login page + POST /api/auth/login, POST /api/auth/logout
- lib/auth.ts: HMAC-SHA256 signed session cookie (lib/auth), Web Crypto
only so the same code runs in Edge middleware and Node handlers
- middleware now admits: the signing flow (unchanged, still public),
/login + /api/auth/*, local requests, and valid sessions. Everything
else redirects to /login (pages) or returns 401 JSON (API calls).
- password compared with a constant-time compare, wrong attempts
delayed 600ms to slow online guessing
- cookie is HttpOnly + SameSite=Lax, 12h, Secure when x-forwarded-proto
says https (Coolify serves the proxy on http today)
APP_PASSWORD / AUTH_SECRET added to .env.example, with a warning: an
unquoted # in an env value starts a comment, which silently truncated
the first generated password and made login fail.
Verified against a real built server: anon->307 /login, anon API->401,
wrong password->401, correct password->200+cookie, signed-in request
reaches PocketBase, /resign/ still 200 without a cookie, forged
cookie->401.
next build runs 'Linting and checking validity of types' and fails on
the first type error, so the Coolify image build died at
app/api/cipc/search/route.ts:640. These errors were pre-existing but
only surface on next build (npm run dev does not type check).
- cipc/search/route.ts: coerce spawnSync stdout/stderr with toString();
result is typed as the un-overloaded return, so they are string|Buffer
- group-report pdf+email: use fetchCollection<YsterbaardGroup> instead of
an invalid cast; normalise description null -> undefined for ReportData
- CipcExplorer (cipc + cipc-2): split the state ternary into two returns;
the RowState union correlates state with differs, so 'diff' | 'match'
matched neither variant
- ComplianceTreeGrid: ar_non_compliance_date is optional; the API omits it
and this component never reads it
Verified: tsc --noEmit 0 errors, next build exit 0, standalone output ok.
- add .dockerignore: without it the build context shipped .env.local
(PB_TOKEN + CIPC passwords), node_modules and the full .git history
- bake NEXT_PUBLIC_PB_URL / NEXT_PUBLIC_BASE_URL / NEXT_PUBLIC_APP_URL
as build args; NEXT_PUBLIC_PB_URL was read by client components but
never supplied, so browser-side PocketBase calls would break
- docker-compose: same build args + Telegram runtime env
- document that agent-browser / PDFD sidecar routes stay on the PC
PB returns 400 when sorting a freshly imported collection by autodate system
fields (created, updated) — those fields aren't yet exposed for sorting.
id always works.