diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0d51f..28e4c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Remove sample-data flash on load (2026-06-16) + +### Changed +- App starts with an empty in-memory board and waits for `GET /api/board` before the first render. +- Sample tasks moved to `src/data/sample-tasks.js` and used only when the server is unreachable (static `npx serve` mode). + +### Reasoning +- Rendering built-in sample data first, then swapping in the DB board caused a visible flash on every refresh when running `python server.py`. + ## SQLite persistence (2026-06-15) ### Added diff --git a/README.md b/README.md index 83cf226..b03139c 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ npm run test:watch | `index.html` | UI markup and styles | | `src/app/main.js` | Application logic (DOM, rendering, interactions) | | `src/data/constants.js` | Team roster, clients, sizes, colors | -| `src/lib/board-sync.js` | Server load/save (non-blocking) | +| `src/data/sample-tasks.js` | Demo tasks for static-only fallback | +| `src/lib/board-sync.js` | Server load/save (first render after load) | | `server.py` | Flask app + SQLite API | | `tests/` | Vitest unit tests | diff --git a/src/app/main.js b/src/app/main.js index e1f4f53..553a100 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -15,6 +15,7 @@ import { normalizeProposal, mockTranscript, isoCap, } from "../lib/capture.js"; import { startBoardSync } from "../lib/board-sync.js"; +import { buildSampleTasks } from "../data/sample-tasks.js"; /* ================= sample data ================= */ /* al = ASR aliases: common Whisper mishearings of each name. @@ -25,82 +26,7 @@ const VOCAB_TEXT = buildVocabText(); const { T, setUid, getUid } = createTaskFactory(); -const DATA = [ - /* ---- Client pilot: Derichebourg (waste-sorting robot) ---- */ - T("Derichebourg pilot — sorting robot","ia",{p:"high",d:"2026-07-10",open:true,c:[ - T("Integrate RS03 drive motors","sk",{p:"high",d:"2026-06-16",s:"l",open:true,c:[ - T("Mount RS03 motors and couplers","sk",{done:true,d:"2026-06-08",s:"m"}), - T("Wire motor CAN bus to controller","sk",{d:"2026-06-14",s:"m"}), - T("Calibrate RS03 torque limits","sk",{d:"2026-06-17",s:"s"}), - ]}), - T("Tune obstacle avoidance for the sorting line","ak",{p:"high",d:"2026-06-19",s:"l",open:true,c:[ - T("Collect depth data along the conveyor","ak",{done:true,d:"2026-06-10",s:"m"}), - T("Train avoidance model","ak",{d:"2026-06-18",s:"l"}), - T("Field test near the conveyor","ak",{d:"2026-06-22",s:"m"}), - ]}), - T("Fix D-Wave board brownout under load","ly",{p:"high",d:"2026-06-12",s:"m",open:true,c:[ - T("Diagnose the power regulator","ly",{done:true,d:"2026-06-11",s:"s"}), - T("Replace regulator and retest","ly",{d:"2026-06-13",s:"m"}), - ]}), - T("Approve motor procurement budget","jn",{d:"2026-06-15",s:"s"}), - T("Coordinate on-site pilot install","fd",{d:"2026-06-30",s:"l"}), - ]}), - - /* ---- Client pilot: JCDecaux (billboard-servicing robot) ---- */ - T("JCDecaux pilot — billboard servicing","ak",{p:"high",d:"2026-07-20",open:true,c:[ - T("Design board-mount manipulator arm","ia",{d:"2026-06-23",s:"l",open:true,c:[ - T("CAD the arm linkage","ia",{d:"2026-06-18",s:"m"}), - T("Source Feetech servos for the arm","lm",{d:"2026-06-20",s:"s"}), - ]}), - T("Autonomous navigation between billboards","ak",{p:"high",d:"2026-06-26",s:"xl",open:true,c:[ - T("Build city route planner","ak",{d:"2026-06-24",s:"l"}), - T("GPS waypoint following","ak",{d:"2026-06-25",s:"m"}), - ]}), - T("Hub-motor sizing for outdoor terrain","sk",{d:"2026-06-20",s:"m"}), - T("Demo prep for JCDecaux","fd",{d:"2026-06-27",s:"s"}), - ]}), - - /* ---- Client pilot: Onet (floor-cleaning autonomy) ---- */ - T("Onet pilot — floor-cleaning autonomy","ak",{d:"2026-08-01",open:true,c:[ - T("Map the Onet facility floorplan","ak",{d:"2026-06-21",s:"m"}), - T("Integrate Feetech servos for the brush arm","sk",{d:"2026-06-24",s:"m",open:true,c:[ - T("Mount the brush assembly","lm",{d:"2026-06-22",s:"s"}), - T("Tune servo sweep pattern","sk",{d:"2026-06-25",s:"s"}), - ]}), - T("Safety e-stop wiring","ly",{p:"high",d:"2026-06-16",s:"s"}), - ]}), - - /* ---- Internal R&D: core platform ---- */ - T("RoboOS v2 — core platform","ia",{p:"high",d:"2026-07-31",open:true,c:[ - T("Migrate OS to RS04 motor drivers","sk",{p:"high",d:"2026-06-24",s:"l",open:true,c:[ - T("Port CAN driver to RS04","sk",{d:"2026-06-22",s:"m"}), - T("Bench-test RS04 closed loop","sk",{d:"2026-06-23",s:"m"}), - ]}), - T("Real-time locomotion controller","ak",{d:"2026-06-29",s:"l"}), - T("Evaluate EL05 actuators","sk",{d:"2026-06-17",s:"m",open:true,c:[ - T("Run EL05 load tests","sk",{done:true,d:"2026-06-09",s:"s"}), - T("Compare EL05 vs RS02 efficiency","sk",{d:"2026-06-18",s:"s"}), - ]}), - T("Nightly build + hardware-in-the-loop rig","ia",{d:"2026-06-21",s:"m"}), - T("Assemble robot chassis v2","ia",{d:"2026-07-06",s:"l"}), - ]}), - - /* ---- Client pilot: NSI (inventory-scanning robot) ---- */ - T("NSI pilot — inventory scanning","ia",{d:"2026-07-15",open:true,c:[ - T("Scoping follow-up with NSI","fd",{d:"2026-06-19",s:"s"}), - T("Barcode scanner integration","sk",{d:"2026-06-28",s:"m"}), - T("Aisle navigation tuning","ak",{d:"2026-07-02",s:"m"}), - ]}), -]; - -/* subtasks inherit their parent task's owner by default — with an occasional - (seeded-random) different owner sprinkled in, like a real team would have */ -{ let seed=7; const rnd=()=>(seed=(seed*1103515245+12345)%2147483648)/2147483648; - const keys=Object.keys(PEOPLE); - const walk=(nodes,parent,depth)=>nodes.forEach(n=>{ - if(depth>=2&&parent) n.owner=rnd()<0.7?parent.owner:keys[Math.floor(rnd()*keys.length)]; - walk(n.children,n,depth+1); }); - walk(DATA,null,0); } +const DATA = []; const findPath = (id, nodes = DATA, path = []) => findPathIn(id, nodes, path); const depthOf = (id) => depthOfIn(id, DATA); @@ -1587,8 +1513,6 @@ function renderAll(){ renderFilter(); renderDash(); } Object.defineProperty(window, "CAP", { get: () => CAP, configurable: true }); -renderAll(); - const _globals = { toggleSearch, openTeam, micFabTap, openTranscript, toggleSettings, toggleSidebar, closeSettings, toggleFlyout, toggleFocus, toggleShowDone, toggleSubs, closeCapture, toggleCapLang, minimizeCapture, @@ -1607,4 +1531,5 @@ startBoardSync({ setUid, renderAll, onReady: (save) => { requestSave = save; }, + fallback: () => { DATA.splice(0, DATA.length, ...buildSampleTasks(T)); }, }); diff --git a/src/data/sample-tasks.js b/src/data/sample-tasks.js new file mode 100644 index 0000000..ac1329c --- /dev/null +++ b/src/data/sample-tasks.js @@ -0,0 +1,77 @@ +import { PEOPLE } from "./constants.js"; + +export function buildSampleTasks(T) { + const data = [ + T("Derichebourg pilot - sorting robot", "ia", { p: "high", d: "2026-07-10", open: true, c: [ + T("Integrate RS03 drive motors", "sk", { p: "high", d: "2026-06-16", s: "l", open: true, c: [ + T("Mount RS03 motors and couplers", "sk", { done: true, d: "2026-06-08", s: "m" }), + T("Wire motor CAN bus to controller", "sk", { d: "2026-06-14", s: "m" }), + T("Calibrate RS03 torque limits", "sk", { d: "2026-06-17", s: "s" }), + ] }), + T("Tune obstacle avoidance for the sorting line", "ak", { p: "high", d: "2026-06-19", s: "l", open: true, c: [ + T("Collect depth data along the conveyor", "ak", { done: true, d: "2026-06-10", s: "m" }), + T("Train avoidance model", "ak", { d: "2026-06-18", s: "l" }), + T("Field test near the conveyor", "ak", { d: "2026-06-22", s: "m" }), + ] }), + T("Fix D-Wave board brownout under load", "ly", { p: "high", d: "2026-06-12", s: "m", open: true, c: [ + T("Diagnose the power regulator", "ly", { done: true, d: "2026-06-11", s: "s" }), + T("Replace regulator and retest", "ly", { d: "2026-06-13", s: "m" }), + ] }), + T("Approve motor procurement budget", "jn", { d: "2026-06-15", s: "s" }), + T("Coordinate on-site pilot install", "fd", { d: "2026-06-30", s: "l" }), + ] }), + + T("JCDecaux pilot - billboard servicing", "ak", { p: "high", d: "2026-07-20", open: true, c: [ + T("Design board-mount manipulator arm", "ia", { d: "2026-06-23", s: "l", open: true, c: [ + T("CAD the arm linkage", "ia", { d: "2026-06-18", s: "m" }), + T("Source Feetech servos for the arm", "lm", { d: "2026-06-20", s: "s" }), + ] }), + T("Autonomous navigation between billboards", "ak", { p: "high", d: "2026-06-26", s: "xl", open: true, c: [ + T("Build city route planner", "ak", { d: "2026-06-24", s: "l" }), + T("GPS waypoint following", "ak", { d: "2026-06-25", s: "m" }), + ] }), + T("Hub-motor sizing for outdoor terrain", "sk", { d: "2026-06-20", s: "m" }), + T("Demo prep for JCDecaux", "fd", { d: "2026-06-27", s: "s" }), + ] }), + + T("Onet pilot - floor-cleaning autonomy", "ak", { d: "2026-08-01", open: true, c: [ + T("Map the Onet facility floorplan", "ak", { d: "2026-06-21", s: "m" }), + T("Integrate Feetech servos for the brush arm", "sk", { d: "2026-06-24", s: "m", open: true, c: [ + T("Mount the brush assembly", "lm", { d: "2026-06-22", s: "s" }), + T("Tune servo sweep pattern", "sk", { d: "2026-06-25", s: "s" }), + ] }), + T("Safety e-stop wiring", "ly", { p: "high", d: "2026-06-16", s: "s" }), + ] }), + + T("RoboOS v2 - core platform", "ia", { p: "high", d: "2026-07-31", open: true, c: [ + T("Migrate OS to RS04 motor drivers", "sk", { p: "high", d: "2026-06-24", s: "l", open: true, c: [ + T("Port CAN driver to RS04", "sk", { d: "2026-06-22", s: "m" }), + T("Bench-test RS04 closed loop", "sk", { d: "2026-06-23", s: "m" }), + ] }), + T("Real-time locomotion controller", "ak", { d: "2026-06-29", s: "l" }), + T("Evaluate EL05 actuators", "sk", { d: "2026-06-17", s: "m", open: true, c: [ + T("Run EL05 load tests", "sk", { done: true, d: "2026-06-09", s: "s" }), + T("Compare EL05 vs RS02 efficiency", "sk", { d: "2026-06-18", s: "s" }), + ] }), + T("Nightly build + hardware-in-the-loop rig", "ia", { d: "2026-06-21", s: "m" }), + T("Assemble robot chassis v2", "ia", { d: "2026-07-06", s: "l" }), + ] }), + + T("NSI pilot - inventory scanning", "ia", { d: "2026-07-15", open: true, c: [ + T("Scoping follow-up with NSI", "fd", { d: "2026-06-19", s: "s" }), + T("Barcode scanner integration", "sk", { d: "2026-06-28", s: "m" }), + T("Aisle navigation tuning", "ak", { d: "2026-07-02", s: "m" }), + ] }), + ]; + + let seed = 7; + const rnd = () => (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648; + const keys = Object.keys(PEOPLE); + const walk = (nodes, parent, depth) => nodes.forEach((n) => { + if (depth >= 2 && parent) n.owner = rnd() < 0.7 ? parent.owner : keys[Math.floor(rnd() * keys.length)]; + walk(n.children, n, depth + 1); + }); + walk(data, null, 0); + + return data; +} diff --git a/src/lib/board-sync.js b/src/lib/board-sync.js index 84235d1..5513ee7 100644 --- a/src/lib/board-sync.js +++ b/src/lib/board-sync.js @@ -55,7 +55,7 @@ export function applyBoard(board, data, setUid) { return true; } -export function startBoardSync({ data, getUid, setUid, renderAll, onReady }) { +export function startBoardSync({ data, getUid, setUid, renderAll, onReady, fallback }) { let boardReady = false; let saveTimer = null; let saveInFlight = false; @@ -95,16 +95,18 @@ export function startBoardSync({ data, getUid, setUid, renderAll, onReady }) { } (async () => { + let loaded = false; try { const res = await fetch("/api/board"); if (!res.ok) throw new Error("load failed"); - if (applyBoard(await res.json(), data, setUid)) { - boardReady = true; - renderAll(); - } + loaded = applyBoard(await res.json(), data, setUid); + if (!loaded) console.warn("Board from server rejected, using fallback data."); } catch (e) { console.warn("Board load skipped, using built-in sample data.", e); } + if (!loaded && fallback) fallback(); + if (loaded) boardReady = true; + renderAll(); onReady(scheduleSave); })(); } diff --git a/tests/board-sync.test.js b/tests/board-sync.test.js new file mode 100644 index 0000000..4ae6e82 --- /dev/null +++ b/tests/board-sync.test.js @@ -0,0 +1,65 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { PEOPLE } from "../src/data/constants.js"; +import { createTaskFactory } from "../src/lib/tree.js"; +import { applyBoard, startBoardSync } from "../src/lib/board-sync.js"; + +describe("board-sync", () => { + const { T, setUid } = createTaskFactory(); + + beforeEach(() => { + setUid(0); + Object.assign(PEOPLE, { + fd: { name: "Florian", initials: "FD", color: "#3b6ef6", role: "Lead", al: [] }, + }); + }); + + it("applyBoard replaces in-memory data from a server payload", () => { + const data = []; + const board = { + people: { fd: { name: "Florian", initials: "FD", color: "#3b6ef6", role: "Lead", al: [] } }, + tasks: [T("Server project", "fd", { d: "2026-06-20", c: [T("Leaf", "fd", { d: "2026-06-18" })] })], + uid: 2, + }; + expect(applyBoard(board, data, setUid)).toBe(true); + expect(data).toHaveLength(1); + expect(data[0].title).toBe("Server project"); + }); + + it("startBoardSync renders once after loading from the server", async () => { + const data = []; + const renderAll = vi.fn(); + const onReady = vi.fn(); + const board = { + people: { fd: { name: "Florian", initials: "FD", color: "#3b6ef6", role: "Lead", al: [] } }, + tasks: [T("DB project", "fd", { d: "2026-06-20", c: [T("Task", "fd", { d: "2026-06-18" })] })], + uid: 2, + }; + + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ + ok: true, + json: async () => board, + })); + + startBoardSync({ data, getUid: () => 0, setUid, renderAll, onReady, fallback: vi.fn() }); + await vi.waitFor(() => expect(renderAll).toHaveBeenCalledTimes(1)); + expect(data[0]?.title).toBe("DB project"); + expect(onReady).toHaveBeenCalledOnce(); + + vi.unstubAllGlobals(); + }); + + it("startBoardSync uses fallback when the server is unavailable", async () => { + const data = []; + const renderAll = vi.fn(); + const fallback = vi.fn(() => { data.push(T("Fallback project", "fd", { d: "2026-06-20" })); }); + + vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("offline"))); + + startBoardSync({ data, getUid: () => 0, setUid, renderAll, onReady: () => {}, fallback }); + await vi.waitFor(() => expect(renderAll).toHaveBeenCalledTimes(1)); + expect(fallback).toHaveBeenCalledOnce(); + expect(data[0]?.title).toBe("Fallback project"); + + vi.unstubAllGlobals(); + }); +});