134 lines
9.0 KiB
Markdown
134 lines
9.0 KiB
Markdown
# Roast Planner Webapp
|
||
|
||
A fillable, live-computing web version of the manual coffee roast plan worksheet
|
||
(`hope_roaster/roast-planner/manual-roast-planner.html`), plus:
|
||
|
||
- **Prefill from a bean URL** — paste a roaster's product page URL; the server fetches it and
|
||
uses the Pi Coding Agent SDK (zero-tool, one-turn extraction) to pull out whatever it can
|
||
find (cultivar, origin, processing, roast level), then deterministically maps that to
|
||
worksheet field IDs using the same reference tables as the paper worksheet. It never
|
||
invents a first-crack anchor or a modifier — those only ever come from the reference
|
||
tables, keyed off what the page actually states.
|
||
- **Reference curve from an Artisan `.alog`** — upload a real roast log (or browse an administrator-only local
|
||
library directory) to overlay its actual milestones/curve on the same SVG grid as your
|
||
plan, dashed and in a different color.
|
||
- **Finished-roast log with Pi agent review** — upload one or more finished Artisan `.alog`
|
||
files against a plan (or standalone from `/roasts`); each is stored verbatim (downloadable
|
||
as a backup), parsed, and deep-reviewed asynchronously by the same zero-tool Pi agent
|
||
pattern as prefill (grade, highlights, concerns, next-batch suggestions, plan-vs-actual).
|
||
The `/roasts` page lists every actual roast and renders its curve with the plan overlaid.
|
||
- **Brewing section** — the app is split into Roasting and Brewing workflows in the nav.
|
||
`/beans` manages roasted/purchased coffee (with LLM URL prefill from a roaster's product
|
||
page); `/brews` logs every cup against a bean with a silhouette brewer picker (immersion /
|
||
percolation / espresso & pressure — 13 methods), recipe fields (dose, water/yield, auto
|
||
ratio, grind, temp, times), a 0–10 rating, and tasting notes. Bean remaining weight is
|
||
derived from logged brew doses. Green inventory's lot form also prefills from a URL.
|
||
- **Plan chat and roast history** — plan chat uses the open plan and computed ledger.
|
||
Roasters organize uploads and show descriptive statistics; roaster selection, learned pace,
|
||
and historical temperature medians never modify plan timing or temperature suggestions.
|
||
- **API + tokens + Swagger** — every capability (including admin) is a JSON endpoint,
|
||
documented by a hand-authored OpenAPI 3 spec at `/api/openapi.json` and a self-hosted
|
||
Swagger UI at `/api-docs`. Users generate bearer tokens (`rpt_…`) on the Account page;
|
||
token requests skip CSRF (header-borne credentials can't be forged cross-site).
|
||
- **Backup** — Admin → Backup exports the entire database as one JSON file and can import
|
||
it back transactionally (delete-and-restore, refuses backups with no active admin, keeps
|
||
the importing admin's session when possible). Users can download their own data from the
|
||
Account page.
|
||
- **Academy** — `/academy` teaches roasting and brewing as narrated, animated slide lessons:
|
||
green-bean physics through every roast stage (drying, Maillard, first crack, development,
|
||
curve reading, defects) and extraction plus technique for every brewer family. Narration is
|
||
Azure TTS (`en-US-Andrew` HD voice), pre-generated to committed MP3s by
|
||
`scripts/generate-academy-audio.mjs` (reads `AZURE_VOICE_KEY`/`AZURE_VOICE` from `.env`,
|
||
region eastus — run it locally after editing lesson scripts; production never needs the
|
||
key). Slides carry display transcripts as captions and phonetic respellings for the voice.
|
||
- **Live ledger** — shared timing math with explicit manual corrections, invalid-duration
|
||
checks, density-unit warnings, and a direct development override. Light-roast modifier
|
||
suggestions have a disclosed 1:15 floor; an explicit override takes precedence.
|
||
- **Printable report** — preview, print, or download a standalone HTML file in grams and
|
||
Fahrenheit. Includes an annotated smooth SVG curve, phase durations, milestone cues,
|
||
separate preheat fields, actuator schedule, and roast/cup record. Blank temperature inputs
|
||
use an illustrative reference (not a calibrated prediction); entered readings are preserved.
|
||
|
||
Local Node development requires Node 22.19 or newer (Docker uses Node 24).
|
||
Product-page fetching accepts public HTTPS destinations on port 443, validates every DNS
|
||
answer and redirect, pins the socket address, and keeps a deadline through body streaming.
|
||
The former Browserless fallback is removed because it could bypass these network protections.
|
||
|
||
## Run it
|
||
|
||
```bash
|
||
cp .env.example .env # set strong secrets
|
||
npm install
|
||
npm test
|
||
# local PostgreSQL stack (database is not published to the host)
|
||
docker compose --env-file .env up --build
|
||
```
|
||
|
||
The public landing page is at `/`; plans require an account at `/app`. Production is configured for `https://roast.srmr.xyz`: retain `APP_ORIGIN=https://roast.srmr.xyz` and `COOKIE_SECURE=true` behind its HTTPS proxy. Database migrations in `db/migrations/` run at application startup exactly once.
|
||
|
||
### First administrator
|
||
|
||
Generate `BOOTSTRAP_SETUP_TOKEN` with `openssl rand -base64 48`, keep it only in the deployment environment, then call `POST /api/auth/bootstrap` with that token, `[email protected]`, and a 12+ character password. The endpoint can create that account only once. Remove the setup token after success; it is optional thereafter and no administrator password is stored in source control.
|
||
|
||
### Pi agent configuration in Docker
|
||
|
||
The `app` service mounts `PI_AGENT_CONFIG_DIR` (default `./appdata/pi-agent`) read-only at `/run/pi-agent-config`. Its entrypoint copies that seed into the non-root Node user's writable runtime configuration directory before startup: Pi's credential storage needs to create a lock beside `auth.json`. This lets `/api/prefill` use the configured model without baking credentials into the image or mutating the host configuration. The directory is ignored by Git and Docker build context; do not commit its contents.
|
||
|
||
Before bringing up the stack, sync only the local Pi agent configuration you intend to make available to the container:
|
||
|
||
```bash
|
||
mkdir -p appdata/pi-agent
|
||
rsync -a --delete ~/.pi/agent/ appdata/pi-agent/
|
||
docker compose --env-file .env up --build
|
||
```
|
||
|
||
Set `PI_AGENT_CONFIG_DIR` to another protected host directory instead if preferred. Restrict access to that directory because it can contain provider credentials. The mount is read-only, so Pi cannot alter the host configuration. If deployed behind a reverse proxy, set `TRUST_PROXY` only to that proxy's specific IP/CIDR (or keep it blank when the app is directly exposed).
|
||
|
||
## Mobile and PWA use
|
||
|
||
The planner is responsive and caches its app shell for offline use after the first visit. Browser installation and service-worker caching require HTTPS in production (localhost is exempt). Put the Docker container behind an HTTPS reverse proxy before using it as an installable PWA on a phone.
|
||
|
||
Prefill requires a model configured via the `pi` CLI (`~/.pi/agent/{models,auth}.json`) —
|
||
without one, `/api/prefill` returns `503 no_model` rather than crashing. Everything else
|
||
(the form, ledger, curve, `.alog` upload) works with no model configured. Which configured
|
||
model runs prefill and roast reviews is chosen in the admin page's **LLM** section
|
||
(`app_settings.llm_model`, "Auto" = first available); the UI refers to the model backend
|
||
simply as "the LLM".
|
||
|
||
## Layout
|
||
|
||
- `shared/` — browser-safe ESM modules imported by *both* the Node server and the browser
|
||
(served statically at `/shared/`). This is the single source of truth for the ledger math,
|
||
time parsing, and reference data tables — never duplicate this logic in `server/` or `public/js/`.
|
||
- `server/` — Express routes: `/api/prefill`, `/api/alog`, `/api/alog/library[/:filename]`.
|
||
- `public/` — the static frontend: `index.html` (planning form), `app.css` (screen UI), and `js/`.
|
||
`shared/report.js` owns the standalone/print report. `worksheet.css` is a legacy asset.
|
||
|
||
## Where the reference numbers come from
|
||
|
||
`shared/reference-data.js` is ported from
|
||
`/Users/shane/dev/hope_roaster/roast-planner/manual-roast-planner.html`'s Step 1 (cultivars),
|
||
Step 3 (processing), Step 4 (roast levels), Step 8 (machine bands), and Step 7 (sanity bands).
|
||
If those tables change in the paper worksheet, port the change here too.
|
||
|
||
## Known gaps (v1)
|
||
|
||
- Offline drafts are intentionally scoped to the authenticated browser account and are cleared on logout; account-backed plans remain the authoritative copy.
|
||
- Roastetta (roastetta.com) integration is intentionally out of scope — it needs a headed,
|
||
Cloudflare-clearing browser and the operator's own credentials. Use the `.alog` file picker,
|
||
or point `ALOG_DIR` at wherever the `roastetta` skill already downloaded files.
|
||
|
||
## Regression checks
|
||
|
||
`npm test` includes account ownership, authorization, persistence, parsing, planner/report,
|
||
URL-fetch security, and Docker startup/migration checks. For the Chromium interaction check:
|
||
|
||
```sh
|
||
npx playwright install chromium
|
||
npm run test:browser
|
||
```
|
||
|
||
The browser check covers Fahrenheit/Celsius round trips without deleting inputs, the report
|
||
preview, standalone download, and print-only layout. It runs separately from the default test
|
||
suite so production CI does not require installing a browser.
|