Onboarding wizard — ingest any staffing CSV in 3 steps
New /onboard page. Client-facing wizard for getting real data into the system without engineering help. Flow: 1. Drop a CSV (or click 'Use the sample as my data' — ships a 25-row realistic staffing roster under /samples/staffing_roster_sample.csv) 2. Browser parses client-side. Columns auto-typed (text/int/decimal/ date). PII flagged by name hint AND content regex (emails, phones). First rows previewed. Read-only — nothing written yet. 3. Name the dataset (lowercase+underscores). Commit. 4. Post-commit: dataset is live. Shows 4 next steps the operator can take (SQL query, vector index, dashboard search, playbook training). Backend: - /onboard serves onboard.html - /samples/*.csv serves CSV files from mcp-server/samples/ with filename validation (only [a-zA-Z0-9_-.]+.csv, prevents path traversal) - /onboard/ingest forwards multipart/form-data to gateway /ingest/file preserving the boundary. The generic /api/* passthrough breaks multipart because it reads as text and forwards as JSON; this route uses arrayBuffer + original Content-Type. Verified end-to-end: upload sample roster (25 rows, 12 columns) → parse in browser → show columns + PII flags + preview → commit → gateway writes Parquet, registers in catalog → immediately queryable: SELECT * FROM onboard_demo2 LIMIT 3 → Sarah Johnson, Forklift Operator, Chicago, IL, 0.92 Round-trip <1 second. Nav updated on all pages to link Onboard. Shipped with a sample CSV so the full flow is demonstrable without real client data. When a real client shows up, same path — they upload their CSV. No engineering ticket, no code change, no schema pre-definition. Security: sample filename regex prevents path traversal. CSV parse is client-side pure JS (no DOM injection). Commit uses existing /ingest/file validation (schema fingerprint, PII server-side, content-hash dedup).
This commit is contained in:
@@ -650,6 +650,54 @@ async function main() {
|
||||
});
|
||||
}
|
||||
|
||||
// Onboard — client-facing ingest wizard. Upload any CSV, preview
|
||||
// columns + PII + sample rows, commit via /ingest/file. Works
|
||||
// with a shipped sample roster so anyone can trial the flow
|
||||
// without real client data.
|
||||
if (url.pathname === "/onboard") {
|
||||
return new Response(Bun.file(import.meta.dir + "/onboard.html"), {
|
||||
headers: { ...cors, "Content-Type": "text/html" },
|
||||
});
|
||||
}
|
||||
|
||||
// Onboard ingest — forwards multipart/form-data correctly to
|
||||
// the Rust gateway /ingest/file. The generic /api/* passthrough
|
||||
// can't handle multipart because it reads as text and forwards
|
||||
// as JSON, losing the boundary. This route preserves the body
|
||||
// and Content-Type.
|
||||
if (url.pathname === "/onboard/ingest" && req.method === "POST") {
|
||||
const name = url.searchParams.get("name");
|
||||
if (!name || !/^[a-z][a-z0-9_]*$/.test(name)) {
|
||||
return err("dataset name required (lowercase+underscores)", 400);
|
||||
}
|
||||
const contentType = req.headers.get("content-type") || "";
|
||||
const upstream = await fetch(`${BASE}/ingest/file?name=${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": contentType },
|
||||
body: await req.arrayBuffer(),
|
||||
});
|
||||
const body = await upstream.text();
|
||||
return new Response(body, {
|
||||
status: upstream.status,
|
||||
headers: { ...cors, "Content-Type": upstream.headers.get("content-type") || "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Sample files (downloadable + fetchable from the onboard wizard)
|
||||
if (url.pathname.startsWith("/samples/")) {
|
||||
const name = url.pathname.slice("/samples/".length);
|
||||
if (!/^[a-zA-Z0-9_\-\.]+\.csv$/.test(name)) {
|
||||
return err("invalid sample filename", 400);
|
||||
}
|
||||
const path = `${import.meta.dir}/samples/${name}`;
|
||||
const file = Bun.file(path);
|
||||
if (!(await file.exists())) return err("sample not found", 404);
|
||||
return new Response(file, {
|
||||
headers: { ...cors, "Content-Type": "text/csv",
|
||||
"Content-Disposition": `attachment; filename="${name}"` },
|
||||
});
|
||||
}
|
||||
|
||||
// Proof JSON API (same data, no HTML)
|
||||
if (url.pathname === "/proof.json") {
|
||||
const ds = await api("GET", "/catalog/datasets") as any[];
|
||||
|
||||
Reference in New Issue
Block a user