Commit graph

425 commits

Author SHA1 Message Date
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
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
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
DEls
3551e09170 fix(chat): compute today's date per request, not at boot
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:11:25 +02:00
DEls
e83a0d302c fix: overview counts, wrong-unit fuel limits, meter guard, en-ZA numbers, vehicle merge, docs
- Overview/Plant tiles and pages share one vehicle bucket rule (blank unit_type = fleet)
- Plant landing equipment figures were always 0; hour diffs now reject 0 baselines and >24h/day jumps
- km/L limits on plant (and L/h on vehicles) report unit-mismatch instead of false alerts
- formatPercent/formatDecimal replace display toFixed (en-ZA)
- POST /api/vehicles/[id]/merge + Merge duplicate dialog (dry run first)
- Attention links go to equipment vs vehicles by unit type; PO upload view link fixed
- Conditional hook fixed in DN document view; ESLint flat config (next lint removed in 16)
- Docs: current modules/routes/env vars; login + Ask reports plans

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:04:08 +02:00
DEls
2d998370b8 feat(receipts): edit posted received quantities with audit 2026-09-25 07:39:46 +02:00
DEls
a59a5a86d2 Merge branch 'codex/clock-daily-shifts' 2026-09-25 07:32:46 +02:00
DEls
5cdfaa3128 feat(clock): review missed scan-outs without changing phone flow 2026-09-25 07:27:12 +02:00
DEls
176b6180bf fix(clock): keep old unmatched scans out of today board 2026-09-25 07:06:24 +02:00
DEls
8d728333f9 fix(clock): start new local day with an in scan 2026-09-25 07:04:45 +02:00
DEls
29edf2e5aa Revert "feat(clock): add named kiosk and supervisor sessions"
This reverts commit b163ce9f94.
2026-09-25 03:22:43 +02:00
DEls
b163ce9f94 feat(clock): add named kiosk and supervisor sessions 2026-09-25 03:19:25 +02:00
DEls
df201b2a1f feat(receipt-correction): add support for receipt correction form and enhance item editing
Introduced a new secret key for receipt correction in the environment configuration. Enhanced the API for handling item edits in delivery notes, allowing for more granular updates and preserving existing manual overrides. Updated the document detail view to display additional supplier and customer information, and integrated a toggle for skipping customer identity checks. Improved validation for item entries and added tests to ensure robust handling of various edge cases in item editing.
2026-09-25 02:59:16 +02:00
DEls
488eb2af99 feat(clock): define local workday and shift records 2026-09-25 02:03:46 +02:00
DEls
47ec4f1d1b feat(purchase-orders): enhance PO handling with improved validation and item management
Updated the purchase order processing to include enhanced validation for item edits, ensuring that all required fields are checked and properly formatted. Introduced new functions for computing item amounts, taking into account discounts and VAT. Refactored the PO detail view to streamline item management, including a new ItemsCard component for better organization of line items. Additionally, improved the handling of PO number sanitization to strip leading and trailing characters, enhancing matching accuracy. This update aims to improve the overall robustness and user experience of the purchase order system.
2026-09-23 23:37:37 +02:00
DEls
844ba8fe12 feat(workforce): enhance on-site dashboard with detailed employee status
Updated the on-site dashboard to provide a clearer overview of employee statuses. Renamed sections for better clarity, changing "On site now" to "On site today" and updated descriptions to reflect current employee activity. Refactored the data handling to categorize employees into three distinct groups: those currently on site, those clocked out today, and those who have not arrived. This change improves the visibility of employee attendance and enhances the overall user experience on the dashboard.
2026-09-23 00:25:33 +02:00
DEls
0edd8503b1 refactor(clock): update ConfirmDialog layout and timing constants
Removed the DialogFooter component and restructured the ConfirmDialog layout for improved usability. Increased AUTO_DISMISS_MS from 5 seconds to 30 seconds and adjusted DOUBLE_CONFIRM_WINDOW_MS from 4 seconds to 6 seconds to enhance user experience during the scanning process. Updated button styles for better visibility and interaction. This change aims to streamline the confirmation flow and reduce accidental dismissals.
2026-09-23 00:08:32 +02:00
DEls
97fe75931d feat(clock): implement two-step QR scanning flow for employee clocking
Enhanced the PersonScanner component to support a two-step flow for scanning employee badges. The new process includes a lookup phase to identify the employee and their last scan status, followed by a confirmation phase where the operator can verify the scan before recording the clock event. This change addresses issues with accidental double scans and improves user feedback with warnings for recent scans. Updated state management and error handling to reflect the new flow, ensuring a smoother user experience.
2026-09-22 23:50:59 +02:00
DEls
f88d5c519e feat(fuel-logs): enhance vehicle auto-creation and error handling
Introduced a new function to infer vehicle unit type from descriptions, improving the auto-creation process for vehicles in the fuel logs. Enhanced error handling for missing identifiers during vehicle creation, providing clearer feedback on the reasons for failures. Updated the SheetJS parsing to prevent date misinterpretation and refined the PlantPage to display all plant vehicles, regardless of cost per hour, ensuring better visibility of available resources.
2026-09-22 23:47:28 +02:00
DEIs
1760f77cff fix(map): isolate Leaflet stacking context so the Topbar dropdown sits above the map
Leaflet's internal panes use z-index 200..1000 (tile, overlay,
marker, popup, controls). The map wrapper had no z-index of its
own, so those values leaked up to the dashboard layout and buried
the Topbar's `All Sites` dropdown — which sits at z-50 by default.
The Topbar itself is in a non-overlapping zone so it looked fine,
but the dropdown's content (portalled into document.body) dropped
into the map area and got hidden behind the tile pane.

