diff --git a/app/lib/version.mjs b/app/lib/version.mjs
index be79714..ea5d250 100644
--- a/app/lib/version.mjs
+++ b/app/lib/version.mjs
@@ -1 +1 @@
-export const APP_VERSION = "0.4.0";
+export const APP_VERSION = "0.5.0";
diff --git a/app/package-lock.json b/app/package-lock.json
index d6fb391..c409951 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "bms-backbone",
- "version": "0.4.0",
+ "version": "0.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bms-backbone",
- "version": "0.4.0",
+ "version": "0.5.0",
"dependencies": {
"@prisma/client": "^6.7.0",
"bcryptjs": "^3.0.2"
diff --git a/app/package.json b/app/package.json
index b14be17..78821d2 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,6 +1,6 @@
{
"name": "bms-backbone",
- "version": "0.4.0",
+ "version": "0.5.0",
"private": true,
"type": "module",
"scripts": {
diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma
index 3aad118..ca51339 100644
--- a/app/prisma/schema.prisma
+++ b/app/prisma/schema.prisma
@@ -167,3 +167,36 @@ model PriceEntry {
note String @default("")
created DateTime @default(now())
}
+
+// ── Classes binder — a course, workshop, book, or video series Bonna learns from
+model Course {
+ id Int @id @default(autoincrement())
+ title String
+ subject String @default("") // methods · skills · marketing · business · glaze chemistry …
+ source String @default("") // teacher / school / author / channel
+ url String @default("")
+ status String @default("taking") // taking · want · done
+ notes String @default("")
+ created DateTime @default(now())
+ pages CoursePage[]
+}
+
+model CoursePage {
+ id Int @id @default(autoincrement())
+ courseId Int
+ course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
+ title String @default("")
+ text String
+ created DateTime @default(now())
+ updated DateTime @default(now())
+}
+
+// ── Mind dump — ideas, filed by category ("" = unsorted, from the Today jot box)
+model Idea {
+ id Int @id @default(autoincrement())
+ text String
+ category String @default("") // make · marketing · packaging · display · shop · other
+ medium String @default("") // for "make": clay · wood · silver · metal · mixed
+ status String @default("spark") // spark · trying · made · parked
+ created DateTime @default(now())
+}
diff --git a/app/public/app.js b/app/public/app.js
index 7a22feb..f4709c8 100644
--- a/app/public/app.js
+++ b/app/public/app.js
@@ -19,7 +19,7 @@ document.querySelectorAll("#nav button").forEach((b) =>
render();
}));
-function render() { ({ today, glazes, firings, pieces, files, sourcing, notes })[current](); }
+function render() { ({ today, glazes, firings, pieces, files, sourcing, classes, ideas, notes })[current](); }
// ── jot box (shared) ─────────────────────────────────────────────
const JOT_HINT = "⌨️ Enter saves · Shift+Enter = subtask line · Tab switches type · press / from anywhere to jot";
@@ -44,6 +44,7 @@ function wireCapture(after) {
await after();
const j = $("#jot");
if (j) { j.value = ""; j.focus(); } // stay in the box — rapid-fire jotting
+ if (kind === "idea") jotSaid("💭 dropped in the mind dump — file it there whenever");
if (kind === "suggestion") jotSaid(r.issue ? `💡 sent to Tony — suggestion #${r.issue}, thank you!`
: "💡 kept in your notes — it'll get passed along");
};
@@ -100,7 +101,7 @@ async function today() {
? `
🔥 Firing #${s.activeFiring.id} in progress — ${esc(s.activeFiring.type)} ${esc(s.activeFiring.cone)}
`
: `Kiln idle${s.lastUnload ? ` — last unload ${esc(s.lastUnload.unloaded?.slice(0, 10))}` : ""}
`;
- view.innerHTML = captureHTML([["note", "✨ note"], ["todo", "✓ to-do"], ["shopping", "🧺 shopping"], ["glaze idea", "🧪 glaze idea"], ["suggestion", "💡 suggestion"]]) +
+ view.innerHTML = captureHTML([["note", "✨ note"], ["todo", "✓ to-do"], ["shopping", "🧺 shopping"], ["glaze idea", "🧪 glaze idea"], ["idea", "💭 idea"], ["suggestion", "💡 suggestion"]]) +
`
To-do
${todoRows}
Shopping list 🧺
${shopRows}
@@ -434,14 +435,15 @@ function fileRow(f, showHome) {
${fmtSize(f.size)} · ${esc(f.created.slice(0, 10))}
${f.tag ? `${esc(f.tag)}` : ""}
${showHome && f.product_name ? `🏺 ${esc(f.product_name)}` : ""}
+ ${showHome && f.course_name ? `📚 ${esc(f.course_name)}` : ""}
`;
}
-function uploadWidget(entity, entityId, after) {
+function uploadWidget(entity, entityId, after, tags = FILE_TAGS) {
return {
html: `
-
tag as…${FILE_TAGS.map((t, i) => `${t}`).join("")}
`,
+ tag as…${tags.map((t, i) => `${t}`).join("")}
`,
wire() {
view.querySelectorAll("[data-ftag]").forEach((c) =>
c.addEventListener("click", () => view.querySelectorAll("[data-ftag]").forEach((x) => x.classList.toggle("on", x === c))));
@@ -472,7 +474,7 @@ async function files() {
view.innerHTML = `${up.html}
${section("Studio files 🗂️")}
${loose.map((f) => fileRow(f, false)).join("") || '
STLs for master molds, foam form templates, cut files… upload anything ⬆️
'}
- ${attached.length ? `${section("Attached to pieces 🏺")}${attached.map((f) => fileRow(f, true)).join("")}
` : ""}`;
+ ${attached.length ? `${section("Attached to pieces & classes")}${attached.map((f) => fileRow(f, true)).join("")}
` : ""}`;
up.wire();
wireFileDeletes(files);
}
@@ -623,6 +625,254 @@ async function notes() {
wireCapture(notes);
}
+// ── Classes (the education binder) ──────────────────────────────
+const SUBJECTS = ["methods", "skills", "marketing", "business", "glaze chemistry", "other"];
+const CLASS_STATUS = [["taking", "📖 taking now"], ["want", "🔖 want to take"], ["done", "✅ finished"]];
+const CLASS_TAGS = ["handout", "slides", "worksheet", "reference", "other"];
+const sectionH = (label) => `${label}
`;
+async function classes() {
+ const list = await api("/api/courses");
+ const card = (c) => `
+
${esc(c.title)}
+
${c.subject ? `${esc(c.subject)} ` : ""}${c.page_count} page${c.page_count === 1 ? "" : "s"}
+ ${c.source ? `
${esc(c.source)}
` : ""}
`;
+ const group = (key) => list.filter((c) => (c.status || "taking") === key);
+ view.innerHTML = `
+ courses, workshops, books, video series — one binder tab each
+ ${CLASS_STATUS.map(([k, label]) => {
+ const g = group(k);
+ if (!g.length && k !== "taking") return "";
+ return sectionH(label) + `${g.map(card).join("") ||
+ '
your binder starts here — add a class, a workshop, a book, or a video series 📚
'}
`;
+ }).join("")}`;
+ $("#new-class").addEventListener("click", newClassForm);
+ view.querySelectorAll("[data-c]").forEach((c) => c.addEventListener("click", () => classDetail(Number(c.dataset.c))));
+}
+
+function newClassForm() {
+ view.innerHTML = `
+ New class
+
A title is enough — this can be a course, a workshop, a book, a YouTube series, anything you're learning from.
+
+
+
+
+
${CLASS_STATUS.map(([k, l], i) => `${l}`).join("")}
+
+
+
+
+
`;
+ view.querySelectorAll("[data-cs]").forEach((c) =>
+ c.addEventListener("click", () => view.querySelectorAll("[data-cs]").forEach((x) => x.classList.toggle("on", x === c))));
+ $("#back").addEventListener("click", classes);
+ $("#c-title").focus();
+ $("#save-class").addEventListener("click", async () => {
+ const title = $("#c-title").value.trim();
+ if (!title) return alert("A title is all it needs 🙂");
+ const r = await api("/api/courses", { title, subject: $("#c-subject").value.trim(), source: $("#c-source").value.trim(),
+ url: $("#c-url").value.trim(), notes: $("#c-notes").value.trim(), status: view.querySelector("[data-cs].on")?.dataset.cs || "taking" });
+ classDetail(r.id);
+ });
+}
+
+async function classDetail(id) {
+ const c = await api(`/api/course?id=${id}`);
+ if (!c) return classes();
+ const up = uploadWidget("course", id, () => classDetail(id), CLASS_TAGS);
+ const page = (p) => `
+
${esc(p.title || "Page from " + p.created.slice(0, 10))}
+ ${esc(p.created.slice(0, 10))}
+
+
${esc(p.text)}
+
`;
+ view.innerHTML = `
+
+
${esc(c.title)}
+
${c.subject ? `
${esc(c.subject)} ` : ""}${c.source ? `
${esc(c.source)} ` : ""}${c.url ? `
open link ↗` : ""}
+
${CLASS_STATUS.map(([k, l]) => `${l}`).join("")}
+
+
+
+
+
Handouts & files
+ ${c.files.map((f) => fileRow(f, false)).join("") || '
nothing attached yet
'}
+ ${up.html}
+
+
+ ${sectionH("New page ✍️")}
+
+
+
+
⌘/Ctrl+Enter also saves
+ ${sectionH(`Pages (${c.pages.length})`)}
+ ${c.pages.map(page).join("") || 'no pages yet — your first class notes go above ✨
'}`;
+ $("#back").addEventListener("click", classes);
+ view.querySelectorAll("[data-cs]").forEach((ch) =>
+ ch.addEventListener("click", async () => { await api("/api/course/update", { id, status: ch.dataset.cs }); classDetail(id); }));
+ $("#save-notes").addEventListener("click", async () => {
+ await api("/api/course/update", { id, notes: $("#c-notes").value });
+ $("#notes-said").textContent = "saved ✓";
+ setTimeout(() => { const s = $("#notes-said"); if (s) s.textContent = ""; }, 3000);
+ });
+ $("#ph-add").addEventListener("click", () => $("#ph-file").click());
+ $("#ph-file").addEventListener("change", async (e) => {
+ const f = e.target.files[0];
+ if (f) { await fetch(`/api/photos?entity=course&id=${id}&name=${encodeURIComponent(f.name)}`, { method: "POST", body: f }); classDetail(id); }
+ });
+ up.wire();
+ wireFileDeletes(() => classDetail(id));
+ $("#del-class").addEventListener("click", async () => {
+ if (confirm(`Delete "${c.title}" and all its pages?`)) { await api("/api/course/delete", { id }); classes(); }
+ });
+ const addPage = async () => {
+ const text = $("#np-text").value.trim();
+ if (!text) return $("#np-text").focus();
+ await api("/api/course/pages", { course_id: id, title: $("#np-title").value.trim(), text });
+ classDetail(id);
+ };
+ $("#np-save").addEventListener("click", addPage);
+ $("#np-text").addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); addPage(); } });
+ $("#np-title").addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); $("#np-text").focus(); } });
+ const pageEl = (pid) => view.querySelector(`[data-page="${pid}"]`);
+ const toggleEdit = (pid, on) => {
+ const el = pageEl(pid);
+ el.querySelector(".pageform").hidden = !on;
+ el.querySelector(".page-text").hidden = on;
+ if (on) el.querySelector(".pe-text").focus();
+ };
+ view.querySelectorAll("[data-pedit]").forEach((b) => b.addEventListener("click", () => toggleEdit(Number(b.dataset.pedit), true)));
+ view.querySelectorAll("[data-pcancel]").forEach((b) => b.addEventListener("click", () => toggleEdit(Number(b.dataset.pcancel), false)));
+ view.querySelectorAll("[data-psave]").forEach((b) =>
+ b.addEventListener("click", async () => {
+ const pid = Number(b.dataset.psave), el = pageEl(pid);
+ await api("/api/course/pages/update", { id: pid, title: el.querySelector(".pe-title").value.trim(), text: el.querySelector(".pe-text").value });
+ classDetail(id);
+ }));
+ view.querySelectorAll("[data-pdel]").forEach((b) =>
+ b.addEventListener("click", async () => {
+ if (confirm("Delete this page?")) { await api("/api/course/pages/delete", { id: Number(b.dataset.pdel) }); classDetail(id); }
+ }));
+}
+
+// ── Mind Dump (ideas, categorized) ───────────────────────────────
+const IDEA_CATS = [["make", "🏺 things to make"], ["marketing", "📣 marketing"], ["packaging", "📦 packaging"], ["display", "🎪 display & booth"], ["shop", "🛍️ shop & site"], ["other", "💭 other"]];
+const MEDIA = ["clay", "wood", "silver", "metal", "mixed"];
+const IDEA_STATUS = [["spark", "✨ spark"], ["trying", "🔧 trying it"], ["made", "✅ did it"], ["parked", "💤 parked"]];
+const IDEA_HINT = "⌨️ Enter saves · Shift+Enter = new line · Tab switches category · press / from anywhere";
+let ideaFilter = "all";
+const catLabel = (k) => (IDEA_CATS.find(([c]) => c === k) || [k, "🗂️ unsorted"])[1];
+async function ideas() {
+ const list = await api("/api/ideas");
+ const count = (fn) => list.filter(fn).length;
+ const live = (i) => i.status !== "parked";
+ const shown = list.filter((i) =>
+ ideaFilter === "all" ? live(i)
+ : ideaFilter === "unsorted" ? !i.category && live(i)
+ : ideaFilter === "parked" ? i.status === "parked"
+ : i.category === ideaFilter && live(i));
+ const small = 'style="font-size:11.5px;padding:3px 9px"';
+ const card = (i) => `
+
${esc(i.text)}
+
${catLabel(i.category)} ${i.medium ? `${esc(i.medium)} ` : ""}${esc(i.created.slice(0, 10))}
+ ${i.photos.length ? `
${i.photos.map((p) => `

`).join("")}
` : ""}
+
${IDEA_STATUS.map(([k, l]) => `${l}`).join("")}
+
+
+
file under${IDEA_CATS.map(([k, l]) => `${l}`).join("")}
+ ${i.category === "make" ? `
medium${MEDIA.map((m) => `${m}`).join("")}
` : ""}
+
`;
+ const filterChip = (k, label, n) => n || k === "all" ? `${label}${n ? ` ${n}` : ""}` : "";
+ const filters = filterChip("all", "everything", count(live))
+ + filterChip("unsorted", "🗂️ unsorted", count((i) => !i.category && live(i)))
+ + IDEA_CATS.map(([k, l]) => filterChip(k, l, count((i) => i.category === k && live(i)))).join("")
+ + filterChip("parked", "💤 parked", count((i) => i.status === "parked"));
+ let body;
+ if (ideaFilter === "all") {
+ const groups = [["", "🗂️ unsorted — file these when you have a minute"], ...IDEA_CATS].map(([k, l]) => [k, l, shown.filter((i) => (i.category || "") === k)]).filter(([, , g]) => g.length);
+ body = groups.map(([, l, g]) => sectionH(l) + `${g.map(card).join("")}
`).join("")
+ || 'empty head, full studio — dump the first idea above 💭
';
+ } else {
+ body = `${shown.map(card).join("") || '
nothing here yet
'}
`;
+ }
+ view.innerHTML = `
+
+
+
file under…${IDEA_CATS.map(([k, l], i) => `${l}`).join("")}
+
medium${MEDIA.map((m) => `${m}`).join("")}
+
${IDEA_HINT}
+ ${filters}
+ ${body}
+ `;
+ // capture
+ const jot = $("#jot");
+ const catChips = [...view.querySelectorAll("[data-ncat]")];
+ const selectCat = (c) => { catChips.forEach((x) => x.classList.toggle("on", x === c)); $("#medium-row").hidden = c.dataset.ncat !== "make"; };
+ catChips.forEach((c) => c.addEventListener("click", () => selectCat(c)));
+ view.querySelectorAll("[data-nmed]").forEach((c) =>
+ c.addEventListener("click", () => view.querySelectorAll("[data-nmed]").forEach((x) => x.classList.toggle("on", x === c && !x.classList.contains("on")))));
+ const save = async () => {
+ const text = jot.value.trim();
+ if (!text) return;
+ const category = view.querySelector("[data-ncat].on")?.dataset.ncat || "make";
+ await api("/api/ideas", { text, category, medium: category === "make" ? view.querySelector("[data-nmed].on")?.dataset.nmed || "" : "" });
+ await ideas();
+ const j = $("#jot");
+ if (j) { j.value = ""; j.focus(); }
+ jotSaid("💭 saved — it's in the pile");
+ };
+ $("#idea-save").addEventListener("click", save);
+ jot.addEventListener("keydown", (e) => {
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); save(); }
+ else if (e.key === "Tab") {
+ e.preventDefault();
+ const i = catChips.findIndex((c) => c.classList.contains("on"));
+ selectCat(catChips[(i + (e.shiftKey ? catChips.length - 1 : 1)) % catChips.length]);
+ }
+ });
+ jot.addEventListener("input", () => setTimeout(() => { jot.style.height = "auto"; jot.style.height = jot.scrollHeight + "px"; }));
+ // filters
+ view.querySelectorAll("[data-if]").forEach((c) => c.addEventListener("click", () => { ideaFilter = c.dataset.if; ideas(); }));
+ // per-card actions
+ const idOf = (el) => Number(el.closest("[data-idea]").dataset.idea);
+ view.querySelectorAll("[data-ist]").forEach((c) =>
+ c.addEventListener("click", async () => { await api("/api/ideas/update", { id: idOf(c), status: c.dataset.ist }); ideas(); }));
+ view.querySelectorAll("[data-irefile]").forEach((b) =>
+ b.addEventListener("click", () => { const r = b.closest("[data-idea]").querySelector(".refile"); r.hidden = !r.hidden; }));
+ view.querySelectorAll("[data-icat]").forEach((c) =>
+ c.addEventListener("click", async () => {
+ const cat = c.dataset.icat;
+ await api("/api/ideas/update", { id: idOf(c), category: cat, ...(cat === "make" ? {} : { medium: "" }) });
+ ideas();
+ }));
+ view.querySelectorAll("[data-imed]").forEach((c) =>
+ c.addEventListener("click", async () => {
+ await api("/api/ideas/update", { id: idOf(c), medium: c.classList.contains("on") ? "" : c.dataset.imed }); ideas();
+ }));
+ view.querySelectorAll("[data-itodo]").forEach((b) =>
+ b.addEventListener("click", async () => {
+ const text = b.closest("[data-idea]").querySelector("p").textContent;
+ await api("/api/todos/add", { text: text.split("\n")[0].slice(0, 200) });
+ b.textContent = "✓ on your to-do list"; b.disabled = true;
+ }));
+ let photoTarget = null;
+ view.querySelectorAll("[data-iphoto]").forEach((b) =>
+ b.addEventListener("click", () => { photoTarget = idOf(b); $("#idea-ph").click(); }));
+ $("#idea-ph").addEventListener("change", async (e) => {
+ const f = e.target.files[0];
+ if (f && photoTarget) { await fetch(`/api/photos?entity=idea&id=${photoTarget}&name=${encodeURIComponent(f.name)}`, { method: "POST", body: f }); ideas(); }
+ });
+ view.querySelectorAll("[data-idel]").forEach((b) =>
+ b.addEventListener("click", async () => {
+ if (confirm("Delete this idea?")) { await api("/api/ideas/delete", { id: idOf(b) }); ideas(); }
+ }));
+}
+
document.getElementById("signout")?.addEventListener("click", async () => {
await api("/api/logout", {});
location.href = "/login";
diff --git a/app/public/index.html b/app/public/index.html
index 6e73566..2e14450 100644
--- a/app/public/index.html
+++ b/app/public/index.html
@@ -98,6 +98,8 @@
+
+
diff --git a/app/server.mjs b/app/server.mjs
index c0046a4..457baa2 100644
--- a/app/server.mjs
+++ b/app/server.mjs
@@ -109,6 +109,10 @@ const sFile = (f) => ({ id: f.id, entity: f.entity, entity_id: f.entityId, store
const sProduct = (p) => ({ id: p.id, name: p.name, kind: p.kind, category: p.category, price: p.price,
cost: p.cost, qty: p.qty, status: p.status, notes: p.notes, created: fmtDate(p.created) });
const sSale = (s) => ({ id: s.id, product_id: s.productId, qty: s.qty, price: s.price, cost: s.cost, created: fmtDate(s.created) });
+const sCourse = (c) => ({ id: c.id, title: c.title, subject: c.subject, source: c.source, url: c.url, status: c.status,
+ notes: c.notes, created: fmtDate(c.created), page_count: c._count?.pages ?? 0 });
+const sPage = (p) => ({ id: p.id, course_id: p.courseId, title: p.title, text: p.text, created: fmtDate(p.created), updated: fmtDate(p.updated) });
+const sIdea = (i) => ({ id: i.id, text: i.text, category: i.category, medium: i.medium, status: i.status, created: fmtDate(i.created) });
const sSupplier = (s) => ({ id: s.id, name: s.name, kind: s.kind, distance: s.distance, url: s.url,
notes: s.notes, search_url: s.searchUrl, created: fmtDate(s.created) });
@@ -249,6 +253,10 @@ const routes = {
for (const l of lines) await db.shopping.create({ data: { text: l } });
return { ok: true };
}
+ if (kind === "idea") { // → mind dump, unsorted
+ await db.idea.create({ data: { text: b.text } });
+ return { ok: true };
+ }
if (kind === "suggestion") {
try {
const issue = await fileSuggestion(b.text, session?.email || "");
@@ -405,11 +413,78 @@ const routes = {
"GET /api/files": async () => {
const files = await db.fileAsset.findMany({ orderBy: { id: "desc" } });
- const productIds = [...new Set(files.filter((f) => f.entity === "product").map((f) => f.entityId))];
- const products = await db.product.findMany({ where: { id: { in: productIds } } });
+ const idsOf = (entity) => [...new Set(files.filter((f) => f.entity === entity).map((f) => f.entityId))];
+ const products = await db.product.findMany({ where: { id: { in: idsOf("product") } } });
+ const courses = await db.course.findMany({ where: { id: { in: idsOf("course") } } });
const nameOf = Object.fromEntries(products.map((p) => [p.id, p.name]));
- return files.map((f) => ({ ...sFile(f), product_name: f.entity === "product" ? nameOf[f.entityId] ?? null : null }));
+ const courseOf = Object.fromEntries(courses.map((c) => [c.id, c.title]));
+ return files.map((f) => ({ ...sFile(f), product_name: f.entity === "product" ? nameOf[f.entityId] ?? null : null,
+ course_name: f.entity === "course" ? courseOf[f.entityId] ?? null : null }));
},
+ // ── Classes binder ─────────────────────────────────────────────
+ "GET /api/courses": async () =>
+ (await db.course.findMany({ orderBy: [{ status: "asc" }, { title: "asc" }], include: { _count: { select: { pages: true } } } })).map(sCourse),
+ "GET /api/course": async (_b, q) => {
+ const id = Number(q.get("id"));
+ const c = await db.course.findUnique({ where: { id }, include: { pages: { orderBy: { id: "desc" } } } });
+ if (!c) return null;
+ return { ...sCourse(c), pages: c.pages.map(sPage),
+ photos: (await db.photo.findMany({ where: { entity: "course", entityId: id } })).map(sPhoto),
+ files: (await db.fileAsset.findMany({ where: { entity: "course", entityId: id }, orderBy: { id: "desc" } })).map(sFile) };
+ },
+ "POST /api/courses": async (b) => {
+ const c = await db.course.create({ data: { title: b.title, subject: b.subject || "", source: b.source || "",
+ url: b.url || "", status: b.status || "taking", notes: b.notes || "" } });
+ return { id: c.id };
+ },
+ "POST /api/course/update": async (b) => {
+ const data = {};
+ for (const k of ["title", "subject", "source", "url", "status", "notes"]) if (typeof b[k] === "string") data[k] = b[k];
+ await db.course.update({ where: { id: b.id }, data });
+ return { ok: true };
+ },
+ "POST /api/course/delete": async (b) => {
+ await db.course.delete({ where: { id: b.id } }); // pages cascade
+ await db.photo.deleteMany({ where: { entity: "course", entityId: b.id } });
+ for (const f of await db.fileAsset.findMany({ where: { entity: "course", entityId: b.id } })) {
+ try { unlinkSync(join(FILES, f.stored)); } catch { /* already gone */ }
+ }
+ await db.fileAsset.deleteMany({ where: { entity: "course", entityId: b.id } });
+ return { ok: true };
+ },
+ "POST /api/course/pages": async (b) => {
+ const p = await db.coursePage.create({ data: { courseId: b.course_id, title: b.title || "", text: b.text || "" } });
+ return { id: p.id };
+ },
+ "POST /api/course/pages/update": async (b) => {
+ await db.coursePage.update({ where: { id: b.id }, data: {
+ ...(typeof b.title === "string" ? { title: b.title } : {}), ...(typeof b.text === "string" ? { text: b.text } : {}), updated: new Date() } });
+ return { ok: true };
+ },
+ "POST /api/course/pages/delete": async (b) => { await db.coursePage.delete({ where: { id: b.id } }); return { ok: true }; },
+
+ // ── Mind dump ──────────────────────────────────────────────────
+ "GET /api/ideas": async () => {
+ const list = await db.idea.findMany({ orderBy: { id: "desc" } });
+ const photos = await db.photo.findMany({ where: { entity: "idea" } });
+ return list.map((i) => ({ ...sIdea(i), photos: photos.filter((p) => p.entityId === i.id).map(sPhoto) }));
+ },
+ "POST /api/ideas": async (b) => {
+ const i = await db.idea.create({ data: { text: b.text, category: b.category || "", medium: b.medium || "", status: b.status || "spark" } });
+ return { id: i.id };
+ },
+ "POST /api/ideas/update": async (b) => {
+ const data = {};
+ for (const k of ["text", "category", "medium", "status"]) if (typeof b[k] === "string") data[k] = b[k];
+ await db.idea.update({ where: { id: b.id }, data });
+ return { ok: true };
+ },
+ "POST /api/ideas/delete": async (b) => {
+ await db.idea.delete({ where: { id: b.id } });
+ await db.photo.deleteMany({ where: { entity: "idea", entityId: b.id } });
+ return { ok: true };
+ },
+
"POST /api/files/delete": async (b) => {
const f = await db.fileAsset.findUnique({ where: { id: b.id } });
if (f) {