PR #137 shipped a code change under App/app with no test — the test-presence
gate in pr-checks.yml would reject it, but the watcher opened the PR anyway.
Add the same gate to the watcher's fix phase: before pushing/opening a PR, run
the pr-checks.yml test-presence check against the branch diff. If code under
App/(app|lib|components|hooks) changed with no accompanying test, the watcher
does NOT open a PR — it marks the issue claude-failed and comments, so the
queue can retry. Never raises a PR the CI would immediately fail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add NEXT_PUBLIC_SUBMITTER_VIEW_ALL_ENABLED=true to the staging env so
submitters (TECHNICAL/MANNING) can read all POs and open the History page
on the staging instance for testing ahead of a prod rollout. The flag is
written into the freshly-provisioned .env heredoc and also appended
idempotently to already-provisioned staging .envs on the next refresh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the hardcoded PROJECT_CODES array with an admin-managed
`ProjectCode` model, mirroring the Delivery Locations pattern (PR #100):
- ProjectCode model (unique `code` + isActive) + migration seeding the
five previously-hardcoded codes; PO.projectCode stays a free-text
snapshot (no FK) so history/exports/imports are unchanged.
- manage_project_codes permission (Manager + SuperUser + Admin).
- /admin/project-codes CRUD screen (table + Add/Edit + activate/delete)
and an Administration sidebar link.
- ProjectCodeField now takes `options` from the active codes; the three
PO forms + pages fetch them from the DB. Static list removed.
- Unit test reworked to the options API; CRUD integration test added;
documented in App/CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Report detail pages now link to the underlying POs, addressing the PR #126
review comment: drilling into a cost centre or accounting code opens PO
History pre-filtered to that dimension and the period in view.
- Cost Centre / Accounting Code detail pages gain a "View POs" link.
- periodRange() maps the on-screen period onto History's approved-date
window (weekly→month, monthly→FY, yearly→full span); spend is dated by
approvedAt.
- PO History gains an accountId filter (any tree node, expanded to leaves
via accountLeafIds()) matching PO-level OR line-item accounts — the same
basis the reports use.
- History page + CSV/PDF export share one buildPoHistoryWhere() builder so
they never diverge.
- Tests: unit (periodRange, accountLeafIds) + integration (History account
filter across PO-level/line-item, with the approved window).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First live run on PR #126 left the foreground shell stuck: Claude pushed the
commit itself and then did not exit, so the supervisor code after it (push +
ack + handled marker) never ran. Under cron that also holds the flock lock,
freezing every later run, and the comment never gets marked handled (so it
would be re-processed forever).
- Wrap the Claude invocation in `setsid timeout -k 30s "$CLAUDE_TIMEOUT"`
(default 30m). `setsid` detaches from the controlling terminal so a lingering
child can't stick an interactive run; `timeout` returns control to the
supervisor, which still pushes any commits (idempotent if Claude pushed) and
writes the handled marker. A timed-out run (rc=124) is logged, not fatal.
- Give this watcher its own dev port (devPort, default 3101) distinct from the
issue watcher's 3100, and reap it after each run -- no cross-watcher kill.
- Reinforce the prompt: stop any dev server and END THE TURN; never push.
Adds claudeTimeout + devPort to the example config and documents both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
update-pr-review-watcher.sh refreshes the deployed script from the repo via a
dedicated self-update checkout (~/pr-review-watcher/.src) that never races the
issue watcher's work clone. Reads the live config for auth, never clobbers the
config (real token), and self-updates. Optional ref arg for pre-merge testing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The repo CHANGELOG had fallen behind. Adds [Unreleased] entries for the recently
shipped work and a README for the previously undocumented PdfService.
Added (changelog):
- Reports — Purchasing spend analytics (cost centres + accounting codes).
- Email PO to vendor + the PdfService microservice (cached per PO).
- EpfoService + PdfService microservices and their release auto-deploy.
- Unsaved-changes prompt on PO create/edit (#18).
- Crew login-on-hire for management ranks.
- Delivery Locations (#19), T&C catalogue (#11), advance payment (#92).
Fixed (changelog):
- "Email to vendor" never rendered (auth middleware bounced the svc fetch) — #127.
- Reports charts all one colour (RSC client/server boundary) — #120.
New: PdfService/README.md (endpoints, token/origin security, env, app integration).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues surfaced on first live run:
- The per-run cap truncated the PR list to the lowest-numbered PR, so a
comment on a higher-numbered PR was never scanned. The cap now limits only
how many PRs Claude RUNS on; comment-less PRs are skipped for free, so newer
PRs are never crowded out.
- The bot posts as the repo owner's account, so excluding "the bot's own
comments" by author also excluded the owner's legitimate review comments
(and required a read:user token scope, which 403'd). Replaced with a guard
that excludes only comments carrying the HANDLED_TAG marker -- robust even
when the bot and the reviewer are the same account. The /user call is gone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sibling to claude-issue-watcher.sh: polls open Claude-raised PRs (head
branch under claude/, or labelled claude-pr) for review comments carrying
the marker `claude-review:` — in the PR conversation, review summaries, or
inline on-file comments — and runs headless Claude Code on the PR's own
branch to address them, pushing the follow-up commit(s) to the same branch.
- Authorization gate: only repo collaborators (write access) + the owner can
trigger it; the bot's own comments are ignored.
- Idempotent: handled comments are tracked by a hidden marker on the bot's
acknowledgements, so the 10-min poll never redoes a comment.
- Own clone (~/pelagia-pr-review), config, and lock so it never races the
issue watcher. Token needs write:repository + write:issue.
Adds the script, an example config, .gitignore entries for the live
config/lock, and an automation/README.md section with deploy + cron steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously every "Email to vendor" click re-rendered the PO via PdfService and
re-uploaded to R2 under a timestamped key — wasteful, and it orphaned a new
object each time.
Now the PDF is stored at a deterministic per-PO key (buildPoPdfKey →
po-pdf/<poId>/<slug>.pdf). On each send, statObject() checks for an existing
copy: if it exists and is at least as new as the PO's updatedAt, it's reused
(no re-render, no re-upload) and only a fresh presigned URL is minted —
refreshing the 7-day download timer. It re-renders only when there's no copy
yet or the PO changed since the cached one (so an edited PO never emails a
stale PDF).
- lib/storage.ts: buildPoPdfKey (deterministic) + statObject (HEAD/stat, no
body transfer; null when absent).
- email-actions.ts: reuse-or-render decision keyed on updatedAt; always
re-presign.
- Tests: +2 (reuse-on-second-send-only-refreshes-link, re-render-when-changed).
email-vendor suite 8 green; tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Email PO to vendor" (issue #14) relies on PdfService fetching
/api/po/<id>/export?...&svc=<token> WITHOUT a user session, authenticating
with a `svc` token that matches PDF_SERVICE_TOKEN. The route handler validates
that token, but the auth middleware runs first and its matcher doesn't exempt
the export route — so every unauthenticated fetch was redirected to /login
(307) and the svc bypass never executed. Net effect: the feature could never
render a real PDF on any deployed env, even with the service configured.
Fix: middleware now lets exactly `/api/po/<id>/export` through when its `svc`
query param matches `process.env.PDF_SERVICE_TOKEN` (the route handler still
re-validates it — defense in depth). Everything else stays auth-gated. The
match lives in a dependency-free, edge-safe, unit-tested helper
(lib/pdf-export-auth.ts); middleware already reads server env at runtime via
auth()/NEXTAUTH_SECRET, so reading PDF_SERVICE_TOKEN there is consistent.
Verified on a running build: correct svc + real PO -> 200, correct svc + bogus
PO -> 404 (handler ran), wrong/no svc -> 307 (still gated). 324 unit tests
green; tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the free-text Project Code input with a native <select> carrying a
fixed list of project codes (Petronet LNG Cochin, COMACOE Trombay, Haldia
Reach, Haldia MMT, COMACOE Mandvi) plus an empty "— none —" option, across
all three PO forms (new / edit / manager-edit).
- Add a shared PROJECT_CODES constant in lib/validations/po.ts as the single
source of truth.
- Add a reusable <ProjectCodeField> (mirrors <DeliveryLocationField>): plain
HTML select keeping name="projectCode" so the server actions are unchanged.
- The field stays optional; projectCode remains a nullable free-text snapshot
(no schema/migration, no validation tightening) so legacy/imported values
are not rejected. On edit, a current value not in the list is preserved as a
leading "(current)" option so it is never silently dropped.
- Add unit tests for the new field.
Fixes#124
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PO History had no way to narrow by accounting code. Add an "Accounting
Code" filter (the shared type-to-search combobox) alongside Cost Centre,
backed by the PO-level account already included in the query.
- history/page.tsx: read `accountId` searchParam, fetch selectable leaf
accounting codes (active, no children) via buildAccountGroups, apply
`where.accountId`, thread the param into pagination + export links, and
surface an Accounting Code column for context.
- history-filters.tsx: new SearchableSelect control wired into
buildParams/apply/clear/hasFilters like the Cost Centre select.
- api/reports/export: apply the same `accountId` filter so CSV/PDF export
respects the on-screen filter.
- tests/staging: verify picking a code drives an `accountId` query param.
Fixes#121
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The comparison charts (and detail-page breakdown swatches) rendered every
series in recharts' default colour instead of the per-item palette.
Root cause: `SERIES_COLORS` was defined in `components/reports/charts.tsx`,
which is a "use client" module. The report **pages are server components** and
imported the palette from it. A plain value imported from a client module into
a server component is a client-reference proxy, not the real array — so
`SERIES_COLORS[i % SERIES_COLORS.length]` was `SERIES_COLORS[NaN]` → undefined,
every line got `stroke={undefined}`, and recharts fell back to #3182bd. (The
literal `strokeWidth={2}` still applied, which is why only the colour was wrong.
It passed jsdom tests because those import the array directly, not across the
RSC boundary.)
Fix: move the palette to a dependency-free shared module `lib/report-colors.ts`
(no "use client", no server-only imports) that resolves to the real array in
both server and client graphs. `charts.tsx` and all four report pages import it
from there. It can't live in `lib/reports.ts` (that imports Prisma `db`, which
must not enter the client bundle).
Verified in a real browser: line strokes now cycle the 10-colour palette
(#2563eb, #16a34a, …) instead of a uniform #3182bd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a Playwright suite (App/tests/staging/) that logs into the running staging
instance (ppms-staging, :3200) and verifies each closed portal issue is actually
fixed — feature level, driving the real UI, one spec per issue.
To make credential login possible against the prod-mirror pelagia_test (which only
holds real, mostly SSO-only users), prisma/seed-test-users.ts idempotently seeds one
known-password @pelagia.local user per role, and automation/refresh-test-db.sh runs
it after every daily refresh so the logins persist on staging.
Result against staging: 41 passed, 1 skipped (#10 — no attachment data on staging).
Two closed issues were found NOT fixed and are recorded as documented test.fail():
- #13 Accounts "payments completed this month" card is absent.
- #24/#40 logout tooltip still reads "Sign out" (pipeline test issues).
Docs/TESTING.md documents the suite, the seeded users, how to run it against
staging, and the full issue -> script mapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The monthly/weekly comparison already drew one colour per item (series =
items). Yearly mode instead coloured by financial year (series = FYs, items on
the x-axis), so multiple cost centres / accounting codes in the same yearly
graph shared colours. Unify all three granularities to series = items: the
x-axis is months / weeks / FYs and each item keeps its own distinct colour
(yearly becomes grouped bars per item rather than per year).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picks up the three pieces deferred from the initial reports PR:
#3 Line-item account allocation — allocatePoSpend() splits each PO across the
accounting codes its line items carry (line accountId, falling back to the
PO-level account), proportionally so per-PO rows sum back to totalAmount. The
accounting-code report now attributes multi-account POs correctly. SpendRow
gains poId; poCount is now distinct POs, not row count.
#2 Custom "Add to graph" — tick rows on either index (SelectCheckbox links
write ?sel=id1,id2), then "Compare selected" (?cmp=1) shows a custom comparison
of just those entities. Fully server-rendered + shareable; export honours sel.
#1 Weekly granularity — a third Granularity that focuses one FY month and
buckets spend by week-of-month (W1–W5) from approvedAt, with a Month picker in
the toolbar. Real buckets (not the mockup's synthetic split).
All three are URL-driven like the rest, so no client fetching. Charts/KPIs/
detail trends all branch on the new mode.
Tests: +8 unit cases (allocation proportional/fallback/empty, weekly buckets,
sel parse/toggle, month + granularity parsing); fixture updated for poId/week.
Full unit suite 311 green; tsc clean. Smoke-tested weekly + custom-compare +
exports end-to-end (all 200). Docs + wiki updated to mark them implemented.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the wiki "Reports Mockup" as a Reports → Purchasing sidebar section,
wired to real approved-PO spend.
Two report families, each index → drill/detail:
- Cost Centres (/reports/cost-centres) — spend compared across vessels; row
opens a cost-centre report with a Top-accounting-codes breakdown re-pivotable
by tier (Heading/Sub/Leaf) + Top-N.
- Accounting Codes (/reports/accounting-codes) — drills the Account tree
(headings → sub → leaves) via ?parent=; a leaf opens its report broken down by
cost centre (or, for a non-leaf, by sub-account).
Shared: a pinned filter toolbar (Granularity Monthly/Yearly, Financial Year,
Show Top5/Top10/Bottom5/All) whose values live in the URL query so the server
component re-renders — no client fetching. KPI tiles, recharts comparison/trend/
breakdown charts, per-row trend sparklines, and CSV export (/api/reports/spend).
- lib/reports.ts: the pure, unit-tested aggregation core. Spend = a PO once it
reaches POST_APPROVAL_STATUSES, dated by approvedAt, valued at totalAmount
(the dashboard's basis); Indian Apr–Mar FY; each PO's leaf accountId rolled up
to parents. One query in getReportDataset(), everything else pure.
- Sidebar: new collapsible "Reports" section with a "Purchasing" subheading
(subgroup support added to the Section model). Gated by view_analytics
(Manager/SuperUser/Auditor/Admin); export by the same.
Deferred (documented): synthetic Weekly granularity, the "Add to graph" custom
multi-select, and line-item-level account allocation (v1 uses the PO-level
account). Sites are not cost centres — only vessels.
Tests: 11 unit cases for the aggregation core + 3 sidebar cases for the Reports
section. Full unit suite 303 green; tsc clean. Smoke-tested all routes end to
end against seed data (index/drill/detail/export 200; non-analytics role 307/403).
Wiki: "Reports Mockup" marked implemented; "Pages and Navigation" lists the new
routes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes#18. Navigating away from a PO create/edit screen with unsaved
changes could silently lose in-progress work. The forms now track a dirty
flag and guard both navigation paths:
- Hard navigations (refresh / tab close / external link) → the browser's
native "Leave site?" prompt via beforeunload.
- In-app navigations (sidebar / header / any internal link) → a capture-phase
click interceptor opens a modal offering Save as draft / Discard changes /
Stay on page. Save as draft runs the form's existing draft save (which
redirects to the PO); Discard continues to the intended destination.
The guard (components/po/unsaved-changes-guard.tsx) is reusable and wired into
both new-po-form and edit-po-form. dirty is cleared before a successful submit
so saving never trips the prompt. SPA back-button (popstate) is left to
beforeunload only; the manager inline-edit panel is out of scope (saves in
place, no draft concept).
Tests: 7 new unit cases for the guard (intercept-when-dirty, no-op-when-clean,
external links pass through, Stay/Discard/Save actions, beforeunload arming).
Unit suite 296 green; tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GSTIN lookup rendered its CAPTCHA (image + 6-digit input + Verify /
New image) inline inside the Add/Edit Vendor dialog. AdminDialog has no
internal scroll and is vertically centred, so the taller form pushed its
footer (Cancel / Create Vendor / Save) off-screen and out of reach.
Extract the CAPTCHA into a dedicated popup (CaptchaPopup) overlaid on the
vendor form at z-[60] with an explicit Cancel button and a ✕ close
control. It handles Escape on the capture phase so dismissing the CAPTCHA
does not also close the underlying form. In-flight CAPTCHA errors now
show inside the popup (it stays open so the user can retry / get a new
image); the success line still lands on the main form. The form footer is
never displaced.
Adds a unit test covering popup open on Look up, Cancel closing only the
popup, and a successful verify populating the fields.
Fixes#114
Move New PO, Closed POs, Import PO and History into the Purchasing
section and rename them: "New PO" to "New Purchase Order", "Import PO"
to "Import Purchase Order", "History" to "Purchase Order History".
Per-role visibility is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fully expand the five abbreviated rank names so the canonical seed (which
upserts ranks by code and overwrites name) matches the names loaded into prod:
- PM → Project Manager
- Assistant PM → Assistant Project Manager
- Sr. Dredge Operator → Senior Dredge Operator
- Jr. Dredge Operator → Junior Dredge Operator
- Sr. Fabricator → Senior Fabricator
Hierarchy, codes, category, isSeafarer and grantsLogin are unchanged. (The
prod Rank table was seeded with these 19 ranks out-of-band; this keeps the
source of truth in sync so a future seed won't revert the names.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The vendor field on the PO forms was a plain native <select>, forcing
users to scroll the full vendor list. Mirror the item-search UX with a
searchable combobox that filters by vendor name and formal code
(vendorId), case-insensitively. The vendor list is already client-side,
so this is a pure in-memory filter — no API or DB change.
New VendorSelect component (components/ui/vendor-select.tsx) is a
self-contained portal-rendered combobox posting a hidden vendorId input,
so it drops into all three PO forms unchanged on the server:
- po/new/new-po-form
- po/[id]/edit/edit-po-form
- approvals/[id]/manager-edit-po-form
Preserves the optional field, "No vendor selected" empty option, and the
"{name} (CODE)" / "(unverified)" label. Unverified vendors (null code)
remain findable by name. Adds unit tests for the filter logic and
component behaviour.
Fixes#109
- tests/e2e/inventory → tests/e2e/catalogue (folder name only; playwright globs
./tests/e2e so nothing else changes).
- remove the next.config redirects from /inventory/{items,vendors}: the old
routes are intentionally left free for a future feature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renames the product-catalogue pages (items + vendors, incl. their [id] detail
pages) out of /inventory into /catalogue. /inventory/cart is unchanged. All
internal links, redirects, revalidatePath calls, sidebar nav, and tests are
updated; next.config redirects keep old /inventory/{items,vendors}[/...] URLs
working (permanent) so existing bookmarks don't 404.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously a PO's free-text line items only became reusable catalogue products
(/inventory/items) on full payment (markPaid → syncProductCatalog). An approved-
but-unpaid PO's items weren't selectable for further POs yet.
- extract syncProductCatalog into lib/product-catalog.ts (shared).
- call it from approvePo so approved items are immediately catalogued (create
product by name if unknown, link the line item, upsert last/per-vendor price);
payment still re-syncs to refresh prices. Idempotent.
- test: approving a PO with a free-text line creates + links the product and
records the per-vendor price.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the merged #11 PR (which shipped the enum-based catalogue): make
categories user-defined data and the PO T&C a dynamic editor.
- categories are a TermsCategory TABLE (not an enum) — admins add new ones;
- every PO T&C line is catalogued, incl. the previously-fixed boilerplate
(seeded under a "General" category) and an "Others" bucket;
- the PO form is a dynamic editor: "+ Add term", pick a category, type/pick a
clause (components/po/po-terms-editor.tsx), used by new/edit/manager-edit.
Migration: the already-released 20260624140000 migration is untouched; a new
20260624150000 FORWARD migration renames the enum, creates the table, migrates
existing enum clauses onto category rows, adds isDefault/sortOrder + the two
fixed lines under General, and adds PurchaseOrder.terms (JSON snapshot that
supersedes the legacy tc* columns for export/detail; old POs fall back to tc*).
Tests rewritten for category creation + catalogue/default helpers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>