Commit graph

226 commits

Author SHA1 Message Date
DEls
e0e2feb61c docs(pwa): manual install verification steps for real devices 2026-08-30 22:42:55 +02:00
DEls
0d056e116f fix(pwa): restore static manifest.json + remove dead next-pwa config
The next-pwa plugin's `manifest` option is silently ignored in
@ducanh2912/next-pwa@10.2.9 — only the service worker is generated.
The dashboard Web App Manifest now lives as a static file at
public/manifest.json, sourced from the next.config.js block we just
removed. This is the same shape the project shipped at HEAD~3, just
with the new short_name and id field added by Task 4.
2026-08-30 22:33:35 +02:00
DEls
95be557983 build(pwa): wire build-icons.mjs into the build pipeline 2026-08-30 22:22:42 +02:00
DEls
363410a735 feat(pwa): set MEGA OPS short_name + add id field to dashboard manifest 2026-08-30 22:21:51 +02:00
DEls
11d47fffda feat(pwa): rewrite chat.webmanifest with MEGA Ask metadata + chat icons 2026-08-30 22:20:48 +02:00
DEls
797a69c4ec chore(pwa): drop stale manifest.json (regenerated by next-pwa) 2026-08-30 22:19:56 +02:00
DEls
6ce3c24f12 refactor(pwa): address code review on build-icons.mjs 2026-08-30 22:18:25 +02:00
DEls
5a7d2931f9 feat(pwa): render chat icon set + per-route apple-touch-icons 2026-08-30 22:15:15 +02:00
DEls
055387cc6d docs(plan): MEGA Ask vs MEGA OPS PWA split-install icon plan 2026-08-30 22:12:30 +02:00
DEls
459905758f docs(spec): MEGA Ask vs MEGA OPS PWA split-install icon design 2026-08-30 22:11:02 +02:00
DEls
091030d978 fix(chat): strengthen the arithmetic ban after a probe found a violation
Probing 'combined value of the top three delivery notes' against the
production model returned R 3 285 785 — arithmetically correct, exactly
what the ban was written to prevent.

The previous prompt said 'never do arithmetic' but did not say what
to do instead when the user explicitly asks for an aggregate. The
model picked the path of least resistance and summed them.

New prompt lists the verbs that count as arithmetic (including average,
percent, ratio, weighted, combined, total, sum) and gives the model a
concrete response shape: report each row's value, declare the
aggregate 'not available from me', tell the user which number is
missing.

Re-probed — the model now lists the three values individually and
declines the sum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:02:06 +02:00
DEls
e528589e9c feat(chat): save a single answer as PDF via the print dialog
Hides app chrome via a 'printing-answer' class on body, leaves only the
.print-target visible. The class is removed in a finally so a cancelled
print dialog can't leave the app invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:09:06 +02:00
DEls
7d066f37dc feat(chat): take or attach a photo and ask about it
Adds a camera button next to the paperclip that opens the device camera
on phones (capture='environment') and a file picker otherwise. Photos
are read into base64 and sent with the next user turn.

Pending photos render as a thumbnail strip above the composer with a
remove button each. The state had to be hoisted above the composer
handlers — useCallback closes over it at handler-creation time, so
declaring it lower in the file produces a TS2448.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:03:21 +02:00
DEls
c5dbb6cf64 feat(chat): accept images in a user turn on both providers
Anthropic adapter: image content block before text.
OpenAI-compatible adapter: image_url with a data: URL.
Route normalizer carries images through the trust boundary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:57:52 +02:00
DEls
6274d90b36 docs(env): default MiniMax-M3 (vision + tools verified)
M2.7 is text-only; M3 is the first MiniMax model that accepts both the
tools parameter and image inputs on the OpenAI-compatible endpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:56:30 +02:00
DEls
00269b90b5 feat(agent): add six listing and ranking tools
Closes the coverage gaps: delivery notes, plant hours, fixed costs,
income claims, the employee roster and import batches. Each takes an
optional site, an enumerated sortBy and a limit, so list/highest/lowest
questions work across every collection.

Verified live against the dashboard:
  - list_delivery_notes sorts by value; top result AS0002086 at R3 255 444
  - list_employees returns 72 (16 permanent + 56 casual)
  - list_imports returns 44 batches, none failed
  - list_plant_hours returns 0 rows — same as /dashboard/plant
    (the production plant_hours_log collection is empty today)
  - list_fixed_costs / list_income_claims wired through same shape

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:38:33 +02:00
DEls
94c393b279 feat(agent): add listing loaders for the uncovered collections
Six new loaders: delivery notes (with received items bucketed for
value computation), plant hours, fixed costs, income claims, employees
and import batches. Each takes optional filters rather than full
queries, so callers always run through typed tool handlers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 12:36:04 +02:00
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