Commit graph

209 commits

Author SHA1 Message Date
DEls
0c7cd5b7d5 feat(metrics): compute delivery note value from received items
delivery_notes stores no value; a note is worth the quantity received on
each line times the matched PO item's rate. Unmatched lines contribute
zero rather than a guessed rate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:34:51 +02:00
DEls
818a679922 feat(types): add IncomeClaimRecord
Queried by two dashboard pages and the agent loader, but previously
untyped — those reads compiled only because untyped getFullList returns
loose records.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:33:44 +02:00
DEls
b2b72dacfe docs(plan): agent coverage gaps, vision and PDF export
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:27:38 +02:00
DEls
4902340b5f fix(pwa): declare the root manifest via metadata so routes can override it
The root layout hardcoded <link rel="manifest" href="/manifest.json">
in its head. Next's metadata API cannot override a hand-written tag, so
/dashboard/chat emitted two manifest links — browsers honour only the
first, making which app installed depend on tag order.

Declaring it as metadata.manifest lets the chat route's own manifest
replace it. Each route now emits exactly one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 09:07:44 +02:00
DEls
d40b7eb516 feat(pwa): install the chat as a standalone app
The chat page now exports Metadata that points at /chat.webmanifest and
declares appleWebApp for Safari's Add to Home Screen path (which
ignores manifest scope rules).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:58:30 +02:00
DEls
fce03e44f3 feat(pwa): add a separate web manifest scoped to the chat
The dashboard's manifest is generated by next-pwa from next.config.js.
A second static manifest in public/, linked from the chat page,
installs as its own app called Ask. The explicit id field stops the
two manifests on the same origin from being treated as updates to one
another.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:57:45 +02:00
DEls
e63c653886 feat(chat): upload a purchase order from the composer
Reuses the existing /api/purchase-orders/upload endpoint with a 20 MB
cap, SHA-256 dedup, MIME allowlist and a status poll. The model never
enters the write path — the human uploads, and the agent only reads
the result. read-only.test.ts is unmodified.

Also made the paperclip button more visible: a plain grey-on-grey
icon is easy to miss for a primary input. Now it has a bordered
treatment consistent with the send button.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:57:37 +02:00
DEls
fb612f0e6f feat(chat): add purchase order upload card
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:53:34 +02:00
DEls
342b56ee92 feat(imports): return the purchase order number from batch status
The status endpoint reports a PB record id, which is meaningless to the
chat upload card. Resolve it to the readable order number when the
batch type is purchase_orders, and leave the label null otherwise.

Failure to resolve the PO (e.g. it was deleted) is swallowed — the
batch status still stands and the raw id is still in the response as
the fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:53:24 +02:00
DEls
ad8275aa14 feat(agent): add PO, delivery note, fuel log and worker tools
Brings the tool set to 11. All five build on the same metrics pass the
dashboard uses, so per-fill variance, per-worker totals and the
underlying records reconcile with the pages they sit next to.

Also: sort='-created' on purchase_orders returns HTTP 400 from
PocketBase for this collection. Switched to -required_by_date, which
sorts by ordering recency — closer to what a project manager wants
than document creation time anyway.

Caught by calling list_purchase_orders against live data; the test
suite does not cover the live sort syntax.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:52:30 +02:00
DEls
b3d27ddaae feat(agent): add worker-hours and vehicle fuel-log formatters
Both build on the same metrics pass the dashboard uses, so per-fill
variance and per-worker totals reconcile with the pages they sit next
to. No new business math — the data already existed, but no tool
exposed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:42:00 +02:00
DEls
634f9681fe feat(agent): add loaders for PO, delivery note, fuel and workforce detail
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:41:05 +02:00
DEls
40bbbf8b5d feat(agent): add free-text vehicle resolver
Mirrors resolveSite: id, then QR code, then exact description, then
substring — and ambiguity is reported, never guessed. Picking the wrong
vehicle silently attributes fuel spend to the wrong machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:40:34 +02:00
DEls
410e2f6606 docs(plan): chat detail tools, PO upload and standalone PWA
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 08:27:49 +02:00
DEls
b69900fc1b feat(chat): render answers as dashboard-grade markdown
The models return GFM — tables of vehicles, bold figures, lists — and
the view rendered it as preformatted text, so users saw raw pipes and
asterisks.

