Commit graph

621 commits

Author SHA1 Message Date
DEls
2f613181ac feat(ask): download real PDFs from the chat UI, drop the screenshot path
The "Download PDF" button (both the report CTA and the per-answer icon)
and the once-per-report auto-download now fetch
GET /api/ask/messages/[id]/pdf instead of screenshotting the DOM with
html-to-image. That fetch/blob/anchor-download logic lives in its own
new download-pdf-button.tsx, kept small and separate from chat-view.tsx
since another change is landing in that file's sidebar at the same time.

A Turn's `id` (the stored ask_messages row id) now rides in from a
thread reload (rowToTurn) or from the stream's "done" event once the
row is persisted; both the button and the auto-download wait for it.

Removes answer-pdf.ts (the html-to-image renderer and its page-slicing
math) and its tests — nothing else used them. slugify moved to
src/lib/reports/pdf/filename.ts in an earlier commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:16:46 +02:00
DEls
58b625701e feat(ask): serve a real PDF from a stored assistant message
GET /api/ask/messages/[id]/pdf builds a PDF for one stored assistant
row: its structured make_pdf_report document when `parsed.pdf` is set,
otherwise its own markdown (headings/paragraphs/lists/GFM tables, with
[[chart:id]]/[[image:id]] tokens resolved against that message's own
stored charts and any referenced generated-image rows). 404s for a
missing message, a non-assistant row, or a thread owned by someone
else — same "not found, not forbidden" reasoning as the existing
thread routes, since there are no logins yet. Returns
application/pdf with Content-Disposition: attachment; filename="ask-<slug>-<date>.pdf".

The chat stream's "done" event now carries the persisted assistant
row's own id (messageId), which the browser needs to ask this route
for a PDF — see the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:16:38 +02:00
DEls
51e80bfe1d feat(ask): make_pdf_report takes a structured document
make_pdf_report no longer just tags an answer with a title/subtitle —
it now takes { title, subtitle?, sections: [{ heading?, text?, kpis?,
chartId?, imageId?, table? }] }, matching src/lib/reports/pdf/document.ts's
ReportDocument. A section's table prefers { resultId } (pulled from the
cached query_records result server-side) over { columns, rows }, so the
model still never hand-copies a figure. The document is stored as-is
(no rendering here — that's the next commit's route) and streamed to
the browser via the existing `pdf` SSE event.

Updates the system prompt's PDF rule to describe the structured call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:16:28 +02:00
DEls
3e06a78fcb feat(reports): add a real, server-side PDF builder
Replaces Ask's screenshot-then-wrap PDFs with an actual PDF pipeline
that runs in plain Node (no Chromium/Puppeteer, Alpine-safe): pdfmake
draws real text, real tables (zebra rows, right-aligned numbers, a
repeated header row across a page break) and vector charts (bar,
stacked bar, line, area, donut) built from ChartSpec as hand-rolled
SVG, using pdfkit's standard 14 fonts so no font files ship at all.
Chosen over @react-pdf/renderer for its native repeated-table-header
support, which that library only gets via manual pagination.

- chart-svg.ts: ChartSpec -> drawing plan (bar count, axis max) -> SVG.
- markdown-blocks.ts: GFM markdown -> headings/paragraphs/lists/tables.
- table-content.ts / kpi-content.ts / chart-content.ts / image-content.ts:
  the pdfmake content nodes for each piece.
- chrome.ts: masthead + running header + "Page n of N" footer.
- resolve-section-table.ts: a report section's table, preferring the
  cached query_records result (table: { resultId }) so figures are
  never hand-copied.
- build.ts: assembles either an answer's markdown or a make_pdf_report
  structured document into one PDF.

Verified against real pdftotext/pdftoppm output, including the header
repeating across a page break.

