Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
81 changes: 3 additions & 78 deletions src/app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -1607,4 +1531,5 @@ startBoardSync({
setUid,
renderAll,
onReady: (save) => { requestSave = save; },
fallback: () => { DATA.splice(0, DATA.length, ...buildSampleTasks(T)); },
});
77 changes: 77 additions & 0 deletions src/data/sample-tasks.js
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 7 additions & 5 deletions src/lib/board-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
})();
}
65 changes: 65 additions & 0 deletions tests/board-sync.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading