diff --git a/README.md b/README.md index d6f9f75..ab322c2 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,25 @@ A fillable, live-computing web version of the manual coffee roast plan worksheet 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 + learned roaster behavior** — an "Ask the LLM" drawer on the planner chats + about the open plan, grounded in the computed ledger, the learned pace profile, and a + roaster-behavior profile (`/api/roaster-profile`) aggregated from every uploaded .alog + (turning-point lag, phase RoR, median milestone temps). The same profile feeds roast + reviews and supplies the planner curve's fallback milestone temps. +- **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. - **Live ledger** — the worksheet's time-ledger math (first crack, yellow, Maillard, development, drop, and the four sanity-check ratios) recomputes as you type, using the exact same arithmetic as the paper worksheet (verified against both its worked examples). diff --git a/db/migrations/006_brewing.sql b/db/migrations/006_brewing.sql new file mode 100644 index 0000000..dfb6876 --- /dev/null +++ b/db/migrations/006_brewing.sql @@ -0,0 +1,53 @@ +-- Additive only. Brewing side of the app: roasted/purchased bean management, brew logs, and +-- user-generated API tokens. Bean remaining weight is computed (initial - sum of brew doses), +-- never stored, so it can't drift. +CREATE TABLE roasted_beans ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name text NOT NULL, + roaster text NOT NULL DEFAULT '', + origin text NOT NULL DEFAULT '', + process text NOT NULL DEFAULT '', + variety text NOT NULL DEFAULT '', + roast_level text NOT NULL DEFAULT '', + roast_date date, + initial_weight_g numeric, + url text NOT NULL DEFAULT '', + tasting_notes text NOT NULL DEFAULT '', + notes text NOT NULL DEFAULT '', + roast_plan_id uuid REFERENCES roast_plans(id) ON DELETE SET NULL, + archived boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +CREATE TABLE brews ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + bean_id uuid REFERENCES roasted_beans(id) ON DELETE SET NULL, + method text NOT NULL, + dose_g numeric, + water_g numeric, + yield_g numeric, + grinder text NOT NULL DEFAULT '', + grind_setting text NOT NULL DEFAULT '', + water_temp_c numeric, + brew_time_s integer, + bloom_time_s integer, + rating numeric, + tasting_notes text NOT NULL DEFAULT '', + notes text NOT NULL DEFAULT '', + brewed_at timestamptz NOT NULL DEFAULT now(), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +CREATE TABLE api_tokens ( + token_hash text PRIMARY KEY, + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name text NOT NULL DEFAULT '', + created_at timestamptz NOT NULL DEFAULT now(), + last_used_at timestamptz +); +CREATE INDEX roasted_beans_user ON roasted_beans(user_id, archived, created_at DESC); +CREATE INDEX brews_user ON brews(user_id, brewed_at DESC); +CREATE INDEX brews_bean ON brews(bean_id); +CREATE INDEX api_tokens_user ON api_tokens(user_id); diff --git a/package-lock.json b/package-lock.json index 85266e6..933926d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "bcryptjs": "^3.0.3", "express": "^5.0.1", "nodemailer": "^9.0.3", - "pg": "^8.22.0" + "pg": "^8.22.0", + "swagger-ui-dist": "^5.32.12" }, "devDependencies": { "pg-mem": "^3.0.14", @@ -1861,6 +1862,13 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -3330,6 +3338,15 @@ "node": ">=14.18.0" } }, + "node_modules/swagger-ui-dist": { + "version": "5.32.12", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.12.tgz", + "integrity": "sha512-ceXE+KNc5LXafhh36trB6nxK+U2EIKUWzHT7sBiMosf2eJLAefalYnTwMouQBmSyED6m7Yz+GYZEcL+Glt3sww==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/package.json b/package.json index 0c771c0..5ea5ec0 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "bcryptjs": "^3.0.3", "express": "^5.0.1", "nodemailer": "^9.0.3", - "pg": "^8.22.0" + "pg": "^8.22.0", + "swagger-ui-dist": "^5.32.12" }, "devDependencies": { "pg-mem": "^3.0.14", diff --git a/public/account.html b/public/account.html index 3fd45e7..7a1b139 100644 --- a/public/account.html +++ b/public/account.html @@ -15,36 +15,7 @@ Roast Planner - + + + +
+ + + + + + + + + + +
NameCreatedLast used
+
+ + + +
+

Your data

+
+

+ Download everything you've put into Roast Planner — plans, + roasts, inventory, cupping, beans, and brews — as one JSON + file. +

+ Download my data +
+
+

Danger zone

diff --git a/public/admin.html b/public/admin.html index 3e5ae94..7fdec61 100644 --- a/public/admin.html +++ b/public/admin.html @@ -15,36 +15,7 @@ Roast Planner
- +
+
+

Backup

+
+

+ Export downloads the entire database (all + users and their data) as one JSON file. Import + replaces everything with a previously + exported file. +

+
+ Export full backup + +
+