Adds pdfmake, unified, remark-parse (+ @types/pdfmake, @types/pdfkit);
drops html-to-image, whose only caller is removed in a later commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:16:20 +02:00
DEls
31655a573c fix(ask): call the MiniMax image endpoint once, not /v1/v1
MINIMAX_BASE_URL already ends in /v1, so appending /v1/image_generation
hit a non-existent path and every image request failed. Also accept the
image_urls response shape by downloading the image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:44:19 +02:00
DEls
2f34308840 fix(ask): bake CSS custom properties into PDF chart captures
Ask-made PDF reports were shipping charts with axes but no bars/line:
html-to-image serializes the answer clone to a standalone SVG document
and rasterizes that, and never re-resolves CSS custom properties in
that step. Recharts sets fill/stroke straight to var(--series-1) etc.
as raw SVG attributes (report-chart.tsx's seriesColor), so an
unresolvable fill fell back to SVG's default (black — solid black
bars) and an unresolvable stroke fell back to "none" (an invisible
line), reproduced live with "Make me a PDF report on fuel at The Hills
for September".

bakeCssVariableColors walks the clone once it's attached to the
document with its light theme forced, and replaces every var(...)
fill/stroke attribute with its computed literal color before capture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:05:37 +02:00
DEls
e6772bfb1f fix(ask): migrate created/updated to autodate, sort ties by id
ask_threads/ask_messages/ask_log shipped with created/updated typed as
plain `date` fields, which PocketBase never populates — every row's
created has been empty since. pb-add-ask-collections.py now emits
autodate fields (onCreate, plus onUpdate for updated) and migrates the
three live collections in place under --apply, re-adding
idx_ask_messages_thread_created if PocketBase drops it with the field.

--backfill (apply-only) just reports affected row counts: PocketBase
can only set an autodate field to *now*, never a historical value, so
there's nothing safe to backfill.

Since old rows keep an empty created either way, the read side sorts
ask_messages by `created,id` instead of `created` alone, so tied
(all-empty) rows land in a stable, deterministic order rather than
whatever order PocketBase happens to return them in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:05:23 +02:00
DEls
b065eb58b8 fix(ask): hide an unparseable filed-on date instead of showing "Invalid Date"
ask_log rows come back with created: "" — the collection's created/
updated fields are declared as plain date fields rather than PocketBase's
autodate type, so nothing populates them on create. That's a schema fix
(a human --apply step per Task 1's own rules, out of scope here). In the
meantime, the Change Requests page rendered new Date("").toLocaleDateString()
verbatim as the string "Invalid Date". Omit the line instead when the
date doesn't parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:28:56 +02:00
DEls
aeba1a2225 test(ask): give the actions route tests a matching Host header
Follow-up to 7773ac8: those tests build a bare new Request(), which
(unlike a real HTTP request) has no Host header unless one is set
explicitly. isSameOrigin now checks Host, so all three routes' tests
started failing every non-cross-site case. Default a Host header
derived from the test's own ORIGIN, and set it explicitly (mismatched
against Origin) in the cross-site case so that test still exercises a
real mismatch rather than a merely-missing header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:28:47 +02:00
DEls
7b1d0baa5c fix(ask): stop PDF pages from duplicating content at page breaks
Live-testing the PDF report ("fuel at The Hills for September") showed
the same chart block rendered twice: once at the bottom of page 1,
again at the top of page 2. Every page drew the *entire* rendered
answer image, repositioned so its own break offset lined up with the
page's top — relying on the page's fixed height to crop away
everything else. That only works when a page's content run happens to
be exactly CONTENT_HEIGHT_PX tall; slicePages deliberately breaks
*earlier* to avoid cutting a block in half, so the leftover space at
the bottom of the shorter page bled into the next block's content
instead of staying blank.

Now each page gets its own canvas-cropped PNG sized to exactly the
content it should show (breaks[i] to breaks[i+1]), embedded as a
separate image — no reliance on page bounds to "crop".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:21:12 +02:00
DEls
7773ac8f55 fix(ask): compare Origin against the Host header, not request.url
Confirm/Reject/Undo were unusable from any host that didn't happen to
match Next's internally-reported request.url hostname: dev serves on
127.0.0.1 (or a LAN IP) but request.url came back normalised to
"localhost", so every genuine same-origin click failed with "Cross-
site request refused" (403) even though the browser's real Origin
header was correct. Compare Origin's host against the request's own
Host header instead, which always reflects what the client actually
sent.