Add `isolation: isolate` to the map wrapper so Leaflet's z-index
ladder is contained inside the wrapper as a single atomic unit.
External UI at z-50+ now floats above the map as expected. The
legend and layer switcher inside the wrapper are unchanged (still
z-[400]) — they remain above the tile pane and below the map
controls, which is where they were before.

No-op for every other page; the wrapper is only on /dashboard/plant/map.
2026-09-22 22:12:49 +02:00
DEIs
87f75dd98f feat(plant): fleet map page with GPS-denormalised vehicles, clustering, and tile switcher
Adds /dashboard/plant/map: a Leaflet view of every vehicle scanned from
the MEGA Clock kiosk, plotted at its last known position.

Schema
- vehicles.last_latitude / last_longitude / last_scanned_at /
  last_location_accuracy_m denormalised off fuel_logs so the map page
  renders every vehicle in a single query. Same best-effort pattern as
  attention_dismissed_at; updated by /api/clock/vehicle-scan after every
  successful scan. PB init defaults new number cols to 0 (not null) —
  filter uses 'last_latitude > 0 || last_latitude < 0' to mean 'real fix'.
- Indexed via idx_vehicles_last_scanned.

Map UI
- Custom SVG markers per unit_type: car silhouette for fuel, tractor
  silhouette for plant, 24x24, drop-shadowed.
- Clustering via react-leaflet-cluster over Leaflet.markercluster.
  Cluster icons colour-ramp with size (green->sky->amber->rose) and
  scale 32->54px.
- Three-button base-layer switcher: Street (OSM), Dark (ESRI Dark
  Gray Canvas — CartoDB watermarks tiles without a key), Satellite
  (ESRI World Imagery). Last choice persists in localStorage.
- Site pins: small purple map-marker divIcon, click flies the map to
  zoom level 15.
- Empty-state is a floating chip on the map rather than a full-page
  card so the map is always visible.

APIs
- /api/clock/vehicle-scan: stores the GPS fix on the fuel_logs row
  AND denormalises onto the vehicle when present; missing-coord
  scan leaves last_* untouched.