Answers now render through react-markdown with every element mapped to
this app's own conventions: tables get the borders, muted header and
hover of components/ui/table.tsx, and numeric columns get tabular-nums
so rand figures line up as they do on the fuel and sites pages.

Alignment is computed per COLUMN, not per cell — the table inspects its
own rows, decides which columns are numeric, and publishes that through
context so a header never ranges left above a right-ranged figure.

Layout: questions are compact chips, answers are full-width documents
with a thin amber rule. Bubbles fight tables. Each answer carries a
provenance strip naming the tools it used — for a tool whose value is
that its numbers reconcile with the dashboard, the source is part of the
answer.

Motion is limited to a streaming caret and the running-tool pulse, both
disabled under motion-reduce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 21:00:35 +02:00
DEls
801c2d7c5c fix(chat): stop the second message of a conversation from crashing
The browser sends assistant turns as {role, text} with no toolCalls, so
both adapters threw 'Cannot read properties of undefined (reading
length)' on the second message. The ChatEntry type guarantees nothing
across a JSON boundary.

Normalizes history at the route (the trust boundary) and makes both
adapters defensive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:50:36 +02:00
DEls
e4dad3469a fix(chat): strip MiniMax <think> reasoning from streamed output
MiniMax M2.x emits its chain of thought inline in `content` wrapped in
<think> tags, so the user watched the model reason at itself before
every answer. Anthropic returns reasoning as separate thinking blocks,
so this is a provider quirk and the filter lives in the OpenAI-compatible
adapter only.