Found live-testing the fixed-cost Confirm/Undo flow: PocketBase showed
the row was never actually blocked from being created by the guard
itself, but every apply/reject/undo click from the browser 403'd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:07:07 +02:00
DEls
738fcf9f6d fix(ask): keep bucketed chart rows chronological and reject non-numeric series
Live-testing a free-query chart ("litres per vehicle per week") turned
up two bugs:

- query_records grouped rows in first-seen order, not calendar order.
  Grouping by (vehicle, week) walks one vehicle's weeks at a time, so a
  week bucket could appear more than once and out of order — and a
  chart reads that row order straight onto its x-axis. Bucketed
  results now always sort chronologically by the bucket field first;
  anything the model sorts by still applies as a tie-breaker.

- make_chart accepted a non-numeric column (e.g. the "vehicle" label
  column) as a series, silently producing a blank plot with no y-axis
  since its cell values aren't numbers. It now checks actual cell
  values and returns a clear error naming the numeric columns
  available, so the model can correct itself instead of shipping an
  empty chart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:01:09 +02:00
DEls
693d096c0d fix(ask): log tool failures server-side
A failed tool call only ever reached the model as a short "Error: ..."
message so it could recover — but nothing was logged server-side, so
a real bug (as opposed to the model misusing a tool) left no trace to
debug from. Log the full error to the server console before handing
the short message back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:52:35 +02:00
DEls
4c4f80cd8c fix(ask): resume the active thread after leaving the page
Navigating away from /dashboard/chat and back (e.g. via the sidebar
nav, which drops the ?thread= query) reset the conversation to a
blank composer even though the thread was still live and listed in
the sidebar. Persist the actor's active thread id in localStorage and
restore it when the URL has no ?thread=, so a conversation survives
leaving the page as the plan specifies. "New thread" clears the
stored id so it still starts blank.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:48:54 +02:00
DEls
fecc885d3e docs(ask): agent actions
docs/modules.md §7.1: threads, memory, files, free queries/virtual
datasets, the propose/confirm/apply/undo gateway, both PDF export paths
(manual answer export and Ask-made reports via make_pdf_report),
images, and change requests — with the actor/x-ask-user model and the
three new PocketBase collections.

docs/data-model.md §4.17: field-by-field ask_threads/ask_messages/
ask_log reference, cross-linked from modules.md via explicit anchors.

docs/api-reference.md §12/§12b: /api/chat's new body shape and its
thread/action/chart/image/pdf SSE events, plus every new agent-actions
route (picker, threads, attachments, confirm-card actions, memories,
change requests) and their auth-matrix entries; the pb-files proxy
entry now notes its ask_messages admin-token path.

README.md: the intro, the modules table (adds Change requests), and
the API surface summary now describe an agent that acts, not just a
read-only chat.

Also marks Tasks 1–12 and 9b's implementation checkbox done in the plan
doc (docs/superpowers/plans/2026-09-26-ask-agent-actions.md) — the
remaining unchecked box is 9b's live check, skipped because
ask_threads/ask_messages/ask_log aren't applied on the target
PocketBase yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:32:53 +02:00
DEls
88011eb5b8 chore(ask): configurable Claude model
provider-anthropic.ts's model id is now overridable via ANTHROPIC_MODEL
(default unchanged: claude-opus-5, still the current model per the
claude-api skill — this was a configurability gap, not a stale model
id). docs/operations.md documents the new env var and a 5-prompt
comparison set for evaluating providers/models against each other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:27:32 +02:00
DEls
ba75c65795 feat(ask): download Ask-made PDF reports
Added mid-plan (docs/superpowers/plans/2026-09-26-ask-agent-actions.md,
new Task 9b — image generation from Task 9 is untouched): the user can
ask for any custom report and get a finished PDF, e.g. "give me a PDF
on fuel at The Hills for September", not just a manual export of
whatever is on screen (Task 8).