Sidebar: new Map entry under Plant group.
2026-09-22 21:39:25 +02:00
DEls
a589b14ac9 feat(clock): enhance vehicle scan API with GPS support
Updated the vehicle scan API to include optional GPS coordinates (latitude, longitude) and location accuracy. The API now validates these fields, ensuring both latitude and longitude are provided together and within valid ranges. Enhanced error handling for invalid inputs and updated the API reference documentation accordingly. Additionally, modified the FleetScanner component to capture GPS data and reflect its status in the UI.
2026-09-22 19:59:44 +02:00
DEls
3077223d38 feat(clock): add MEGA Clock API endpoints and update documentation
Introduced three new server-only API endpoints for the MEGA Clock PWA:
- `POST /api/clock/scan` for clocking workers in and out via badge scanning.
- `POST /api/clock/vehicle-lookup` for looking up vehicle details using QR codes.
- `POST /api/clock/vehicle-scan` for logging odometer readings from scanned vehicles.

Updated the README and API reference documentation to include these new endpoints and their functionalities. Replaced the placeholder for fleet scanning in the ClockView component with the new FleetScanner component.
2026-09-22 12:42:39 +02:00
DEls
f1dbc96435 feat(dn): grab and move the order the same way as the slip
The order was a PDF in a frame, which meant the browser's viewer, not
ours: no drag, no shared zoom, controls in a different place. Two halves
of one comparison behaving differently.

Its pages are now rendered to PNGs through the same poppler path the
delivery note's pages use, so both sides go through one viewer and get
the same stepping, zoom, drag-to-pan and remembered width. Switching
sides resets to the first page at 100%, since the position held on one
document means nothing on the other.

The PDF route stays for "Full size". Its file lookup moved to lib and is
now shared with the page renderer, so the two cannot drift on which field
holds the file. Page count comes from pdfinfo rather than rendering, as
the viewer needs the count before it draws anything.

Verified against MEGCPO9239: a two-page order, both pages 200 image/png,
page three 404, no token 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 02:05:31 +02:00
DEls
8027b5a263 feat(dn): flip between the slip and the order it is checked against
Verifying a delivery means comparing two documents, and the order was not
one of them: reaching it meant leaving the note, finding the purchase
order and coming back having lost the comparison.

The viewer gains a toggle. The order is a PDF, so the browser's own
viewer handles it — it already paginates and zooms better than wrapping
it in ours would. The toggle only appears when an order is resolved and
carries a file; an unmatched note has nothing on the other side of it.

The document is proxied behind a signed token rather than linked from
PocketBase, which would put the admin JWT in the browser. The token is
namespaced per document, so one minted for a delivery-note page cannot be
replayed against an order, nor one order's against another's — verified
against a live file: valid token 200, absent 401, replayed 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 01:44:02 +02:00
DEls
4c49e862d3 feat(dn): drag the zoomed slip, and size the controls for a thumb
Grab and drag now pans a zoomed page, which is how anyone expects to move
around an image they have magnified — scrollbars were the only way
before, and they are a poor fit for reading a smudged order number.

Mouse only, deliberately. A finger already pans an overflowing box, with
momentum and edge behaviour that come free and feel native; taking that
over would replace something good with an imitation. The listeners live
on the window rather than the frame, because a fast drag outruns the
cursor and would otherwise leave panning stuck on when the button is
released outside it.

Controls were 28px, below the 44px a thumb needs. They are full-size on
small screens and compact from the sm breakpoint up, where a pointer
makes the smaller target fine. The frame also gives back some height on
a phone, where 70vh left nothing of the fields underneath.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 01:34:50 +02:00
DEls
5a3d0eddbf feat(dn): read the slip beside the fields, not in another tab
Matching a document meant opening its page in a new tab, reading it,
coming back and picking up where you left off — for every document, and
twice for a two-page one.

The slip now sits beside the fields it is being read into. Pages step
with prev/next and a page strip, zoom runs 100% to 400% against a
scrolling frame so inspecting a smudged order number does not push the
fields off screen, and the pane's width is a native resize handle with
the detail panel flexing into whatever is left. Full size is still one
click away for a genuinely unreadable scan.

Width is remembered: this page reloads after every save, and re-sizing
the slip each time would be its own small tax.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 01:29:52 +02:00