361 lines
46 KiB
JavaScript
361 lines
46 KiB
JavaScript
/* Atlas Mission Control POC — all telemetry and operators are fictional. */
|
||
const groups = [
|
||
{ id: 'alpha', name: 'ALPHA', count: 12, task: 'Area Search', operator: 'Alex', progress: 82, health: 'nominal', color: 'cyan', icon: 'A', current: 'Search area north', next: 'Return', lease: 'Alex' },
|
||
{ id: 'bravo', name: 'BRAVO', count: 10, task: 'Route Recon', operator: 'Sara', progress: 71, health: 'nominal', color: 'blue', icon: 'B', current: 'Coastal route', next: 'Survey pass', lease: 'Sara' },
|
||
{ id: 'charlie', name: 'CHARLIE', count: 8, task: 'Relay', operator: 'David', progress: 64, health: 'nominal', color: 'violet', icon: 'C', current: 'Relay station', next: 'Hold', lease: 'David' },
|
||
{ id: 'delta', name: 'DELTA', count: 12, task: 'Reserve', operator: 'Maria', progress: 35, health: 'attention', color: 'amber', icon: 'D', current: 'Ready reserve', next: 'Awaiting task', lease: 'Maria' },
|
||
{ id: 'reserve', name: 'RESERVE', count: 8, task: 'Available', operator: 'Ken', progress: 100, health: 'nominal', color: 'violet', icon: 'R', current: 'Base service', next: 'Available', lease: 'Ken' }
|
||
];
|
||
|
||
const operators = [
|
||
{ id: 'alex', name: 'Alex', role: 'Commanding Alpha', color: 'cyan', activity: 'selected Alpha' },
|
||
{ id: 'sara', name: 'Sara', role: 'Commanding Bravo', color: 'green', activity: 'updated Bravo search area' },
|
||
{ id: 'david', name: 'David', role: 'Planning', color: 'amber', activity: 'drafted Task-144' },
|
||
{ id: 'maria', name: 'Maria', role: 'Monitoring', color: 'violet', activity: 'added monitoring note' },
|
||
{ id: 'ken', name: 'Ken', role: 'Engineering', color: 'yellow', activity: 'updated comms plan' }
|
||
];
|
||
|
||
const alphaVehicles = [
|
||
['UAV-01', 18, 32, 92], ['UAV-02', 25, 39, 86], ['UAV-03', 33, 28, 43],
|
||
['UAV-04', 39, 44, 80], ['UAV-05', 45, 35, 74], ['UAV-06', 53, 45, 88],
|
||
['UAV-07', 60, 30, 74], ['UAV-08', 66, 42, 83], ['UAV-09', 72, 35, 79],
|
||
['UAV-10', 76, 51, 90], ['UAV-11', 55, 57, 82], ['UAV-12', 34, 57, 76]
|
||
].map((item, index) => ({ id: item[0], x: item[1], y: item[2], energy: item[3], heading: 20 + index * 16, status: item[0] === 'UAV-03' ? 'critical' : 'nominal' }));
|
||
|
||
const availableVehicles = [
|
||
['ALP-01', 18, 50, 92], ['ALP-02', 31, 47, 71], ['ALP-03', 17, 35, 82], ['ALP-04', 72, 42, 65],
|
||
['ALP-05', 71, 55, 80], ['ALP-06', 64, 69, 74], ['ALP-07', 35, 18, 78], ['ALP-08', 38, 72, 61]
|
||
].map(item => ({ id: item[0], x: item[1], y: item[2], energy: item[3], heading: 30, status: 'nominal' }));
|
||
|
||
const requestedView = new URLSearchParams(window.location.search).get('view');
|
||
const validViews = ['overview', 'zoom', 'selection', 'collaboration', 'recovery', 'queue', 'system'];
|
||
|
||
let state = {
|
||
view: validViews.includes(requestedView) ? requestedView : 'overview',
|
||
selectedGroup: 'alpha',
|
||
selectedVehicle: 'UAV-07',
|
||
selectedCandidate: 'UAV-19',
|
||
modal: null,
|
||
toast: '',
|
||
layersOpen: false,
|
||
reassigned: false,
|
||
queueDraft: false,
|
||
queue: [
|
||
{ title: 'Transit', detail: 'to Northern Sector', duration: '8 min', status: 'completed' },
|
||
{ title: 'Search Area A', detail: 'coverage pattern', duration: '18 min', status: 'current' },
|
||
{ title: 'Loiter', detail: '6 min', duration: '6 min', status: 'queued' },
|
||
{ title: 'Return', detail: 'staging point', duration: '12 min', status: 'queued' },
|
||
{ title: 'Recharge', detail: 'base service', duration: '35 min', status: 'queued' },
|
||
{ title: 'Search Area B', detail: 'coverage pattern', duration: '20 min', status: 'queued' }
|
||
]
|
||
};
|
||
|
||
const icons = {
|
||
menu: '<path d="M4 7h16M4 12h16M4 17h16"/>',
|
||
map: '<path d="m4 6 5-2 6 2 5-2v14l-5 2-6-2-5 2V6Z"/><path d="M9 4v14m6-12v14"/>',
|
||
layers: '<path d="m12 3 8 4.5-8 4.5-8-4.5L12 3Z"/><path d="m4 12 8 4.5 8-4.5M4 16.5l8 4.5 8-4.5"/>',
|
||
plane: '<path d="m12 2 2.4 7.1 5.6 2.1v1.6l-6.2.5-1.1 7-1.5.1-1.2-7-6.2-.5v-1.6l5.6-2.1L12 2Z"/>',
|
||
people: '<circle cx="9" cy="8" r="3"/><circle cx="16.5" cy="9" r="2.5"/><path d="M3.5 20c.5-3.5 2.4-5.3 5.5-5.3s5 1.8 5.5 5.3M14.2 15.2c3.1-.6 5.5 1.1 6.1 4.8"/>',
|
||
task: '<rect x="5" y="4" width="14" height="17" rx="1"/><path d="M8 4.5h8v3H8zM8 12h8M8 16h5"/>',
|
||
alert: '<path d="M12 3 21 20H3L12 3Z"/><path d="M12 9v5m0 3h.01"/>',
|
||
shield: '<path d="M12 3 20 6v5c0 5-3.2 8.2-8 10-4.8-1.8-8-5-8-10V6l8-3Z"/><path d="m8.5 12 2.2 2.2 4.8-5"/>',
|
||
chevron: '<path d="m8 10 4 4 4-4"/>',
|
||
plus: '<path d="M12 5v14M5 12h14"/>',
|
||
close: '<path d="m6 6 12 12M18 6 6 18"/>',
|
||
target: '<circle cx="12" cy="12" r="7"/><circle cx="12" cy="12" r="2"/><path d="M12 2v3m0 14v3M2 12h3m14 0h3"/>',
|
||
transfer: '<path d="M4 8h12l-3-3m3 3-3 3M20 16H8l3 3m-3-3 3-3"/>',
|
||
hold: '<path d="M8 5v14M16 5v14"/>',
|
||
move: '<path d="m4 11 16-7-7 16-2-7-7-2Z"/>',
|
||
clock: '<circle cx="12" cy="12" r="8"/><path d="M12 7v5l3 2"/>',
|
||
check: '<path d="m5 12 4.2 4.2L19 6.7"/>',
|
||
search: '<circle cx="10.5" cy="10.5" r="5.5"/><path d="m15 15 5 5"/>',
|
||
info: '<circle cx="12" cy="12" r="8"/><path d="M12 11v5m0-8h.01"/>',
|
||
bolt: '<path d="m13 2-8 12h6l-1 8 8-12h-6l1-8Z"/>',
|
||
route: '<path d="M5 18c3-8 6-12 14-12"/><circle cx="5" cy="18" r="2"/><path d="m16 4 3 2-3 2"/>',
|
||
settings: '<circle cx="12" cy="12" r="3"/><path d="M19 12a7.2 7.2 0 0 0-.1-1.2l2-1.5-2-3.4-2.4 1a7 7 0 0 0-2-1.1L14.2 3h-4.1l-.4 2.8a7 7 0 0 0-2 1.1l-2.4-1-2 3.4 2 1.5a7.2 7.2 0 0 0 0 2.4l-2 1.5 2 3.4 2.4-1a7 7 0 0 0 2 1.1l.4 2.8h4.1l.4-2.8a7 7 0 0 0 2-1.1l2.4 1 2-3.4-2-1.5c.1-.4.1-.8.1-1.2Z"/>'
|
||
};
|
||
|
||
function icon(name, extra = '') {
|
||
return '<svg class="icon ' + extra + '" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + (icons[name] || icons.info) + '</svg>';
|
||
}
|
||
|
||
function groupById(id) {
|
||
return groups.find(group => group.id === id) || groups[0];
|
||
}
|
||
|
||
function statusTag(status, label) {
|
||
const text = label || ({ nominal: 'Nominal', attention: 'Attention', critical: 'Critical', planned: 'Planned' }[status] || status);
|
||
return '<span class="status ' + status + '"><span class="status-dot"></span>' + text + '</span>';
|
||
}
|
||
|
||
function button(label, action, options = {}) {
|
||
const cls = 'button ' + (options.kind || 'secondary') + (options.iconOnly ? ' icon-button' : '') + (options.className ? ' ' + options.className : '');
|
||
const attrs = (options.disabled ? ' disabled' : '') + (options.label ? ' aria-label="' + options.label + '"' : '') + (options.value ? ' data-value="' + options.value + '"' : '');
|
||
return '<button class="' + cls + '" data-action="' + action + '"' + attrs + '>' + (options.icon ? icon(options.icon) : '') + (options.iconOnly ? '' : '<span>' + label + '</span>') + '</button>';
|
||
}
|
||
|
||
function navItem(id, label, iconName) {
|
||
return '<button class="nav-item ' + (state.view === id ? 'is-active' : '') + '" data-action="set-view" data-value="' + id + '">' + icon(iconName) + '<span>' + label + '</span></button>';
|
||
}
|
||
|
||
function topBar() {
|
||
return '<header class="topbar">' +
|
||
'<div class="brand">' + button('', 'toggle-menu', { icon: 'menu', iconOnly: true, label: 'Open navigation' }) +
|
||
'<div class="brand-mark">A</div><span>ATLAS</span><span class="brand-divider"></span><span class="mission-name">Mission Alpha-17</span></div>' +
|
||
'<nav class="view-nav" aria-label="POC screens">' +
|
||
navItem('overview', 'Mission', 'map') + navItem('zoom', 'Zoom', 'target') + navItem('selection', 'Selection', 'search') +
|
||
navItem('collaboration', 'Collaboration', 'people') + navItem('recovery', 'Recovery', 'alert') + navItem('queue', 'Intent queue', 'route') + navItem('system', 'System', 'settings') +
|
||
'</nav>' +
|
||
'<div class="top-metrics">' +
|
||
'<div><small>Fleet</small><strong class="green">48 / 50 active</strong></div><div><small>Tasks</small><strong>7 active</strong></div><div><small>Alerts</small><strong class="amber">3</strong></div>' +
|
||
'<div><small>Operators</small><strong class="green">5 online</strong></div><div class="hide-at-wide"><small>Network</small><strong class="green">Healthy</strong></div>' +
|
||
'<div class="operator-avatar"><span>AX</span><div><strong>Alex</strong><small>Mission operator</small></div></div>' +
|
||
'</div></header>';
|
||
}
|
||
|
||
function mapBase() {
|
||
return '<svg class="map-art" viewBox="0 0 1000 700" preserveAspectRatio="none" aria-hidden="true">' +
|
||
'<defs><filter id="soft"><feGaussianBlur stdDeviation="9"/></filter><pattern id="grid" width="64" height="64" patternUnits="userSpaceOnUse"><path d="M0 0H64V64" fill="none" stroke="#6993b0" stroke-opacity=".11"/></pattern></defs>' +
|
||
'<rect width="1000" height="700" fill="url(#grid)"/><path class="land" d="M0 0H352C397 63 392 126 447 179c48 47 30 105 87 162 35 35 30 116-11 151-47 40-126 53-169 109-42 55-8 102-54 99H0Z"/>' +
|
||
'<path class="coast" d="M352 0c45 63 40 126 95 179 48 47 30 105 87 162 35 35 30 116-11 151-47 40-126 53-169 109-42 55-8 102-54 99"/><path class="river" d="M55 12c92 74 95 150 153 213 48 52 81 73 87 143 5 58-31 87-25 155 4 42 34 85 44 135"/>' +
|
||
'<path class="road" d="M5 520C111 460 148 440 216 376s121-73 177-147c54-70 87-54 158-73 92-24 115-112 233-119"/><path class="road thin" d="M92 52c24 85 85 117 148 161 49 34 45 85 96 121M64 612c84-14 136-81 223-84 97-3 117-68 194-75"/>' +
|
||
'<path class="hill" d="M62 104c56-48 121-47 164-16m-143 55c59-49 115-37 156-14M49 587c63-42 116-33 159-7"/><path class="hill" d="M120 245c34-26 71-31 108-20m-67 76c41-34 93-25 131-1"/>' +
|
||
'<g class="town"><circle cx="208" cy="381" r="20"/><circle cx="208" cy="381" r="7"/><circle cx="114" cy="526" r="13"/><circle cx="114" cy="526" r="4"/></g>' +
|
||
'</svg>' +
|
||
'<span class="map-label north-point" style="left:14%;top:20%">North Point</span><span class="map-label cliffside" style="left:58%;top:26%">Cliffside</span><span class="map-label seabridge" style="left:42%;top:68%">Seabridge</span><span class="map-label west-cove" style="left:16%;top:74%">West Cove</span>' +
|
||
'<div class="scale"><i></i><span>2 km</span></div>';
|
||
}
|
||
|
||
function mapControls() {
|
||
return '<div class="map-controls">' + button('', 'toggle-layers', { icon: 'layers', iconOnly: true, label: 'Toggle map layers' }) + button('', 'map-zoom-in', { icon: 'plus', iconOnly: true, label: 'Zoom map in' }) + button('−', 'map-zoom-out', { iconOnly: true, label: 'Zoom map out' }) + '</div>' +
|
||
(state.layersOpen ? '<div class="layers-popover"><strong>Map layers</strong><label><input type="checkbox" checked> Search areas</label><label><input type="checkbox" checked> Planned routes</label><label><input type="checkbox" checked> Constraints</label><label><input type="checkbox"> Map labels</label></div>' : '');
|
||
}
|
||
|
||
function taskArea(label = 'ALPHA\nSEARCH AREA', classes = '') {
|
||
return '<div class="task-area ' + classes + '"><div class="task-shape"></div><span>' + label.replace('\n', '<br>') + '</span></div>';
|
||
}
|
||
|
||
function groupMarker(group, x, y, active = false) {
|
||
return '<button class="group-marker ' + group.color + (active ? ' is-selected' : '') + '" style="left:' + x + '%;top:' + y + '%" data-action="select-group" data-value="' + group.id + '">' +
|
||
'<span class="marker-dot"><b>' + group.icon + '</b></span><span class="marker-card"><strong>' + group.name + '</strong><small>' + group.count + ' UAV · ' + group.task + ' · ' + group.progress + '%</small></span></button>';
|
||
}
|
||
|
||
function vehicleMarker(vehicle, selected = false, label = false) {
|
||
const critical = vehicle.status === 'critical';
|
||
return '<button class="vehicle-marker ' + (critical ? 'critical' : '') + (selected ? ' selected' : '') + '" style="left:' + vehicle.x + '%;top:' + vehicle.y + '%;--heading:' + vehicle.heading + 'deg" data-action="select-vehicle" data-value="' + vehicle.id + '" aria-label="' + vehicle.id + ', ' + vehicle.energy + '% energy">' +
|
||
icon('plane') + (label ? '<span>' + vehicle.id + '</span>' : '') + '</button>';
|
||
}
|
||
|
||
function routeLine(className = '') {
|
||
return '<svg class="route-line ' + className + '" viewBox="0 0 1000 700" preserveAspectRatio="none" aria-hidden="true"><path d="M180 535C195 463 238 445 282 384S367 280 433 256c76-27 100-91 179-102 82-11 124-55 217-37" fill="none"/><path class="planned-path" d="M610 142c86 55 127 80 166 168 27 59 72 67 151 104" fill="none"/></svg>';
|
||
}
|
||
|
||
function overviewMap() {
|
||
const g = groupById(state.selectedGroup);
|
||
return '<div class="map-canvas overview-map">' + mapBase() + mapControls() + routeLine() + taskArea(g.name + '\nAREA ' + (g.task === 'Area Search' ? 'SEARCH' : g.task.toUpperCase()), 'alpha-area') +
|
||
'<div class="coverage-ring"></div>' +
|
||
alphaVehicles.map(vehicle => vehicleMarker(vehicle, false, vehicle.id === 'UAV-03')).join('') +
|
||
groupMarker(groups[1], 65, 27) + groupMarker(groups[2], 69, 50) + groupMarker(groups[3], 73, 68) + groupMarker(groups[4], 57, 82) +
|
||
'<div class="geofence red-zone"></div>' +
|
||
'<div class="map-mini-exceptions"><strong>Exceptions (3)</strong><p>' + icon('alert') + ' UAV-27 · Navigation degraded</p><p>' + icon('info') + ' Alpha task behind schedule</p><a href="#" data-action="set-view" data-value="recovery">Review recovery</a></div>' +
|
||
'</div>';
|
||
}
|
||
|
||
function groupRail(compact = false) {
|
||
return '<aside class="group-rail ' + (compact ? 'compact' : '') + '"><div class="rail-heading"><span>Groups</span>' + button('', 'new-group', { icon: 'plus', iconOnly: true, label: 'Create a new group' }) + '</div>' +
|
||
'<div class="group-list">' + groups.map(group => {
|
||
const selected = state.selectedGroup === group.id;
|
||
return '<button class="group-card ' + (selected ? 'selected' : '') + '" data-action="select-group" data-value="' + group.id + '">' +
|
||
'<span class="group-badge ' + group.color + '">' + group.icon + '</span><span class="group-card-main"><strong>' + group.name + '</strong><small>' + group.count + ' UAV</small><small>' + group.task + '</small><span class="operator-line">' + group.operator + '</span></span>' +
|
||
'<span class="group-card-side">' + statusTag(group.health, '') + '<b>' + group.progress + '%</b><i class="mini-grid">' + Array(9).fill('<em></em>').join('') + '</i></span></button>';
|
||
}).join('') + '</div>' + button('New group', 'new-group', { icon: 'plus', className: 'full-width' }) + '</aside>';
|
||
}
|
||
|
||
function attentionRail() {
|
||
return '<aside class="attention-rail"><div class="rail-heading"><span>Attention</span>' + button('', 'view-alerts', { icon: 'alert', iconOnly: true, label: 'View all alerts' }) + '</div>' +
|
||
'<button class="attention-card warning" data-action="set-view" data-value="recovery">' + icon('alert') + '<span><strong>UAV-27</strong><small>Navigation degraded</small><small>Since 01:32:11</small></span><em>ALPHA</em></button>' +
|
||
'<button class="attention-card amber" data-action="set-view" data-value="recovery">' + icon('clock') + '<span><strong>Alpha task behind schedule</strong><small>ETA impact +00:08:45</small></span><em>ALPHA</em></button>' +
|
||
'<button class="attention-card amber" data-action="set-view" data-value="selection">' + icon('alert') + '<span><strong>Bravo approaching constraint</strong><small>R-210B coastal limit</small></span><em>BRAVO</em></button>' +
|
||
'<div class="fleet-nominal">' + icon('check') + '<div><b>47</b><span>vehicles nominal</span><small>No action required</small></div></div>' +
|
||
'</aside>';
|
||
}
|
||
|
||
function contextBar() {
|
||
const group = groupById(state.selectedGroup);
|
||
return '<section class="context-bar"><div class="context-summary"><span class="group-badge ' + group.color + '">' + group.icon + '</span><div><h2>' + group.name + '</h2><small>' + group.count + ' vehicles</small></div><dl><div><dt>Task</dt><dd>' + group.task + '</dd></div><div><dt>Progress</dt><dd>' + group.progress + '% complete <i class="progress"><b style="width:' + group.progress + '%"></b></i></dd></div><div><dt>Operator</dt><dd>' + group.operator + '</dd></div><div><dt>Status</dt><dd>' + statusTag(group.health) + '</dd></div><div class="wide-only"><dt>Current</dt><dd>' + group.current + '</dd></div><div class="wide-only"><dt>Next</dt><dd>' + group.next + '</dd></div></dl></div>' +
|
||
'<div class="context-actions">' + button('Task', 'open-preview', { icon: 'task' }) + button('Move', 'move-group', { icon: 'move' }) + button('Hold', 'hold-group', { icon: 'hold' }) + button('Group', 'group-vehicles', { icon: 'people' }) + button('Split', 'split-group', { icon: 'route' }) + button('Transfer', 'open-transfer', { icon: 'transfer' }) + button('Details', 'open-vehicle-detail', { icon: 'info' }) + '</div></section>';
|
||
}
|
||
|
||
function overviewView() {
|
||
return '<main class="workspace overview-workspace">' + groupRail() + '<section class="map-stage">' + overviewMap() + '</section>' + attentionRail() + contextBar() + '</main>';
|
||
}
|
||
|
||
function zoomSmallMap(kind) {
|
||
if (kind === 'strategic') {
|
||
return '<div class="zoom-map strategic">' + mapBase() + taskArea('TASK 143', 'small-task violet') + groupMarker(groups[0], 35, 33, true) + groupMarker(groups[1], 20, 57) + groupMarker(groups[2], 38, 75) + groupMarker(groups[3], 68, 61) + '<div class="zoom-fleet">' + groups.map(g => '<span><b>' + g.name + '</b><i style="width:' + g.progress + '%"></i></span>').join('') + '</div></div>';
|
||
}
|
||
if (kind === 'operational') {
|
||
return '<div class="zoom-map operational">' + mapBase() + taskArea('AREA SEARCH\n82%', 'wide alpha-area') + routeLine() + alphaVehicles.map(v => vehicleMarker(v)).join('') + '<div class="map-kpi"><small>Area search</small><b>82%</b><span class="ring"></span><em>Alex</em></div></div>';
|
||
}
|
||
return '<div class="zoom-map vehicle">' + mapBase() + taskArea('', 'wide alpha-area muted') + routeLine() + alphaVehicles.map(v => vehicleMarker(v, v.id === 'UAV-07')).join('') + '<aside class="vehicle-inspector"><div><strong>UAV-07</strong>' + button('', 'close-inspector', { icon: 'close', iconOnly: true, label: 'Close vehicle inspector' }) + '</div><small>ALPHA GROUP</small><dl><div><dt>Position</dt><dd>34.72245° N<br>18.28512° W</dd></div><div><dt>Heading</dt><dd>128°</dd></div><div><dt>Energy</dt><dd class="green">74%</dd></div><div><dt>Navigation</dt><dd class="green">Nominal</dd></div><div><dt>Communication</dt><dd class="green">Good</dd></div><div><dt>Payload</dt><dd>Imaging</dd></div></dl><hr><p><b>Current task</b>Search Area North</p><p><b>Next action</b>Sweep 4</p></aside></div>';
|
||
}
|
||
|
||
function zoomFooter(kind) {
|
||
const content = kind === 'strategic' ? '<h3>Groups and task intent</h3><p>High-level view of mission groups and task areas.</p>' : kind === 'operational' ? '<h3>Group composition and coverage</h3><p>Coordinated group view with coverage progress.</p>' : '<h3>Vehicle details on request</h3><p>Progressive disclosure for a selected vehicle.</p>';
|
||
return '<footer class="zoom-footer">' + content + '</footer>';
|
||
}
|
||
|
||
function zoomView() {
|
||
return '<main class="zoom-view"><header class="view-intro"><div><h1>Semantic zoom <span>— shared multi-UAV mission planner</span></h1><p>Seamless transition from strategic intent to operational control to individual vehicle detail.</p></div><div class="legend">' + statusTag('blue', 'Active') + statusTag('cyan', 'Selected') + statusTag('nominal', 'Nominal') + statusTag('attention', 'Attention') + statusTag('planned', 'Planned') + '</div></header><section class="zoom-grid">' +
|
||
'<article class="zoom-panel"><header><span class="letter">A</span><h2>Strategic</h2>' + button('', 'zoom-strategic', { icon: 'target', iconOnly: true, label: 'View strategic detail' }) + '</header>' + zoomSmallMap('strategic') + zoomFooter('strategic') + '</article>' +
|
||
'<article class="zoom-panel"><header><span class="letter">B</span><h2>Operational — Alpha</h2>' + button('', 'zoom-operational', { icon: 'target', iconOnly: true, label: 'View operational detail' }) + '</header>' + zoomSmallMap('operational') + zoomFooter('operational') + '</article>' +
|
||
'<article class="zoom-panel"><header><span class="letter">C</span><h2>Vehicle — UAV-07</h2>' + button('', 'zoom-vehicle', { icon: 'target', iconOnly: true, label: 'View vehicle detail' }) + '</header>' + zoomSmallMap('vehicle') + zoomFooter('vehicle') + '</article>' +
|
||
'</section></main>';
|
||
}
|
||
|
||
function filterChip(label, value) {
|
||
return '<div class="filter-chip"><span>' + label + '</span><b>' + value + '</b><button data-action="remove-filter" aria-label="Remove ' + label + ' filter">' + icon('close') + '</button></div>';
|
||
}
|
||
|
||
function selectionMap() {
|
||
return '<div class="map-canvas selection-map">' + mapBase() + '<div class="selection-task violet">' + taskArea('TASK-143\nAREA SEARCH', 'violet') + '<span class="task-pin"></span></div>' + routeLine('selection-routes') + availableVehicles.map(v => vehicleMarker(v, true, true)).join('') + '<div class="floating-task-card"><h3>TASK-143 <span>Area search</span></h3><dl><div><dt>Priority</dt><dd class="amber">High</dd></div><div><dt>Assigned</dt><dd class="blue">Alpha / 4</dd></div><div><dt>Area</dt><dd>Northern sector</dd></div><div><dt>Progress</dt><dd>63% <i class="progress"><b style="width:63%"></b></i></dd></div><div><dt>Required</dt><dd>4 UAV</dd></div><div><dt>Health</dt><dd class="green">Green</dd></div></dl><footer>' + icon('clock') + ' Estimated completion <b>18 min</b></footer></div>' + mapControls() + '</div>';
|
||
}
|
||
|
||
function smartSelectionView() {
|
||
return '<main class="selection-workspace"><aside class="smart-panel"><header><h1>Smart selection</h1>' + button('', 'show-filter-help', { icon: 'info', iconOnly: true, label: 'Selection help' }) + '</header><label class="form-label">Natural language query</label><div class="natural-query">' + icon('search') + '<p>Available ISR aircraft within 20 km of Task Bravo with more than 60% energy.</p></div><div class="filter-heading"><label class="form-label">Structured filters</label><button data-action="clear-filters">Clear all</button></div><div class="filter-stack">' + filterChip('Type', 'ISR') + filterChip('Availability', 'Available') + filterChip('Distance', '< 20 km') + filterChip('Energy', '> 60%') + filterChip('Task', 'None') + '</div><div class="selection-result"><small>Results</small><strong>8 <span>vehicles selected</span></strong><p>Filter distribution</p><dl><div><dt>Type (ISR)</dt><dd><i style="width:72%"></i>32</dd></div><div><dt>Availability</dt><dd><i style="width:42%"></i>12</dd></div><div><dt>Distance</dt><dd><i style="width:32%"></i>9</dd></div><div><dt>Energy</dt><dd><i style="width:28%"></i>8</dd></div></dl></div>' + button('Assign task', 'open-preview', { icon: 'task', kind: 'primary', className: 'full-width' }) + '<small class="updated">Last updated: 14:32:10 UTC</small></aside><section class="selection-map-stage">' + selectionMap() + '</section>' + assignPanel() + '</main>';
|
||
}
|
||
|
||
function validationRow(label, value, stateName = 'ok') {
|
||
return '<div class="validation-row">' + icon(stateName === 'warning' ? 'alert' : stateName === 'info' ? 'info' : 'check') + '<span>' + label + '</span><b class="' + (stateName === 'warning' ? 'amber' : '') + '">' + value + '</b>' + (stateName === 'ok' ? '<i class="round-check">' + icon('check') + '</i>' : '') + '</div>';
|
||
}
|
||
|
||
function assignPanel() {
|
||
return '<aside class="assign-panel"><header><div><h1>Assign task</h1><p>6 vehicles selected</p></div>' + button('', 'close-assign', { icon: 'close', iconOnly: true, label: 'Close assignment panel' }) + '</header><dl class="task-meta"><div><dt>Task</dt><dd class="violet">TASK-143 / AREA SEARCH</dd></div><div><dt>Area</dt><dd class="violet">Northern sector</dd></div></dl><h2>Validation summary</h2><div class="validation-list">' + validationRow('Estimated completion', '18 min') + validationRow('Energy reserve', 'All above minimum') + validationRow('Route conflicts', 'None detected') + validationRow('Constraint conflicts', 'None detected') + validationRow('Vehicle compatibility', '6 / 6 compatible') + validationRow('Communication link', 'All links good') + validationRow('Assignment considerations', '1 aircraft may require reassignment.', 'warning') + '</div><div class="info-callout">' + icon('info') + '<p>Review the preview to confirm routing, timing, and resource allocation before assigning.</p></div><footer>' + button('Cancel', 'cancel-modal') + button('Preview', 'open-preview', { kind: 'secondary' }) + button('Confirm', 'confirm-assignment', { icon: 'check', kind: 'primary' }) + '</footer></aside>';
|
||
}
|
||
|
||
function collabMap() {
|
||
return '<div class="map-canvas collaboration-map">' + mapBase() + '<div class="collab-alpha-boundary">' + taskArea('ALPHA\n3 UAV · 1 USV', 'alpha-area') + alphaVehicles.slice(0, 3).map(v => vehicleMarker(v)).join('') + '</div>' +
|
||
groupMarker(groups[1], 32, 23) + groupMarker(groups[2], 73, 23) + groupMarker(groups[3], 73, 53) + groupMarker(groups[4], 60, 75) +
|
||
operators.map((op, i) => '<span class="operator-cursor ' + op.color + '" style="left:' + [20, 36, 74, 38, 62][i] + '%;top:' + [69, 25, 20, 80, 80][i] + '%"><i>◢</i><b>' + op.name + '</b></span>').join('') +
|
||
'<article class="lease-card"><small>Command authority</small><h2>Alex</h2><p>lease expires <b>02:34</b></p><hr><small>Observers</small><p>' + statusTag('nominal', 'Sara') + statusTag('amber', 'David') + '</p></article>' +
|
||
'</div>';
|
||
}
|
||
|
||
function collaborationView() {
|
||
return '<main class="collaboration-workspace">' + groupRail(true) + '<section class="collab-map-stage">' + collabMap() + '</section><aside class="presence-panel"><header><h1>Mission presence</h1></header><div class="presence-list">' + operators.map(op => '<button class="presence-row" data-action="operator-detail" data-value="' + op.id + '"><i class="presence-dot ' + op.color + '"></i><span><strong>' + op.name + '</strong><small>' + op.role + '</small></span><b>⌄</b></button>').join('') + '</div><header class="subheading"><h2>Collaboration stream</h2><button data-action="view-history">View all</button></header><div class="collab-stream">' + operators.slice().reverse().map((op, i) => '<p><i class="presence-dot ' + op.color + '"></i><span>' + op.activity + '</span><small>' + (i * 2 + 2) + 'm ago</small></p>').join('') + '</div></aside><footer class="mission-events"><strong>Mission events</strong><div><span><i class="presence-dot cyan"></i>14:30:41Z Alex selected Alpha</span><span><i class="presence-dot amber"></i>14:28:15Z David drafted Task-144</span><span><i class="presence-dot green"></i>14:26:32Z Sara updated Bravo search area</span><span><i class="presence-dot violet"></i>14:24:10Z Maria added monitoring note</span></div><p>Shared awareness · authority changes are explicit and auditable.</p></footer></main>';
|
||
}
|
||
|
||
function recoveryMap() {
|
||
return '<div class="map-canvas recovery-map">' + mapBase() + taskArea('ALPHA\nSEARCH AREA', 'alpha-area') + routeLine() + alphaVehicles.slice(0, 4).map(v => vehicleMarker(v, false, true)).join('') + '<button class="failed-marker" style="left:71%;top:45%" data-action="show-failure"><span>!</span><b>UAV-03</b><small>Unavailable</small></button><article class="recovery-task-map-card">' + icon('people') + '<div><h2>Alpha Search</h2><p>Search and locate pattern</p></div><b class="amber">' + (state.reassigned ? '4 / 4' : '3 / 4') + '<small>assigned</small></b></article></div>';
|
||
}
|
||
|
||
function candidateCard(id, energy, distance, compatibility, selected) {
|
||
return '<button class="candidate-card ' + (selected ? 'selected' : '') + '" data-action="select-candidate" data-value="' + id + '"><header>' + icon('people') + '<span><strong>' + id + '</strong>' + statusTag('nominal', 'Ready') + '</span>' + (selected ? '<i class="round-check">' + icon('check') + '</i>' : '') + '</header><dl><div><dt>Energy</dt><dd>' + energy + '% ' + statusTag('nominal', '') + '</dd></div><div><dt>Distance to task</dt><dd>' + distance + ' km</dd></div><div><dt>Task compatibility</dt><dd class="' + (compatibility === 'Medium' ? 'amber' : 'green') + '">' + compatibility + '</dd></div><div><dt>Communication</dt><dd class="green">Strong</dd></div></dl>' + (selected ? '<footer>Recommended</footer>' : '') + '</button>';
|
||
}
|
||
|
||
function recoveryView() {
|
||
const active = state.selectedCandidate;
|
||
return '<main class="recovery-workspace"><aside class="recovery-left"><header><h1>Attention</h1></header><article class="recovery-alert critical">' + icon('alert') + '<div><h2>UAV-03</h2><p>Navigation unavailable</p><small>Since 14:33:02</small><b>High priority</b></div></article><article class="recovery-alert amber">' + icon('alert') + '<div><h2>Alpha search coverage below required</h2><p>Coverage gap detected</p><b>Requires attention</b></div></article><div class="fleet-summary"><h2>Fleet summary</h2><strong>47</strong><p>vehicles nominal</p><small>All other vehicles operating normally.</small>' + button('View fleet status', 'view-fleet', { icon: 'task' }) + '</div></aside><section class="recovery-map-stage">' + recoveryMap() + '</section><aside class="recovery-right"><header><h1>' + icon('shield') + ' Keep TASK-143 on plan</h1></header><section class="current-state"><h2>Current state</h2><dl><div><dt>Task</dt><dd>Alpha Search</dd></div><div><dt>Required</dt><dd>4 vehicles</dd></div><div><dt>Current</dt><dd class="amber">' + (state.reassigned ? '4 / 4' : '3 / 4') + '<small>assigned</small></dd></div><div><dt>Completion impact</dt><dd class="amber">' + (state.reassigned ? 'None' : '+11 minutes') + '</dd></div></dl></section><section class="recommendations"><h2>Recommended replacements</h2><p>' + icon('info') + ' Select a vehicle to review. This is a system recommendation for operator approval.</p><div class="candidate-grid">' + candidateCard('UAV-19', 78, 6.2, 'High', active === 'UAV-19') + candidateCard('UAV-27', 64, 9.8, 'High', active === 'UAV-27') + candidateCard('UAV-31', 82, 12.4, 'Medium', active === 'UAV-31') + '</div><p class="recommendation-note">' + icon('info') + ' Recommended because it can join before coverage gap exceeds threshold.</p></section><section class="local-preview"><h2>Local preview of reassignment</h2><div><span>' + icon('plane') + ' ' + active + '</span><i></i><b>ALPHA SEARCH AREA</b></div></section><footer>' + button('Cancel', 'cancel-modal') + button('Compare candidates', 'compare-candidates') + button(state.reassigned ? 'Recommendation accepted' : 'Accept recommendation', 'accept-replacement', { icon: 'shield', kind: 'primary', disabled: state.reassigned }) + '</footer></aside><footer class="recovery-events"><span>' + icon('alert') + ' <b>14:33:02</b> system detected UAV-03 unavailable</span><span>' + icon('shield') + ' <b>14:33:04</b> system evaluated replacement options</span><span>' + icon('info') + ' <b>14:33:17</b> operator reviewing recommendation</span></footer></main>';
|
||
}
|
||
|
||
function queueMap() {
|
||
return '<div class="queue-map">' + mapBase() + routeLine() + '<div class="base-service">⌾<span>Base<br>Service</span></div>' + taskArea('Search Area A', 'queue-search') + taskArea('Search Area B', 'queue-search future') + '<div class="queue-group-label"><span class="group-badge blue">A</span><strong>ALPHA</strong><small>12</small></div><article class="queue-group-card"><span class="group-badge blue">A</span><h2>Alpha</h2><p>' + icon('people') + '12 vehicles</p><p>' + icon('search') + 'Search</p><p>' + icon('people') + 'Operator Alex</p>' + statusTag('nominal') + '</article></div>';
|
||
}
|
||
|
||
function queueStep(step, index) {
|
||
return '<article class="queue-step ' + step.status + '"><button class="drag-handle" aria-label="Drag to reorder">⠿</button><span class="step-number">' + (index + 1) + '</span><div><h2>' + step.title + '</h2><p>' + step.detail + '</p>' + (step.status === 'current' ? '<i class="progress"><b style="width:82%"></b></i><small>82% complete</small>' : '') + '</div><aside><b>Alex</b><small>' + step.duration + '</small>' + statusTag(step.status === 'completed' ? 'nominal' : step.status === 'current' ? 'blue' : 'planned', step.status) + '</aside><div class="step-arrows">' + button('', 'queue-up', { icon: 'chevron', iconOnly: true, label: 'Move step up', value: index }) + button('', 'queue-down', { icon: 'chevron', iconOnly: true, label: 'Move step down', value: index }) + '</div></article>';
|
||
}
|
||
|
||
function queueView() {
|
||
const current = state.queue[1];
|
||
return '<main class="queue-workspace"><header class="queue-topline"><div><span>Mission</span><b>Coastal Survey</b></div><div><span>Time (local)</span><b>14:35:22</b></div><div><span>Weather</span><b>Overcast 18°C</b></div><div><span>Wind</span><b>12 kt NNE</b></div><div><span>Global status</span>' + statusTag('nominal') + '</div><div><span>Sync</span><b>3s ago</b></div></header><section class="queue-body"><section class="queue-map-stage">' + queueMap() + '</section><section class="queue-panel"><header><h1><span>Alpha</span> · Intent queue</h1><div>' + statusTag('planned', 'Draft changes') + button('', 'queue-menu', { icon: 'settings', iconOnly: true, label: 'Queue options' }) + '</div></header><div class="queue-content"><div class="step-list">' + state.queue.map(queueStep).join('') + '<div class="drop-zone">⠿ Drop step here to reorder</div></div><aside class="step-inspector"><header><b>Step 2 of 6</b>' + button('', 'close-inspector', { icon: 'close', iconOnly: true, label: 'Close step inspector' }) + '</header><h2>Goal</h2><p>Search Area A with coverage pattern to identify objects of interest.</p><h2>Owner</h2><p>' + icon('people') + ' Alex</p><h2>Resources</h2><p>' + icon('people') + ' 12 vehicles</p><h2>Est. duration</h2><p>' + icon('clock') + ' 18 min</p><h2>Expected finish</h2><p>⚑ 14:53 (local)</p><h2>Linked task</h2><p>⛓ TASK-143</p><h2>Local plan status</h2>' + statusTag('nominal', 'Plan valid') + '</aside></div><footer class="queue-actions">' + button('Add step', 'add-step', { icon: 'plus' }) + button('Reorder', 'reorder-queue', { icon: 'transfer' }) + button('Simulate timeline', 'simulate-queue', { icon: 'clock' }) + button('Review changes', 'review-queue', { kind: 'primary' }) + '</footer></section></section><footer class="queue-events"><strong>Activity timeline</strong><span><i class="presence-dot blue"></i>14:32 <b>Alex</b> reordered Return after Search Area A</span><span><i class="presence-dot blue"></i>14:33 <b>System</b> recomputed completion estimate</span></footer></main>';
|
||
}
|
||
|
||
function systemView() {
|
||
const color = (name, code, cls) => '<div class="swatch"><i class="' + cls + '"></i><b>' + name + '</b><small>' + code + '</small></div>';
|
||
return '<main class="system-view"><header class="system-title"><h1>Design system</h1><p>Multi-UAV Mission Control <span>· Screen 7</span></p></header><section class="system-grid"><article class="system-card colors"><h2>1. Color and status</h2><p>Surface (Graphite / Navy)</p><div class="swatches">' + color('Surface 0', '#0B0F14', 's0') + color('Surface 1', '#111724', 's1') + color('Surface 2', '#171D2B', 's2') + color('Surface 3', '#1D2536', 's3') + color('Border', '#2A3447', 'border') + '</div><p>Semantic status (meaning over decoration)</p><div class="swatches semantic">' + color('Blue', 'Active / Primary', 'blue') + color('Cyan', 'Selected', 'cyan') + color('Green', 'Nominal', 'green') + color('Amber', 'Attention', 'amber') + color('Red', 'Critical', 'red') + color('Violet', 'Planned', 'violet') + '</div><ul><li>Color communicates state and priority, not category.</li><li>Do not rely on color alone—use icon, text, and shape.</li><li>Maintain WCAG AA contrast for all text.</li></ul></article><article class="system-card type"><h2>2. Typography</h2><h1>H1 <b>Mission / Group Heading</b></h1><h2>H2 <b>Task Heading</b></h2><p>Body / UI Text <b>Readable action and mission information</b></p><small>Metadata / Secondary</small><pre>UAV-3F7A2C 08:43:21Z ALT 3250 ft</pre></article><article class="system-card components"><h2>3. Components</h2><div class="component-columns"><section><p>Buttons (44px height)</p>' + button('Create group', 'new-group', { icon: 'plus', kind: 'primary' }) + button('Assign task', 'open-preview', { icon: 'task' }) + button('Remove', 'remove', { icon: 'close', kind: 'danger' }) + '</section><section><p>Status chips</p><div class="chip-wrap">' + statusTag('nominal') + statusTag('attention') + statusTag('critical') + statusTag('cyan', 'Selected') + statusTag('planned') + '</div></section><section><p>Authority</p><div class="authority-demo">' + icon('shield') + '<span>Authority<br><b>Operator</b></span></div><div class="authority-demo muted">▣ Read-only observer</div></section></div><div class="mini-components"><article class="mini-task"><b>ISR Sweep North</b>' + statusTag('nominal') + '<p>Type ISR</p><p>Area Sector 12</p><p>Assets 14 UAV</p></article><article class="mini-group"><span class="group-badge cyan">27</span><b>Group Alpha</b>' + statusTag('cyan', 'Selected') + '<p>Members 27 UAVs</p><p>Lead UAV-3F7A2C</p></article><article class="mini-alert">' + icon('alert') + '<span>' + statusTag('critical') + '<b>Link degraded on UAV-7B1C</b></span></article></div></article><article class="system-card interactions"><h2>4. Interaction states</h2><div class="multi-select-demo"><div class="select-map">' + alphaVehicles.slice(0, 9).map(v => vehicleMarker(v, v.id !== 'UAV-03')).join('') + '<i></i></div><div class="multi-summary">' + icon('check') + '<strong>27 vehicles selected</strong><span>ISR <b>14</b></span><span>Relay <b>7</b></span><span>Transport <b>6</b></span>' + button('Create group', 'new-group', { icon: 'plus', kind: 'primary' }) + button('Assign task', 'open-preview') + button('Add to group', 'group-vehicles') + '</div></div></article><article class="system-card state-cards"><h2>Selection state cards</h2><div class="selection-state-card selected"><span class="group-badge cyan">27</span><b>Group Alpha</b>' + statusTag('cyan', 'Selected') + '<small>Members · 27 UAVs</small></div><div class="selection-state-card"><span class="group-badge green">◈</span><b>ISR Sweep North</b>' + statusTag('nominal') + '<small>Start · 09:00Z · Assets 14 UAVs</small></div><div class="selection-state-card"><span class="group-badge blue">✈</span><b>UAV-3F7A2C</b>' + statusTag('cyan', 'Selected') + '<small>Type · ISR · Alt 3,250 ft</small></div><div class="selection-state-card muted"><span class="group-badge">□</span><b>No selection</b><small>Use selection tools to choose vehicles or groups.</small></div></article><article class="system-card responsive"><h2>5. Responsive composition</h2><p>Map-first layouts adapt by collapsing / expanding secondary panels.</p><div class="responsive-samples"><div><b>Laptop</b><i class="laptop"></i></div><div><b>27-inch</b><i class="monitor"></i></div><div><b>Ultrawide</b><i class="ultrawide"></i></div><div><b>4K wall</b><i class="wall"></i></div></div><h2>6. Density and accessibility</h2><div class="accessibility"><p><b>44px</b> minimum tap / click target</p><p class="focus-example">Assign task <kbd>Tab</kbd></p><p><b>Aa</b> high-contrast UI text</p><p>' + icon('info') + ' Semantic label and tooltip</p></div></article></section></main>';
|
||
}
|
||
|
||
function modal() {
|
||
if (!state.modal) return '';
|
||
let content = '';
|
||
if (state.modal === 'preview') {
|
||
content = '<div class="modal-dialog command-preview" role="dialog" aria-modal="true" aria-labelledby="preview-title"><header><div><small>Command preview</small><h1 id="preview-title">Assign TASK-143</h1></div>' + button('', 'cancel-modal', { icon: 'close', iconOnly: true, label: 'Close command preview' }) + '</header><p class="preview-lede">6 selected vehicles will be assigned to <b>Area Search — Northern Sector</b>.</p><div class="preview-route"><div><i></i><b>Current assignment</b></div><span>' + icon('route') + '</span><div class="target"><i></i><b>Proposed task</b></div></div><div class="preview-validation">' + validationRow('Estimated completion', '18 min') + validationRow('Energy reserve', 'All above minimum') + validationRow('Route conflicts', 'None detected') + validationRow('Geofence conflicts', 'None detected') + validationRow('Vehicle compatibility', '6 / 6') + validationRow('Potential issue', '1 aircraft may require reassignment', 'warning') + '</div><footer>' + button('Cancel', 'cancel-modal') + button('Confirm assignment', 'confirm-assignment', { icon: 'check', kind: 'primary' }) + '</footer></div>';
|
||
} else if (state.modal === 'transfer') {
|
||
content = '<div class="modal-dialog transfer-dialog" role="dialog" aria-modal="true" aria-labelledby="transfer-title"><header><div><small>Command lease</small><h1 id="transfer-title">Operator B requests command</h1></div>' + button('', 'cancel-modal', { icon: 'close', iconOnly: true, label: 'Close request dialog' }) + '</header><div class="transfer-authority"><span class="group-badge cyan">A</span><div><b>Alpha · command authority</b><p>Held by Alex · lease expires 02:34</p></div></div><label>Reason<textarea readonly>Emergency intervention</textarea></label><p class="notice">' + icon('info') + ' This action will transfer command authority from <b>Alex</b> to <b>Sara</b>. The change is logged for all operators.</p><footer>' + button('Deny', 'deny-transfer') + button('Transfer control', 'confirm-transfer', { icon: 'transfer', kind: 'danger' }) + '</footer></div>';
|
||
} else if (state.modal === 'help') {
|
||
content = '<div class="modal-dialog help-dialog" role="dialog" aria-modal="true"><header><h1>Smart selection</h1>' + button('', 'cancel-modal', { icon: 'close', iconOnly: true, label: 'Close help' }) + '</header><p>Use a natural-language request or structured filters to select a resource-ready group of aircraft. The POC derives the result from fictional vehicles and always validates the task before confirmation.</p><footer>' + button('Got it', 'cancel-modal', { kind: 'primary' }) + '</footer></div>';
|
||
}
|
||
return '<div class="modal-backdrop" data-action="cancel-modal"><div data-stop-propagation>' + content + '</div></div>';
|
||
}
|
||
|
||
function toast() {
|
||
return state.toast ? '<div class="toast">' + icon('check') + '<span>' + state.toast + '</span><button data-action="clear-toast" aria-label="Dismiss notification">' + icon('close') + '</button></div>' : '';
|
||
}
|
||
|
||
function app() {
|
||
const views = { overview: overviewView, zoom: zoomView, selection: smartSelectionView, collaboration: collaborationView, recovery: recoveryView, queue: queueView, system: systemView };
|
||
document.getElementById('app').innerHTML = topBar() + views[state.view]() + modal() + toast();
|
||
}
|
||
|
||
function setToast(message) {
|
||
state.toast = message;
|
||
window.clearTimeout(window.__atlasToast);
|
||
window.__atlasToast = window.setTimeout(() => { state.toast = ''; app(); }, 3600);
|
||
}
|
||
|
||
function clickAction(action, value) {
|
||
if (action === 'set-view') { state.view = value; state.modal = null; }
|
||
if (action === 'select-group') { state.selectedGroup = value; state.view = state.view === 'collaboration' ? 'collaboration' : 'overview'; setToast(groupById(value).name + ' selected — contextual controls updated.'); }
|
||
if (action === 'select-vehicle') { state.selectedVehicle = value; state.view = 'zoom'; setToast(value + ' selected — vehicle detail is shown on request.'); }
|
||
if (action === 'toggle-layers') state.layersOpen = !state.layersOpen;
|
||
if (action === 'open-preview') state.modal = 'preview';
|
||
if (action === 'open-transfer') state.modal = 'transfer';
|
||
if (action === 'cancel-modal' || action === 'close-assign' || action === 'close-inspector') state.modal = null;
|
||
if (action === 'show-filter-help') state.modal = 'help';
|
||
if (action === 'confirm-assignment') { state.modal = null; setToast('TASK-143 assigned after route, energy, and constraint validation.'); }
|
||
if (action === 'confirm-transfer') { state.modal = null; groups[0].lease = 'Sara'; groups[0].operator = 'Sara'; setToast('Command lease transferred to Sara. The audit event is visible to all operators.'); }
|
||
if (action === 'deny-transfer') { state.modal = null; setToast('Command request denied. Alex retains the Alpha command lease.'); }
|
||
if (action === 'move-group') setToast('Move intent opened. A route preview would be required before confirmation.');
|
||
if (action === 'hold-group') setToast('Hold intent queued for ' + groupById(state.selectedGroup).name + '.');
|
||
if (action === 'group-vehicles') setToast('Create / add to group workflow opened for the current selection.');
|
||
if (action === 'split-group') setToast('Split group is available after selecting a destination task.');
|
||
if (action === 'open-vehicle-detail') { state.view = 'zoom'; state.selectedVehicle = 'UAV-07'; }
|
||
if (action === 'select-candidate') state.selectedCandidate = value;
|
||
if (action === 'accept-replacement') { state.reassigned = true; setToast(state.selectedCandidate + ' assigned to Alpha Search. Task coverage returned to 4 / 4.'); }
|
||
if (action === 'show-failure') setToast('UAV-03 is unavailable. A replacement recommendation needs operator approval.');
|
||
if (action === 'queue-up' || action === 'queue-down') {
|
||
const index = Number(value); const direction = action === 'queue-up' ? -1 : 1; const next = index + direction;
|
||
if (next >= 0 && next < state.queue.length) { const copy = state.queue.slice(); [copy[index], copy[next]] = [copy[next], copy[index]]; state.queue = copy; state.queueDraft = true; setToast('Intent queue reordered locally. Review changes to commit.'); }
|
||
}
|
||
if (action === 'review-queue') { state.queueDraft = false; setToast('Queue review recorded. The plan remains valid.'); }
|
||
if (action === 'reorder-queue') { state.queueDraft = true; setToast('Reorder mode active — use the arrows on each intent step.'); }
|
||
if (action === 'simulate-queue') setToast('Simulation complete: no constraints conflict with the proposed intent queue.');
|
||
if (action === 'add-step') { state.queue.push({ title: 'New intent', detail: 'Configure task', duration: '—', status: 'queued' }); state.queueDraft = true; setToast('New draft intent added to the queue.'); }
|
||
if (action === 'new-group') setToast('New group creation starts with a multi-vehicle selection.');
|
||
if (action === 'remove-filter') setToast('Filter removed. The fictional result set has been recalculated.');
|
||
if (action === 'clear-filters') setToast('Structured filters cleared. Use a query to build a new selection.');
|
||
if (action === 'map-zoom-in') setToast('Map zoomed in — semantic detail increases as you select groups and vehicles.');
|
||
if (action === 'map-zoom-out') setToast('Map zoomed out — groups aggregate to protect situational awareness.');
|
||
if (action === 'view-alerts' || action === 'view-fleet' || action === 'view-history') setToast('This POC surfaces exceptions first; the nominal fleet stays intentionally quiet.');
|
||
if (action === 'compare-candidates') setToast('Candidate comparison keeps energy, task compatibility, and link quality visible.');
|
||
if (action === 'clear-toast') state.toast = '';
|
||
app();
|
||
}
|
||
|
||
document.addEventListener('click', event => {
|
||
const el = event.target.closest('[data-action]');
|
||
if (el) {
|
||
event.preventDefault();
|
||
clickAction(el.dataset.action, el.dataset.value);
|
||
return;
|
||
}
|
||
if (event.target.closest('[data-stop-propagation]')) event.stopPropagation();
|
||
});
|
||
|
||
document.addEventListener('keydown', event => {
|
||
if (event.key === 'Escape' && state.modal) { state.modal = null; app(); }
|
||
});
|
||
|
||
app();
|