Academy v4: 3 research-backed lessons (30 slides + audio); new bean logo; custom plan/lot names
Test and deploy / test-and-deploy (push) Successful in 59s
Test and deploy / test-and-deploy (push) Successful in 59s
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ec078116b4
commit
8b80c3fa6f
+45
-22
@@ -752,7 +752,7 @@ export function createApp({
|
||||
ok: true,
|
||||
plans: (
|
||||
await db.query(
|
||||
"SELECT id,plan,created_at,updated_at FROM roast_plans WHERE user_id=$1 ORDER BY updated_at DESC",
|
||||
"SELECT id,plan,name,created_at,updated_at FROM roast_plans WHERE user_id=$1 ORDER BY updated_at DESC",
|
||||
[req.user.id],
|
||||
)
|
||||
).rows,
|
||||
@@ -765,10 +765,11 @@ export function createApp({
|
||||
try {
|
||||
if (!req.body.plan || typeof req.body.plan !== "object")
|
||||
return res.status(400).json({ ok: false, code: "bad_plan" });
|
||||
const name = String(req.body.name ?? "").trim().slice(0, 120);
|
||||
const p = (
|
||||
await db.query(
|
||||
"INSERT INTO roast_plans(user_id,plan) VALUES($1,$2) RETURNING id,plan,created_at,updated_at",
|
||||
[req.user.id, req.body.plan],
|
||||
"INSERT INTO roast_plans(user_id,plan,name) VALUES($1,$2,$3) RETURNING id,plan,name,created_at,updated_at",
|
||||
[req.user.id, req.body.plan, name],
|
||||
)
|
||||
).rows[0];
|
||||
res.status(201).json({ ok: true, plan: p });
|
||||
@@ -783,11 +784,27 @@ export function createApp({
|
||||
requireUuidParam("id"),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
if (!req.body.plan || typeof req.body.plan !== "object")
|
||||
const hasPlan = req.body.plan !== undefined;
|
||||
const hasName = req.body.name !== undefined;
|
||||
if (hasPlan && (!req.body.plan || typeof req.body.plan !== "object"))
|
||||
return res.status(400).json({ ok: false, code: "bad_plan" });
|
||||
if (!hasPlan && !hasName)
|
||||
return res.status(400).json({ ok: false, code: "bad_plan" });
|
||||
const existing = hasPlan && hasName
|
||||
? null
|
||||
: (
|
||||
await db.query(
|
||||
"SELECT plan,name FROM roast_plans WHERE id=$1 AND user_id=$2",
|
||||
[req.params.id, req.user.id],
|
||||
)
|
||||
).rows[0];
|
||||
const plan = hasPlan ? req.body.plan : existing?.plan;
|
||||
const name = hasName
|
||||
? String(req.body.name ?? "").trim().slice(0, 120)
|
||||
: (existing?.name ?? "");
|
||||
const r = await db.query(
|
||||
"UPDATE roast_plans SET plan=$1,updated_at=now() WHERE id=$2 AND user_id=$3 RETURNING id,plan,updated_at",
|
||||
[req.body.plan, req.params.id, req.user.id],
|
||||
"UPDATE roast_plans SET plan=$1,name=$2,updated_at=now() WHERE id=$3 AND user_id=$4 RETURNING id,plan,name,updated_at",
|
||||
[plan, name, req.params.id, req.user.id],
|
||||
);
|
||||
if (!r.rowCount)
|
||||
return res.status(404).json({ ok: false, code: "not_found" });
|
||||
@@ -855,6 +872,7 @@ export function createApp({
|
||||
}
|
||||
const toLotRow = (row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
origin: row.origin,
|
||||
variety: row.variety,
|
||||
process: row.process,
|
||||
@@ -897,14 +915,16 @@ export function createApp({
|
||||
} catch (e) {
|
||||
return res.status(400).json({ ok: false, code: "bad_lot", error: e.message });
|
||||
}
|
||||
const name = String(req.body.name ?? "").trim().slice(0, 120);
|
||||
const row = (
|
||||
await db.query(
|
||||
`INSERT INTO green_bean_lots
|
||||
(user_id,origin,variety,process,producer,purchase_date,initial_weight_g,remaining_weight_g,cost_total,moisture_pct,density_g_l,notes)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$7,$8,$9,$10,$11) RETURNING *`,
|
||||
(user_id,origin,name,variety,process,producer,purchase_date,initial_weight_g,remaining_weight_g,cost_total,moisture_pct,density_g_l,notes)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$8,$9,$10,$11,$12) RETURNING *`,
|
||||
[
|
||||
req.user.id,
|
||||
origin,
|
||||
name,
|
||||
String(req.body.variety || ""),
|
||||
String(req.body.process || ""),
|
||||
String(req.body.producer || ""),
|
||||
@@ -937,7 +957,7 @@ export function createApp({
|
||||
if (!lot) return res.status(404).json({ ok: false, code: "not_found" });
|
||||
const log = (
|
||||
await db.query(
|
||||
`SELECT c.id,c.weight_g,c.roast_plan_id,c.created_at,p.plan->'fields'->>'0.1' AS plan_title
|
||||
`SELECT c.id,c.weight_g,c.roast_plan_id,c.created_at,CASE WHEN COALESCE(p.name,'')<>'' THEN p.name ELSE p.plan->'fields'->>'0.1' END AS plan_title
|
||||
FROM bean_consumption c LEFT JOIN roast_plans p ON p.id=c.roast_plan_id AND p.user_id=c.user_id
|
||||
WHERE c.lot_id=$1 ORDER BY c.created_at DESC`,
|
||||
[req.params.id],
|
||||
@@ -984,13 +1004,13 @@ export function createApp({
|
||||
await db.query(
|
||||
`SELECT one_change, plan_title, updated_at FROM (
|
||||
SELECT plan->'afterRoast'->>'oneChange' AS one_change,
|
||||
plan->'fields'->>'0.1' AS plan_title, updated_at
|
||||
CASE WHEN COALESCE(name,'')<>'' THEN name ELSE plan->'fields'->>'0.1' END AS plan_title, updated_at
|
||||
FROM roast_plans
|
||||
WHERE user_id=$1 AND plan->'inventory'->>'lotId'=$2
|
||||
AND coalesce(plan->'afterRoast'->>'oneChange','')<>''
|
||||
UNION ALL
|
||||
SELECT a.after->>'oneChange' AS one_change,
|
||||
p.plan->'fields'->>'0.1' AS plan_title, a.updated_at
|
||||
CASE WHEN COALESCE(p.name,'')<>'' THEN p.name ELSE p.plan->'fields'->>'0.1' END AS plan_title, a.updated_at
|
||||
FROM actual_roasts a
|
||||
JOIN roast_plans p ON p.id=a.roast_plan_id AND p.user_id=a.user_id
|
||||
WHERE a.user_id=$1 AND p.plan->'inventory'->>'lotId'=$2
|
||||
@@ -1061,17 +1081,20 @@ export function createApp({
|
||||
// computed inside the UPDATE itself (against the pre-statement initial_weight_g)
|
||||
// rather than from the `existing` row read above, so a concurrent PUT can't lose
|
||||
// the other's delta the way a JS-side read-then-write would.
|
||||
const name =
|
||||
b.name === undefined ? existing.name : String(b.name || "").trim().slice(0, 120);
|
||||
const row = (
|
||||
await db.query(
|
||||
`UPDATE green_bean_lots SET
|
||||
origin=$1, variety=$2, process=$3, producer=$4, purchase_date=$5,
|
||||
remaining_weight_g = remaining_weight_g + COALESCE($6::numeric, initial_weight_g) - initial_weight_g,
|
||||
initial_weight_g = COALESCE($6::numeric, initial_weight_g),
|
||||
cost_total=$7, moisture_pct=$8, density_g_l=$9, notes=$10, archived=$11,
|
||||
origin=$1, name=$2, variety=$3, process=$4, producer=$5, purchase_date=$6,
|
||||
remaining_weight_g = remaining_weight_g + COALESCE($7::numeric, initial_weight_g) - initial_weight_g,
|
||||
initial_weight_g = COALESCE($7::numeric, initial_weight_g),
|
||||
cost_total=$8, moisture_pct=$9, density_g_l=$10, notes=$11, archived=$12,
|
||||
updated_at=now()
|
||||
WHERE id=$12 AND user_id=$13 RETURNING *`,
|
||||
WHERE id=$13 AND user_id=$14 RETURNING *`,
|
||||
[
|
||||
origin,
|
||||
name,
|
||||
b.variety === undefined ? existing.variety : String(b.variety || ""),
|
||||
b.process === undefined ? existing.process : String(b.process || ""),
|
||||
b.producer === undefined ? existing.producer : String(b.producer || ""),
|
||||
@@ -1611,7 +1634,7 @@ export function createApp({
|
||||
const rows = (
|
||||
await db.query(
|
||||
`SELECT a.id,a.roast_plan_id,a.roaster_id,a.filename,a.parsed,a.evaluation,a.evaluation_status,a.evaluation_error,a.created_at,
|
||||
p.plan->'fields'->>'0.1' AS plan_title, m.name AS roaster_name
|
||||
CASE WHEN COALESCE(p.name,'')<>'' THEN p.name ELSE p.plan->'fields'->>'0.1' END AS plan_title, m.name AS roaster_name
|
||||
FROM actual_roasts a
|
||||
LEFT JOIN roast_plans p ON p.id=a.roast_plan_id AND p.user_id=a.user_id
|
||||
LEFT JOIN roasters m ON m.id=a.roaster_id AND m.user_id=a.user_id
|
||||
@@ -1633,7 +1656,7 @@ export function createApp({
|
||||
try {
|
||||
const row = (
|
||||
await db.query(
|
||||
`SELECT a.*, p.plan->'fields'->>'0.1' AS plan_title, p.plan, m.name AS roaster_name
|
||||
`SELECT a.*, CASE WHEN COALESCE(p.name,'')<>'' THEN p.name ELSE p.plan->'fields'->>'0.1' END AS plan_title, p.plan, m.name AS roaster_name
|
||||
FROM actual_roasts a
|
||||
LEFT JOIN roast_plans p ON p.id=a.roast_plan_id AND p.user_id=a.user_id
|
||||
LEFT JOIN roasters m ON m.id=a.roaster_id AND m.user_id=a.user_id
|
||||
@@ -1657,7 +1680,7 @@ export function createApp({
|
||||
const row = (
|
||||
await db.query(
|
||||
`SELECT a.filename, a.original_content, a.after, a.evaluation, a.parsed,
|
||||
p.plan->'fields'->>'0.1' AS plan_title, a.roast_plan_id
|
||||
CASE WHEN COALESCE(p.name,'')<>'' THEN p.name ELSE p.plan->'fields'->>'0.1' END AS plan_title, a.roast_plan_id
|
||||
FROM actual_roasts a
|
||||
LEFT JOIN roast_plans p ON p.id=a.roast_plan_id AND p.user_id=a.user_id
|
||||
WHERE a.id=$1 AND a.user_id=$2`,
|
||||
@@ -2369,7 +2392,7 @@ export function createApp({
|
||||
const rows = (
|
||||
await db.query(
|
||||
`SELECT s.id,s.roast_plan_id,s.data,s.total_score,s.created_at,s.updated_at,
|
||||
p.plan->'fields'->>'0.1' AS plan_title
|
||||
CASE WHEN COALESCE(p.name,'')<>'' THEN p.name ELSE p.plan->'fields'->>'0.1' END AS plan_title
|
||||
FROM cupping_sessions s LEFT JOIN roast_plans p ON p.id=s.roast_plan_id AND p.user_id=s.user_id
|
||||
WHERE s.user_id=$1 AND ($2::uuid IS NULL OR s.roast_plan_id=$2)
|
||||
ORDER BY s.updated_at DESC`,
|
||||
@@ -2423,7 +2446,7 @@ export function createApp({
|
||||
try {
|
||||
const row = (
|
||||
await db.query(
|
||||
`SELECT s.*, p.plan->'fields'->>'0.1' AS plan_title
|
||||
`SELECT s.*, CASE WHEN COALESCE(p.name,'')<>'' THEN p.name ELSE p.plan->'fields'->>'0.1' END AS plan_title
|
||||
FROM cupping_sessions s LEFT JOIN roast_plans p ON p.id=s.roast_plan_id AND p.user_id=s.user_id
|
||||
WHERE s.id=$1 AND s.user_id=$2`,
|
||||
[req.params.id, req.user.id],
|
||||
@@ -2753,7 +2776,7 @@ export function createApp({
|
||||
: null;
|
||||
const rows = (
|
||||
await db.query(
|
||||
`SELECT p.id,p.plan->'fields'->>'0.1' AS title,p.updated_at,u.id AS user_id,u.email
|
||||
`SELECT p.id,CASE WHEN COALESCE(p.name,'')<>'' THEN p.name ELSE p.plan->'fields'->>'0.1' END AS title,p.updated_at,u.id AS user_id,u.email
|
||||
FROM roast_plans p JOIN users u ON u.id=p.user_id
|
||||
WHERE $1::uuid IS NULL OR p.user_id=$1
|
||||
ORDER BY p.updated_at DESC`,
|
||||
|
||||
+3
-3
@@ -57,7 +57,7 @@ const brewBody = obj({
|
||||
tastingNotes: str, notes: str,
|
||||
});
|
||||
const lot = obj({
|
||||
id: str, origin: str, variety: str, process: str, producer: str,
|
||||
id: str, name: str, origin: str, variety: str, process: str, producer: str,
|
||||
purchaseDate: { ...str, nullable: true }, initialWeightG: { type: "number" },
|
||||
remainingWeightG: { type: "number" }, costTotal: num, moisturePct: num, densityGL: num,
|
||||
notes: str, archived: bool,
|
||||
@@ -93,10 +93,10 @@ export function buildOpenApiSpec({ origin = "" } = {}) {
|
||||
// ── Roast plans ──
|
||||
"/api/plans": {
|
||||
get: { tags: ["roast plans"], summary: "List your roast plans", responses: { 200: ok("Plans", obj({ ok: bool, plans: arr({ type: "object" }) })) } },
|
||||
post: { tags: ["roast plans"], summary: "Create a roast plan", requestBody: jsonBody(obj({ plan: { type: "object" } }, ["plan"])), responses: { 201: ok("Created") } },
|
||||
post: { tags: ["roast plans"], summary: "Create a roast plan", requestBody: jsonBody(obj({ plan: { type: "object" }, name: { ...str, description: "Optional custom display name (falls back to worksheet field 0.1 when blank)" } }, ["plan"])), responses: { 201: ok("Created") } },
|
||||
},
|
||||
"/api/plans/{id}": {
|
||||
put: { tags: ["roast plans"], summary: "Update a roast plan", parameters: [idParam], requestBody: jsonBody(obj({ plan: { type: "object" } }, ["plan"])), responses: { 200: ok("Updated"), 404: err("Not found") } },
|
||||
put: { tags: ["roast plans"], summary: "Update a roast plan and/or rename it (send `plan`, `name`, or both — omitted fields are left unchanged)", parameters: [idParam], requestBody: jsonBody(obj({ plan: { type: "object" }, name: str })), responses: { 200: ok("Updated"), 404: err("Not found") } },
|
||||
delete: { tags: ["roast plans"], summary: "Delete a roast plan", parameters: [idParam], responses: { 200: ok("Deleted"), 404: err("Not found") } },
|
||||
},
|
||||
"/api/machine-profile": { get: { tags: ["roast plans"], summary: "Learned per-user machine profile", responses: { 200: ok("Profile", obj({ ok: bool, profile: { type: "object" } })) } } },
|
||||
|
||||
Reference in New Issue
Block a user