+
+
+

Users

diff --git a/public/api-docs.html b/public/api-docs.html new file mode 100644 index 0000000..b62e734 --- /dev/null +++ b/public/api-docs.html @@ -0,0 +1,46 @@ + + + + + + API docs — Roast Planner + + + + + +
+ Roast Planner API + Authenticate with your session (already active in this browser) or an + API token from your account page. + ← Back to the app +
+
+ + + + diff --git a/public/app.css b/public/app.css index b0dd01b..2d35f53 100644 --- a/public/app.css +++ b/public/app.css @@ -2866,3 +2866,137 @@ select.f { padding: 0 0.5mm; color: #1a1512; } + +/* ── Brewer picker + brew log (brews/beans pages) ─────────────────────── */ +.brewer-cat-title { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-3); + margin: 14px 0 6px; +} +.brewer-cat-title:first-child { + margin-top: 0; +} +.brewer-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(94px, 1fr)); + gap: 8px; +} +.brewer-tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 10px 6px 8px; + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: var(--surface-raised); + cursor: pointer; + color: var(--ink-2); + font: inherit; + transition: border-color 0.12s, background 0.12s, color 0.12s; +} +.brewer-tile:hover { + border-color: var(--line-strong); + color: var(--ink); +} +.brewer-tile.selected { + border-color: var(--ember); + color: var(--ember-strong); + background: var(--ember-soft); + box-shadow: inset 0 0 0 1px var(--ember); +} +.brewer-tile svg { + width: 42px; + height: 42px; + display: block; +} +.brewer-tile .brewer-name { + font-size: 11.5px; + line-height: 1.2; + text-align: center; +} +.method-chip { + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} +.method-chip svg { + width: 20px; + height: 20px; + flex: none; + color: var(--ink-2); +} +.rating-pill { + display: inline-block; + min-width: 34px; + text-align: center; + padding: 2px 8px; + border-radius: 999px; + background: var(--surface-sunken); + font-variant-numeric: tabular-nums; + font-weight: 600; +} +.rating-pill.good { + background: var(--pass-bg); + color: var(--pass); +} +.rating-pill.poor { + background: var(--fail-bg); + color: var(--fail); +} +.token-reveal { + background: var(--surface-sunken); + border: 1px dashed var(--line-strong); + border-radius: var(--radius-sm); + padding: 10px 12px; + font-family: var(--font-mono, ui-monospace, monospace); + font-size: 12.5px; + word-break: break-all; + user-select: all; +} + +/* ── Plan chat drawer ─────────────────────────────────────────────────── */ +.chat-thread { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 50vh; + overflow-y: auto; + margin-bottom: 10px; +} +.chat-msg { + padding: 8px 11px; + border-radius: var(--radius-md); + font-size: 13px; + line-height: 1.45; + white-space: pre-wrap; + max-width: 92%; +} +.chat-msg.user { + align-self: flex-end; + background: var(--ember-soft); + color: var(--ember-strong); +} +.chat-msg.assistant { + align-self: flex-start; + background: var(--surface-sunken); + color: var(--ink); +} +.chat-msg.pending { + color: var(--ink-3); + font-style: italic; +} +.chat-input-row { + display: flex; + gap: 8px; + align-items: flex-end; +} +.chat-input-row textarea { + flex: 1; + resize: vertical; + min-height: 40px; +} diff --git a/public/beans.html b/public/beans.html new file mode 100644 index 0000000..f9e0f96 --- /dev/null +++ b/public/beans.html @@ -0,0 +1,186 @@ + + + + + + Beans — Roast Planner + + + + + +
+ + +
+
+
+ +
+
+

Beans

+

Roasted or purchased coffee ready to brew

+
+
+
+
+ + +
+
+
+

Your beans

+ +
+
+
+ + + + + + + + + + + + +
BeanOrigin / processRoastedRemainingBrews
+
+
+
+ +
+

Add a bean

+
+
+ + +
+ +
+
+ + + + + + + + +
+ + + + + +
+
+
+
+
+
+ + + diff --git a/public/brews.html b/public/brews.html new file mode 100644 index 0000000..ea5e9c9 --- /dev/null +++ b/public/brews.html @@ -0,0 +1,190 @@ + + + + + + Brews — Roast Planner + + + + + +
+ + +
+
+
+ +
+
+

Brews

+

Log every cup — method, recipe, and how it tasted

+
+
+
+
+ + +
+
+
+

Log a brew

+ +
+
+
+ +
+
+ + + + + + + + + +
+ + + + +
+
+
+ +
+
+

Brew history

+ +
+
+
+ + + + + + + + + + + + +
WhenMethodBeanRecipeRating
+
+
+
+
+
+
+ + + diff --git a/public/cupping.html b/public/cupping.html index 826af45..784c42b 100644 --- a/public/cupping.html +++ b/public/cupping.html @@ -15,36 +15,7 @@ Roast Planner
- + - Roasts - Inventory - Cupping - Account - - - + +