Tags can straddle chunk boundaries ('<thi' + 'nk>'), so the filter is a
small state machine that holds back a short tail rather than emitting
text that might turn out to start a tag. Verified character-by-character.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:50:36 +02:00
DEls
c543713ae8 feat(chat): add streaming chat UI at /dashboard/chat
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:41:31 +02:00
DEls
d7b5d01333 feat(chat): add env-gated SSE chat route
Returns 404 unless ENABLE_CHAT=true. No authentication yet — must stay
disabled in production until phase 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:41:31 +02:00
DEls
5cc0d91f2d feat(chat): add Anthropic and OpenAI-compatible adapters
Each adapter imports exactly one SDK. index.test.ts pins the env gate as
fail-closed: only the exact string 'true' enables chat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:41:31 +02:00
DEls
89bde4c7ff feat(chat): add provider-neutral agentic loop
Tested against a fake provider, so the control flow is verified without
a network or an API key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:41:30 +02:00
DEls
8667138ad0 feat(chat): add system prompt with a hard arithmetic ban
Every tool figure is computed by src/lib/metrics and matches the
dashboard. A number the model derives itself would look authoritative
and reconcile with nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:41:30 +02:00
DEls
102e304988 feat(chat): convert agent tool schemas to JSON Schema
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:41:30 +02:00
DEls
46f285249b feat(chat): add provider-neutral chat types
Imports no SDK, so the loop never learns which model is behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:41:30 +02:00
DEls
84cf8759e8 build(chat): add Anthropic and OpenAI SDKs, document chat env gate
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:41:30 +02:00
DEls
ca262cc789 docs(plan): chat UI + provider abstraction (phase 3)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:33:03 +02:00
DEls
d09fadefad feat(mcp): read-only stdio MCP server exposing six tools
Runs locally as your own process over stdio, so it opens no network
listener and adds no attack surface to the deployed app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:25:39 +02:00
DEls
37e12c3090 test(agent): guard the tool layer against write operations
Verified to fail on a planted .create( rather than passing vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:25:39 +02:00
DEls
d8d7366f12 feat(agent): add six read-only tools over the metrics layer
list_sites, get_site_financials, get_site_trend,
vehicles_needing_attention, get_fuel_summary, get_pending_verification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:25:39 +02:00
DEls
3884408aa7 feat(agent): add pure tool-result formatting
Every numeric field comes from @/lib/metrics. Results carry a display
block of preformatted strings so the model never formats or rounds
money itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:25:39 +02:00
DEls
7d02ef1e3a feat(agent): add read-only PocketBase loaders
The only module in the agent layer that touches the network. Kept
separate from formatting so tool output is unit-testable offline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:25:39 +02:00
DEls
15ecf1af05 feat(agent): add free-text site resolver
Humans say 'the Hills' or 'C26/09', not record ids. Ambiguity is
returned explicitly so a tool can ask rather than silently pick the
wrong contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:25:39 +02:00
DEls
dee027aeb9 build(mcp): add MCP SDK, zod and tsx for the agent tool layer
zod was previously only a transitive dependency; declared explicitly so
it cannot vanish under a dependency bump. tsx rather than Node's native
type stripping because the metrics modules import via the @/* alias,
which Node does not resolve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:25:39 +02:00
DEls
e82754ee38 docs(plan): agent tool layer + read-only MCP server (phase 2B)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:13:39 +02:00
DEls
2a548e9ebc refactor(fuel): build rows from the shared variance pass
No behaviour change — deletes the duplicate group/sort/pairwise walk now
that computeFuelLogStatuses does it. computeFuelVariance now has exactly
one caller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:04:51 +02:00
DEls
f966a7315a fix(dashboard): honour alert dismissals and stop truncating at 500 rows
The overview built its own fuel-variance walk that never read
attention_dismissed_at, so a vehicle dismissed on /dashboard/fuel stayed
on the overview forever. It also fetched fixed_costs and fuel_logs with
getList(1, 500), silently undercounting past 500 rows and skewing the
monthly cost tile.

Now uses the shared computeVehicleAttention/groupAttentionBySite and
fetches both collections in full.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:04:51 +02:00
DEls
f255761491 feat(metrics): extract fuel attention roll-up
One per-log variance pass plus a per-vehicle aggregate, replacing the
duplicated walks in the overview and fuel pages. Reports acknowledged
state via the existing isAlertAcknowledged rather than filtering, so
callers keep control.

Also declares attention_dismissed_at on VehicleRecord. The field is
written by PATCH /api/vehicles/[id]/attention-dismiss and read by two
pages, but was undeclared — those reads only compiled because an
untyped getFullList returns loosely-typed records.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:04:51 +02:00
DEls
15206fa650 docs(plan): fuel attention extraction plan (phase 2A)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:04:51 +02:00
DEls
a1263b39e6 refactor(fuel): use the shared RATE_PER_LITRE constant
Removes the last two of the four hardcoded 25.65 declarations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:47:30 +02:00
DEls
da8d19ab55 refactor(sites): use shared metrics on the site detail page
Detail and list now resolve start date identically, so projected total
and projected profit agree between the two surfaces. Timesheets are
filtered to this site before feeding the start-date calculation, so
another site's timesheet can no longer set this one's start date.

Deletes eight now-dead local helpers (parseDurationMonths, pickStartDate,
isoWeekKey, monthKey, expenseDate and the three series builders) along
with the duplicate RATE_PER_LITRE and WORKING_DAYS_PER_MONTH consts.

SiteStatus on the detail view gains 'upcoming', which the shared
projection can now return; without the matching STATUS_COPY entry the
header badge would dereference undefined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:47:30 +02:00
DEls
14d62eb34c refactor(sites): use shared metrics for the sites list roll-up
Replaces the inline per-site roll-up with computeSiteSpend /
resolveSiteStartDate / computeSiteProjection, and drops the redundant
second employees fetch now that the first one selects the site field.

Two correctness fixes in the same wiring:

- fuel_logs was fetched with fields 'id,site,litres', so every row fed
  to the start-date resolver carried an undefined date_logged and was
  silently skipped. Now selects date_logged.
- The resolver was only given fixed costs and fuel logs, while the
  detail page gives it all four sources. For a site with no
  job_start_date whose earliest timesheet or income claim predates its
  earliest cost, the two pages would still have disagreed. Timesheets
  are now bucketed by site via the employee join, and income_claims is
  fetched, so both surfaces see identical candidates.

Also drops fixed/payroll/material/fuel from the returned summary — they
are not declared on SiteSummary and no component reads them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:47:22 +02:00
DEls
5dc049ecf4 feat(metrics): add barrel export
Single import surface for the dashboard pages, and later the MCP server
and chat agent tool layer, so every caller gets identical numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:47:14 +02:00
DEls
00168c0439 feat(metrics): extract income-vs-expenses and daily fuel series
Weekly and monthly income-vs-expense buckets plus the 30-day fuel cost
series, lifted from the site detail page. The two near-identical bucket
walkers are unified behind one accrueAll helper, and 'now' is injectable
so the series are deterministic under test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:47:14 +02:00
DEls
7bd6a7659f feat(metrics): extract site projection and burn status
Run-rate extrapolation, remaining budget and the under/on-track/over
ladder. The 'upcoming' status previously existed only on the list page
and now applies to both surfaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:47:09 +02:00
DEls
e0419f9ae9 feat(metrics): extract site spend roll-up
Payroll, fuel, material and P&G aggregation as a pure function. Knows
nothing about time or PocketBase, so the projection and the agent tool
layer can both build on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:47:09 +02:00
DEls
d8b6d887e8 fix(metrics): unify site start date across list and detail pages
The sites list and site detail pages resolved a site's start date
differently, producing different projected profit for the same site:

  list   job_start_date -> LATEST fixed-cost date -> now
  detail EARLIEST activity date, job_start_date ignored entirely

Different start date gives different elapsedMonths, which feeds
projectedTotal and projectedProfit directly. The detail page also read
from a global, unfiltered timesheet list, so another site's timesheet
could set this site's start date.

One implementation now: explicit job_start_date, else the earliest
site-filtered activity date, else now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:47:09 +02:00
DEls
74defc6d95 feat(metrics): extract duration and date-bucket helpers
parseDurationMonths was duplicated verbatim in the sites list and site
detail pages; isoWeekKey and monthKey were private to the detail page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:47:00 +02:00
DEls
19032667dc feat(metrics): add shared constants module
RATE_PER_LITRE was hardcoded in four separate files, so a pump-price
change meant finding all four. It now lives here only, alongside the
working-day, weekly-capacity and month-length constants the site
financial roll-up needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:47:00 +02:00
DEls
2d36e9b1cd fix(vehicles): handle empty unit_type during updates
Updated the vehicles API to allow PATCH requests with an empty unit_type. This change ensures that existing records with unit_type='' can be updated without validation errors, while maintaining the default value of 'fuel' for new inserts. This adjustment resolves issues with updating pre-existing vehicle records that lack a unit_type field.
2026-08-29 19:17:14 +02:00
DEls
9ea0a1f65e fix(pb): make vehicles.unit_type optional
53 vehicles predate the unit_type field and have unit_type=''. PB
validates the WHOLE record on any PATCH, so as long as unit_type was
required, no record with an empty value could be updated (Dismiss,
inline-edit, anything calling .update()). Making the field optional
unblocks all 53 immediately.

The default='fuel' still applies to new inserts.

Note: the live PB schema lost its 'default' value during the PATCH
(see review below) — only 'required' was actually changed in the
stored schema. Fresh records will be inserted with unit_type='' (or
null) until the application sets it explicitly. The bootstrap
definition still has default='fuel' so fresh environments get it.

Idempotent migration: scripts/migrations/2026-08-28-vehicles-unit-type-optional.py
applied successfully; re-run is a no-op.
2026-08-28 15:26:33 +02:00