make_pdf_report({ title, subtitle?, filename? }) is the model's own
"this answer is a finished report" signal, called last once it has
already built the report (headings, narrative, KPI tables quoting tool
display strings, charts via make_chart). extractPdfMeta splits the
meta out the same way extractCharts/extractActionCard already do: the
model gets an ack, the browser gets the full meta via a new `event:
pdf`. The assistant's ask_messages row stores it in the existing
`parsed` json field, so reopening the thread still shows the report
cover and Download button — no schema change.

chat-view.tsx renders a report cover (wordmark, title, subtitle, date,
"Prepared by Ask for {name}") and a prominent "Download PDF · {title}"
button reusing Task 8's answerToPdf (same page-slicing, so a chart is
never cut across pages), and auto-downloads once per turn via
nextPdfTurnToDownload — a pure decision function, unit-tested directly
since this codebase's component tests use renderToStaticMarkup rather
than interactive DOM testing.

Live check skipped: needs a thread, which needs ask_threads (not yet
applied). Covered by unit tests (tool meta, the route's pdf event and
persistence, and the once-per-turn auto-download decision) instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:25:00 +02:00
DEls
67bdd8e73e feat(ask): assistant-style prompt and longer tool loop
Raises runChatLoop's default maxIterations from 8 to 16 (loop.ts) —
"read a file -> match -> propose" and free query/chart reports need
more round trips than a single report tool call; /api/chat needed no
change since it already relies on the loop's own default. The
system prompt's opening now introduces Ask as "the operations
assistant" that can measure, chart, read files, remember and propose
changes (not just answer questions), and a new "How to work" section
gives the five-step recipe (work out the ask, gather data cheaply,
propose for a change, lead-then-detail for a report, ask only when
genuinely ambiguous). The arithmetic ban, chart grammar and glossary
are untouched — asserted by existing and new tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:16:11 +02:00
DEls
bdb2b2361b feat(ask): log app change requests for the developer
file_change_request (tools-actions.ts) logs an ask_log row
(type "request", status "open") for a change to the app itself — a new
page, column, button or report layout — distinct from propose_changes,
which changes data; the prompt now routes between the two.
list_change_requests (tools.ts, read-only) lists them, which the
existing MCP server picks up automatically since it iterates the whole
tools array. /dashboard/change-requests lists and re-statuses them
(open/planned/done/declined) via new GET/PATCH routes under
/api/ask/change-requests, linked from the sidebar under Ask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:07:27 +02:00
DEls
ed49d7e198 feat(ask): generate illustrations
src/lib/agent/images.ts's generateImage posts MiniMax image-01's
documented request shape (verified against the installed mmx-cli SDK:
POST {baseUrl}/v1/image_generation with
{model, prompt, aspect_ratio, n, response_format}) and reads
data.image_base64[0]. A base_resp.status_code of 1028/1030/2061 (the
same codes the CLI itself maps to a "needs Plus plan" hint for this
endpoint) surfaces as a clear, non-retried error rather than a generic
failure. generate_image (tools-actions.ts) stores the result as its own
append-only ask_messages row (kind: "image") and returns its url.

extractGeneratedImages splits a tool result the way extractCharts
already splits chart numbers — the browser gets the full image, the
model keeps only imagesAvailable: [{id}] so it can place
[[image:<id>]] without ever seeing pixels or a URL. answer-parts.ts's
splitAnswer now takes an imageIds set alongside chartIds (existing
chart-only callers keep working via a default empty set), and
AnswerMarkdown renders the image inline.

The pb-files proxy (src/app/api/pb-files/.../route.ts) now also serves
ask_messages (previously only delivery_notes/purchase_orders): since
that collection's rules are all null by design (Task 1), this one
collection mints a short-lived admin file token server-side rather
than exposing the underlying PB rule.

