UI notes — needs outside the UI scope (for the control-plane owner)
BLOCKER — packages/core/src/papers.ts related-papers query
getPaperBySlug builds sql\${paperVersions.keywords} && ${row.v.keywords}`. Drizzle expands the JS array as a tuple (&& ($4,$5,$6)), so PostgreSQL rejects the query and **every paper page returns 500/404** (/research/[slug]and/api/public/papers/[slug]). Verified fix (one line, import arrayOverlapsfromdrizzle-orm`):
or(eq(papers.discipline, row.p.discipline), arrayOverlaps(paperVersions.keywords, row.v.keywords));The e2e suite was run with this fix applied temporarily (25/25 pass); the original file was restored afterwards and
packages/core rebuilt from it, so the paper page is broken again until the fix lands.
Tooling
biome.json: add"css": { "parser": { "tailwindDirectives": true } }soapps/web/src/app/globals.css(@import "tailwindcss",@theme,@apply,@custom-variant) parses; todaypnpm lintreports parse errors there. Alsobiome migrate(schema 2.4.0 vs CLI 2.6.4;recommendeddeprecated).- pnpm 11 "deps status check" aborted
pnpm start/pnpm exec playwrightafter dependencies changed (ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY→ it triedpnpm install --production). Workaround used: run binaries directly (apps/web/node_modules/.bin/next,node_modules/.bin/playwright). Considerverify-deps-before-run=falsein.npmrcfor non-interactive hosts (pm2). next startwarns thatoutput: "standalone"expectsnode .next/standalone/server.js(copy.next/staticandpublicinto the standalone dir on deploy). It still serves correctly; the pm2 manifest should pick one of the two.- Rebuilding
.nextwhile anext startprocess is still serving leaves it with a stale client manifest ("Could not find the module …link.js in the React Client Manifest" → 500 on every page withnext/link). Always stop the server beforenext build(or build to a fresh dir and swap). packages/db/package.json: addedpdf-lib(used only bysrc/seed.tsto generate the staging PDF).
Server helpers / API observations (not changed)
clientIp()returnsnullwhenTRUST_PROXY=0and no proxy header is present (Next does not expose the socket address), so locally all visitors share the limiter keys"unknown"/"public", and for/api/internal/*the CIDR allow-list is skipped (only the bearer token protects it). In production the worker reaches the control plane directly on the LAN (no proxy headers) → same situation. If a LAN-only guard is wanted, bind the internal API to a separate port/listener or set the socket address via a custom server.- Internal worker routes answer 403 (
viaPublicProxy) to Playwright's request context because it sendsx-forwarded-*-free requests… actually they return 403 for any request whose headers include forwarded hints; the e2e asserts 401 or 403 for those routes. - The in-memory login limiter (10 logins / 15 min per username) is easy to hit while developing/testing; the e2e suite reuses one signed-in storage state to stay below it. A restart of the web process resets it.
listEvents()returnsDateobjects;redact()turns aDateinto{}(it walks it as a plain object). The run page converts dates before redacting; the/api/admin/runs/[id]/eventsroute passes rows straight throughredact()→atbecomes{}in the JSON. Suggestredact()to passDate/Bufferinstances through unchanged, or serialise before redacting in that route.- Public artifact route sets
accept-ranges: none; the browser PDF viewer works but cannot seek by byte range on large PDFs.
Seed
pnpm db:seed → seedTopicsAndSeries() always (17 disciplines + 10 subfields + series as-wp), seedStagingPaper() only with STAGING_MODE=1 (one synthetic paper AS-WP-2026-0000, papers.synthetic=true, 6-page PDF written under ARTIFACT_ROOT/public/AS-WP-2026-0000/v1/manuscript/main.pdf with a .sha256 sidecar). Production (STAGING_MODE=0) never lists, serves or indexes it. Remove it before launch with delete from papers where public_id='AS-WP-2026-0000' (cascades) + delete the public object.