Compare commits
No commits in common. "8facb80eab07165809d7e1f94879f0831701065d" and "33007fb5edd669a59be59a882d6320612b25fbfc" have entirely different histories.
8facb80eab
...
33007fb5ed
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* App.jsx — Saltybot Social + Telemetry + Fleet + Mission Dashboard root component.
|
* App.jsx — Saltybot Social + Telemetry + Fleet Dashboard root component.
|
||||||
*
|
*
|
||||||
* Social tabs (issue #107):
|
* Social tabs (issue #107):
|
||||||
* Status | Faces | Conversation | Personality | Navigation
|
* Status | Faces | Conversation | Personality | Navigation
|
||||||
@ -9,9 +9,6 @@
|
|||||||
*
|
*
|
||||||
* Fleet tabs (issue #139):
|
* Fleet tabs (issue #139):
|
||||||
* Fleet (self-contained via useFleet)
|
* Fleet (self-contained via useFleet)
|
||||||
*
|
|
||||||
* Mission tabs (issue #145):
|
|
||||||
* Missions (waypoint editor, route builder, geofence, schedule, execute)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
@ -35,9 +32,6 @@ import { SystemHealth } from './components/SystemHealth.jsx';
|
|||||||
// Fleet panel (issue #139)
|
// Fleet panel (issue #139)
|
||||||
import { FleetPanel } from './components/FleetPanel.jsx';
|
import { FleetPanel } from './components/FleetPanel.jsx';
|
||||||
|
|
||||||
// Mission planner (issue #145)
|
|
||||||
import { MissionPlanner } from './components/MissionPlanner.jsx';
|
|
||||||
|
|
||||||
const TAB_GROUPS = [
|
const TAB_GROUPS = [
|
||||||
{
|
{
|
||||||
label: 'SOCIAL',
|
label: 'SOCIAL',
|
||||||
@ -67,7 +61,6 @@ const TAB_GROUPS = [
|
|||||||
color: 'text-green-600',
|
color: 'text-green-600',
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 'fleet', label: 'Fleet' },
|
{ id: 'fleet', label: 'Fleet' },
|
||||||
{ id: 'missions', label: 'Missions' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -196,7 +189,6 @@ export default function App() {
|
|||||||
{activeTab === 'health' && <SystemHealth subscribe={subscribe} />}
|
{activeTab === 'health' && <SystemHealth subscribe={subscribe} />}
|
||||||
|
|
||||||
{activeTab === 'fleet' && <FleetPanel />}
|
{activeTab === 'fleet' && <FleetPanel />}
|
||||||
{activeTab === 'missions' && <MissionPlanner />}
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* ── Footer ── */}
|
{/* ── Footer ── */}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,246 +0,0 @@
|
|||||||
/**
|
|
||||||
* useMissions.js — Mission planner state management.
|
|
||||||
*
|
|
||||||
* Manages waypoints, patrol routes, geofences, templates, schedules.
|
|
||||||
* Persists to localStorage. Provides JSON export/import.
|
|
||||||
*
|
|
||||||
* Data shapes:
|
|
||||||
* Waypoint: { id, label, x, y, dwellTime } (x,y in metres, world frame)
|
|
||||||
* Route: { id, name, waypointIds[], type, robotId, color }
|
|
||||||
* type: 'loop' | 'oneshot' | 'pingpong'
|
|
||||||
* Geofence: { id, name, type, points[{x,y}], color }
|
|
||||||
* type: 'nogo' | 'allowed'
|
|
||||||
* Template: { id, name, description, waypoints[], routes[], geofences[] }
|
|
||||||
* Schedule: { id, routeId, robotId, time, days[], enabled }
|
|
||||||
* days: 0-6 (Sun-Sat), time: 'HH:MM'
|
|
||||||
* Execution: { routeId, robotId, currentIdx, state, startedAt, paused }
|
|
||||||
* state: 'idle'|'running'|'paused'|'done'|'aborted'
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'saltybot_missions_v1';
|
|
||||||
const EXEC_KEY = 'saltybot_executions_v1';
|
|
||||||
|
|
||||||
const ROUTE_COLORS = ['#06b6d4','#f97316','#22c55e','#a855f7','#f59e0b','#ec4899'];
|
|
||||||
|
|
||||||
function genId() {
|
|
||||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadState() {
|
|
||||||
try {
|
|
||||||
const s = JSON.parse(localStorage.getItem(STORAGE_KEY));
|
|
||||||
if (s && typeof s === 'object') return s;
|
|
||||||
} catch {}
|
|
||||||
return { waypoints: [], routes: [], geofences: [], templates: [], schedules: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveState(state) {
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMissions() {
|
|
||||||
const [waypoints, setWaypoints] = useState(() => loadState().waypoints);
|
|
||||||
const [routes, setRoutes] = useState(() => loadState().routes);
|
|
||||||
const [geofences, setGeofences] = useState(() => loadState().geofences);
|
|
||||||
const [templates, setTemplates] = useState(() => loadState().templates);
|
|
||||||
const [schedules, setSchedules] = useState(() => loadState().schedules);
|
|
||||||
const [executions, setExecutions] = useState({}); // { [routeId]: Execution }
|
|
||||||
|
|
||||||
// Persist on every change
|
|
||||||
useEffect(() => {
|
|
||||||
saveState({ waypoints, routes, geofences, templates, schedules });
|
|
||||||
}, [waypoints, routes, geofences, templates, schedules]);
|
|
||||||
|
|
||||||
// ── Waypoints ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const addWaypoint = useCallback((x, y, label) => {
|
|
||||||
const wp = { id: genId(), label: label ?? `WP${Date.now().toString(36).slice(-4).toUpperCase()}`, x, y, dwellTime: 0 };
|
|
||||||
setWaypoints(w => [...w, wp]);
|
|
||||||
return wp.id;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateWaypoint = useCallback((id, patch) => {
|
|
||||||
setWaypoints(w => w.map(wp => wp.id === id ? { ...wp, ...patch } : wp));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const removeWaypoint = useCallback((id) => {
|
|
||||||
setWaypoints(w => w.filter(wp => wp.id !== id));
|
|
||||||
// Remove from any routes
|
|
||||||
setRoutes(r => r.map(rt => ({ ...rt, waypointIds: rt.waypointIds.filter(wid => wid !== id) })));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const reorderWaypoints = useCallback((fromIdx, toIdx) => {
|
|
||||||
setWaypoints(w => {
|
|
||||||
const arr = [...w];
|
|
||||||
const [item] = arr.splice(fromIdx, 1);
|
|
||||||
arr.splice(toIdx, 0, item);
|
|
||||||
return arr;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Routes ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const addRoute = useCallback((name, waypointIds = [], type = 'loop') => {
|
|
||||||
const id = genId();
|
|
||||||
const color = ROUTE_COLORS[Math.floor(Math.random() * ROUTE_COLORS.length)];
|
|
||||||
setRoutes(r => [...r, { id, name, waypointIds, type, robotId: null, color }]);
|
|
||||||
return id;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateRoute = useCallback((id, patch) => {
|
|
||||||
setRoutes(r => r.map(rt => rt.id === id ? { ...rt, ...patch } : rt));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const removeRoute = useCallback((id) => {
|
|
||||||
setRoutes(r => r.filter(rt => rt.id !== id));
|
|
||||||
setSchedules(s => s.filter(sc => sc.routeId !== id));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const addWaypointToRoute = useCallback((routeId, waypointId) => {
|
|
||||||
setRoutes(r => r.map(rt =>
|
|
||||||
rt.id === routeId
|
|
||||||
? { ...rt, waypointIds: [...rt.waypointIds, waypointId] }
|
|
||||||
: rt
|
|
||||||
));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const removeWaypointFromRoute = useCallback((routeId, idx) => {
|
|
||||||
setRoutes(r => r.map(rt =>
|
|
||||||
rt.id === routeId
|
|
||||||
? { ...rt, waypointIds: rt.waypointIds.filter((_, i) => i !== idx) }
|
|
||||||
: rt
|
|
||||||
));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Geofences ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const addGeofence = useCallback((name, type, points) => {
|
|
||||||
const id = genId();
|
|
||||||
const color = type === 'nogo' ? '#ef444488' : '#22c55e88';
|
|
||||||
setGeofences(g => [...g, { id, name, type, points, color }]);
|
|
||||||
return id;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateGeofence = useCallback((id, patch) => {
|
|
||||||
setGeofences(g => g.map(gf => gf.id === id ? { ...gf, ...patch } : gf));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const removeGeofence = useCallback((id) => {
|
|
||||||
setGeofences(g => g.filter(gf => gf.id !== id));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Templates ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const saveTemplate = useCallback((name, description = '') => {
|
|
||||||
const id = genId();
|
|
||||||
setTemplates(t => [
|
|
||||||
...t.filter(x => x.name !== name),
|
|
||||||
{ id, name, description, waypoints, routes, geofences },
|
|
||||||
]);
|
|
||||||
return id;
|
|
||||||
}, [waypoints, routes, geofences]);
|
|
||||||
|
|
||||||
const loadTemplate = useCallback((id) => {
|
|
||||||
setTemplates(t => {
|
|
||||||
const tmpl = t.find(x => x.id === id);
|
|
||||||
if (!tmpl) return t;
|
|
||||||
setWaypoints(tmpl.waypoints);
|
|
||||||
setRoutes(tmpl.routes);
|
|
||||||
setGeofences(tmpl.geofences);
|
|
||||||
return t;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const removeTemplate = useCallback((id) => {
|
|
||||||
setTemplates(t => t.filter(x => x.id !== id));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Schedules ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const addSchedule = useCallback((routeId, robotId, time, days = [1,2,3,4,5]) => {
|
|
||||||
const id = genId();
|
|
||||||
setSchedules(s => [...s, { id, routeId, robotId, time, days, enabled: true }]);
|
|
||||||
return id;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateSchedule = useCallback((id, patch) => {
|
|
||||||
setSchedules(s => s.map(sc => sc.id === id ? { ...sc, ...patch } : sc));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const removeSchedule = useCallback((id) => {
|
|
||||||
setSchedules(s => s.filter(sc => sc.id !== id));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Executions ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const startExecution = useCallback((routeId, robotId) => {
|
|
||||||
setExecutions(e => ({
|
|
||||||
...e,
|
|
||||||
[routeId]: { routeId, robotId, currentIdx: 0, state: 'running', startedAt: Date.now(), paused: false },
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const pauseExecution = useCallback((routeId) => {
|
|
||||||
setExecutions(e => ({
|
|
||||||
...e,
|
|
||||||
[routeId]: { ...e[routeId], state: 'paused', paused: true },
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const resumeExecution = useCallback((routeId) => {
|
|
||||||
setExecutions(e => ({
|
|
||||||
...e,
|
|
||||||
[routeId]: { ...e[routeId], state: 'running', paused: false },
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const abortExecution = useCallback((routeId) => {
|
|
||||||
setExecutions(e => ({
|
|
||||||
...e,
|
|
||||||
[routeId]: { ...e[routeId], state: 'aborted' },
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const advanceExecution = useCallback((routeId, idx) => {
|
|
||||||
setExecutions(e => ({
|
|
||||||
...e,
|
|
||||||
[routeId]: { ...e[routeId], currentIdx: idx },
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── JSON export / import ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const exportJSON = useCallback(() => {
|
|
||||||
return JSON.stringify({ waypoints, routes, geofences, templates, schedules }, null, 2);
|
|
||||||
}, [waypoints, routes, geofences, templates, schedules]);
|
|
||||||
|
|
||||||
const importJSON = useCallback((json) => {
|
|
||||||
const data = JSON.parse(json);
|
|
||||||
if (data.waypoints) setWaypoints(data.waypoints);
|
|
||||||
if (data.routes) setRoutes(data.routes);
|
|
||||||
if (data.geofences) setGeofences(data.geofences);
|
|
||||||
if (data.templates) setTemplates(data.templates);
|
|
||||||
if (data.schedules) setSchedules(data.schedules);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
waypoints, routes, geofences, templates, schedules, executions,
|
|
||||||
// Waypoints
|
|
||||||
addWaypoint, updateWaypoint, removeWaypoint, reorderWaypoints,
|
|
||||||
// Routes
|
|
||||||
addRoute, updateRoute, removeRoute, addWaypointToRoute, removeWaypointFromRoute,
|
|
||||||
// Geofences
|
|
||||||
addGeofence, updateGeofence, removeGeofence,
|
|
||||||
// Templates
|
|
||||||
saveTemplate, loadTemplate, removeTemplate,
|
|
||||||
// Schedules
|
|
||||||
addSchedule, updateSchedule, removeSchedule,
|
|
||||||
// Executions
|
|
||||||
startExecution, pauseExecution, resumeExecution, abortExecution, advanceExecution,
|
|
||||||
// I/O
|
|
||||||
exportJSON, importJSON,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user