Live check skipped (both the chat flow and MiniMax call): reaching
generate_image needs a thread, which needs ask_threads (not yet
applied). Covered by unit tests against the documented request/response
shape and error codes instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:59:52 +02:00
DEls
93d8fef17d feat(ask): download answers as PDF
src/app/dashboard/chat/answer-pdf.ts: slicePages (the pure page-break
maths — packs consecutive top-level blocks under an A4 content height,
breaking only between blocks, an oversized single block gets its own
page rather than being split) and answerToPdf (clones the answer at a
fixed 794px/A4 width in light theme, renders it with html-to-image at
pixelRatio 2, and embeds one page per slice via pdf-lib with a header
and "Page n of N" footer). A new "PDF" button sits next to the
existing "Save as PDF" print button (relabelled "Print" for clarity —
it was always window.print()), filename
ask-<thread-title-slug>-<date>.pdf.

Live browser check skipped: reaching an answer at all now goes through
the picker -> threaded /api/chat flow, which needs ask_threads (not
yet applied). Covered by unit tests on the page-slicing maths and
slugify instead; the render/embed path itself is browser-only DOM code
that can't be meaningfully unit-tested under vitest's node environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:02:17 +02:00
DEls
5cccff9a77 feat(ask): load workers from lists, sheets, PDFs and photos
src/lib/agent/people-match.ts's matchPeople matches on employee number
first, then normalised full name (case/accent/spacing/hyphen
insensitive, so "André" == "Andre" and "Nkosi-Dlamini" == "Nkosi
Dlamini"), then surname + first initial as "likely" — ambiguous
surname+initial pairs report "none" rather than guess. The
match_workers tool wraps it: typed names, or a previously-read
attachment's sheet (name/employee-no/id-number columns picked by
header text). The prompt's worker-loading recipe wires it to
read_attachment -> match_workers -> propose_changes, refusing to
re-create an exact match.

Live check skipped: needs the full chat pipeline (thread persistence),
which needs ask_threads (not yet applied). Covered by unit tests,
including Afrikaans/Zulu name variants and the ambiguous-match case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:58:05 +02:00
DEls
5aac22ba8f feat(ask): propose, confirm, apply and undo changes
The gateway (src/lib/agent/gateway.ts) is now the one seam every write
goes through: authorize() (NEVER_WRITABLE + dn_batches lock_*/stage),
planOps (schema validation via schema-cache.ts's new validateOp,
before-snapshots, $ref cross-checks, relation-id existence, the
sites/employees duplicate rule, capped at 500 ops), createPendingAction,
applyAction and undoAction. propose_changes is a registered tool;
apply/reject/undo are confirm-card routes under
/api/agent/actions/[id]/, same-origin only.

/api/chat now streams a full action card (`event: action`) while the
model itself only ever sees {actionId, status, problems} — the same
split extractCharts already does for chart numbers (extractActionCard).
The next user turn gets the outcome of the prior turn's action(s)
prepended to what the model reads (ChatEntry has no system role, so
this is the pragmatic equivalent of a system note). chat-view.tsx
renders action-card.tsx per assistant turn, hydrating from
GET /api/agent/actions/[id] when a thread reopens.

Deviations, both noted for the smallest-sensible-adjustment reason:
- Batches run as sequential individual PocketBase calls, not
  pb.createBatch() — that's what makes $ref resolution exact rather
  than dependent on the batch API's own (version-sensitive) request-
  chaining syntax.
- Re-applying a "failed" action isn't specially deduped against ops
  that already partially succeeded; only the single-attempt path is
  implemented in this session.

Live check skipped: ask_threads/ask_messages/ask_log still don't exist
in the target PocketBase (Task 1's --apply is the user's own step).
Covered by mocked unit tests instead — 17 gateway tests plus the
confirm-card routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:54:06 +02:00
DEls
a8f5bb26a8 feat(ask): free queries and charts
Adds the generic query engine: src/lib/agent/schema-cache.ts (live PB
collection schema, 5-min cache), src/lib/agent/query.ts (QuerySpec zod
schema, runQuery with pb.filter-bound where clauses, Monday-start week
bucketing, sum/avg/min/max/count/count_distinct aggregation, a 20000-row
PB cap, and a 2h/200-entry result cache), and src/lib/agent/datasets.ts
(seven virtual datasets — site_summary, fuel_log_status, plant_hours,
payroll_hours, attendance, supplier_balance, po_fulfilment — each built
from the same src/lib/metrics computations the dashboard pages use;
plant_hours totals are tested against summarisePlantMeter on a shared
fixture). query_records and make_chart are now registered tools.

Adjustment: attendance is today-only (groupAttendance, the only
existing pure function for this, has no other granularity — a true
per-worker-per-day history would need new computation this task
doesn't otherwise call for).

Live check skipped: reaching query_records/make_chart through /api/chat
needs a thread, which needs ask_threads (not yet applied). Covered by
unit tests (including the plant_hours/page-aggregate parity check)
instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:37:57 +02:00
DEls
5cf828f4e3 feat(ask): read Excel, CSV, PDF, photos and text attachments
Adds src/lib/agent/attachments.ts (parseAttachment/previewForModel/
summarise for xlsx, xls, csv, pdf — text-layer or scanned, via
pdftotext — image and plain text) with fixtures under
src/lib/agent/__fixtures__/, POST /api/chat/attachments (multipart
upload into an append-only ask_messages row, rasterising a scanned
PDF's first pages so they ride along as vision images the same way a
photographed slip does), and the read_attachment / send_to_po_pipeline
tools in tools-actions.ts. send_to_po_pipeline mirrors
/api/purchase-orders/upload's own hash-dedupe-then-create logic
directly against PocketBase rather than a same-process HTTP call (no
reliable base URL to call itself with from a server-side tool handler).

chat-view.tsx gained a second attach control (FileUp icon) alongside
the existing PO-upload paperclip, wired to the new route; pending
attachment chips ride along on the next message the same way pending
photos already do.

Deviation: send_to_po_pipeline takes { id, site } (the plan's { id }
alone can't resolve which site's purchase_orders row to create,
since /api/purchase-orders/upload always required one).

Live browser check skipped: ask_messages doesn't exist yet in the
target PocketBase (pending the user's --apply of Task 1's schema).
Covered by mocked unit tests and real fixture files instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:25:45 +02:00
DEls
dc25fb13cd feat(ask): long-term memory
Adds src/lib/agent/memory.ts (remember/forget/listMemories, dedupe by
normalised text, formatMemoriesForPrompt) and wires it in: a
"## What you remember" system-prompt section, the remember/forget/
list_memories tools (src/lib/agent/tools-actions.ts, a per-request
factory since these need the actor unlike the static read-only tool
set), a REST view for the UI (/api/ask/memories), and a Memory drawer
in chat-view.tsx (Mine / Shared with everyone, add, delete).

Adjusted src/lib/agent/read-only.test.ts: its blanket "no mutation
calls under src/lib/agent" contract now allowlists the specific write
seams the plan designates (memory.ts here; tools-actions.ts, gateway.ts
and images.ts to follow) instead of forbidding every file, since
memories/requests are specified to bypass the gateway and write
directly.

Live browser check skipped: ask_log doesn't exist yet in the target
PocketBase (pending the user's --apply of Task 1's schema). Covered by
mocked unit tests instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:13:39 +02:00
DEls
7c5df11b84 feat(ask): per-user persistent threads
Adds the "Who's using Ask?" picker (GET /api/ask/users), actor
resolution via x-ask-user (src/lib/agent/context.ts), and thread
persistence (src/lib/chat/threads.ts, /api/chat/threads routes).
/api/chat now accepts { threadId?, message, display?, images? } and
persists each turn as its own append-only ask_messages row; the old
{ history } body still works for one release. chat-view.tsx gained the
picker, a thread sidebar (new/select/rename/archive), and ?thread=<id>
in the URL.

Live browser check skipped: ask_threads/ask_messages/ask_log don't
exist yet in the target PocketBase (Task 1's schema is dry-run only,
pending the user's --apply). Covered by mocked unit tests instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:05:01 +02:00
DEls
b211ce1c8b feat(ask): PocketBase schema for threads, messages and log
Additive, dry-run-by-default script creating ask_threads, ask_messages
and ask_log (change/memory/request), following the
pb-add-dn-staged-ingestion.py pattern. --dry-run confirms all three
collections are new; --apply is a human step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:51:26 +02:00
DEls
7299f81c63 docs(ask): agent actions plan
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:42:46 +02:00
DEls
af71b8534b feat(suppliers): green-row tint, period filter, and KPI tiles
Green when a supplier's ordered > 0 and outstanding === 0 -- the whole
row, not just the number, so a fully-paid-up supplier stands out at a
glance.

Period filter next to the search:
  All time, This week, This month,
  Week 1-4 (of current calendar month, last absorbs any remainder),
  Month 1 (current) / Month 2 (last) / Month 3 (month before),
  Custom range with two date inputs.

Window anchors on delivery_notes.date_received -- liability accrues when
the goods arrive, not when the PO is uploaded ('we only pay what we
received this month'). computeSupplierBalances gains lastDeliveryISO per
row; a pure helper in supplier-period.ts resolves windows in UTC so the
boundary does not drift with the server's timezone.

Four KPI tiles at the top of the page:
  - Total ordered / delivered / outstanding (cumulative, always).
  - Payable this period (delivery value inside the active window).
    Subtitle is honest: 'subtract anything already paid' -- we do not
    track supplier payments, so this is the upper bound.

ui/select.tsx exports a SelectLabel so the period dropdown can group
options.
2026-09-26 17:38:39 +02:00
DEls
16652bb36c fix(auth): remove the ?demo=1 PIN bypass
The query string skipped the PIN gate on any URL. The PIN is now the
only way in. Docs updated; the login plan notes there is no fixed
cutover date while the app is in on-site testing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:37:20 +02:00
DEls
46e9e3dbc4 docs(ask): reports and shortcuts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:57:28 +02:00
DEls
534f3b6a85 feat(ask): slash shortcuts for base reports
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:56:21 +02:00
DEls
1dbe719e2c test(chat): import the route statically so a loaded run cannot time out
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:55:18 +02:00
DEls
774eaca13f fix(ask): findings carry their own cross-site totals
A live /findings run showed the model adding per-site and per-item
counts itself. Rules that span sites now return one finding with the
total in the title and the per-site split in the detail, and the prompt
says not to add counts across findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:52:32 +02:00
DEls
89c73233cb feat(ask): business glossary and report rules in the prompt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:44:37 +02:00
DEls
d81a089a86 feat(ask): rain_outlook tool
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:44:01 +02:00
DEls
648f30c36d feat(ask): plant_equipment_summary tool
Hours and value match the Equipment page (guarded hoursBetweenReadings,
isPlant bucket).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:41:43 +02:00
DEls
e5789f3371 feat(ask): delivery_note_queue tool
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:39:38 +02:00
DEls
4163970cc8 refactor(dn-inbox): move board columns and stage mapping to src/lib/dn-pipeline/board
Moved unchanged; the inbox renders identically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:38:43 +02:00
DEls
10eba6f270 feat(ask): supplier_balances tool
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:37:27 +02:00
DEls
771f13967f refactor(suppliers): move supplier balance roll-up to src/lib/metrics
Moved unchanged; the Suppliers page renders identically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:37:40 +02:00
DEls
30b449009b feat(ask): attendance_today tool
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:35:48 +02:00
DEls
db9e6ddfb2 refactor(on-site): move attendance grouping to src/lib/attendance
Moved unchanged; the On-site page renders identically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:34:06 +02:00
DEls
e088f52cb2 feat(ask): improvement report
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:32:08 +02:00
DEls
6aa3837038 feat(ask): problem report
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:29:37 +02:00
DEls
7e506b46e1 feat(ask): site and portfolio report tools with charts
list_sites now reads computeSiteSummaries (moved verbatim, output pinned
by snapshot). The read-only guard also scans src/lib/reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:25:33 +02:00
DEls
b8e8054725 feat(ask): render report charts inline in answers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:20:01 +02:00
DEls
278d258024 feat(ask): stream chart specs to the browser, not the model
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:14:01 +02:00
DEls
f0736e0e62 feat(ask): chart spec and extraction from tool results
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:12:04 +02:00