diff --git a/frontend/assets/css/style.css b/frontend/assets/css/style.css index 431a559..6edabab 100644 --- a/frontend/assets/css/style.css +++ b/frontend/assets/css/style.css @@ -127,18 +127,6 @@ body { } /* Network Map */ -.node-tooltip { - position: absolute; - background: #131a2b; - border: 1px solid var(--neptune-border); - border-radius: .375rem; - padding: .5rem .75rem; - font-size: .8rem; - pointer-events: none; - z-index: 1000; - display: none; -} - #networkCanvas { cursor: grab; } @@ -147,6 +135,27 @@ body { cursor: grabbing; } +.node-list-item i { + width: 1rem; + text-align: center; +} + +.nav-pills .nav-link { + color: #94a3b8; +} + +.nav-pills .nav-link.active { + background: var(--neptune-accent); + color: #fff; +} + +kbd { + border: 1px solid var(--neptune-border); + padding: 1px 5px; + border-radius: 3px; + font-size: .7rem; +} + /* Scrollbar */ ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: var(--neptune-bg); } diff --git a/frontend/assets/js/app.js b/frontend/assets/js/app.js index 6b0fe8d..5730baa 100644 --- a/frontend/assets/js/app.js +++ b/frontend/assets/js/app.js @@ -7,7 +7,6 @@ let shapes = []; let selectedNodeId = null; let selectedShapeId = null; -// Canvas state let canvas, ctx; let canvasNodes = []; let canvasLinks = []; @@ -16,7 +15,6 @@ let panX = 0, panY = 0; let isPanning = false; let panStartX, panStartY; -// Drag state let dragTarget = null; let dragType = null; let dragOffX, dragOffY; @@ -47,13 +45,12 @@ document.addEventListener('DOMContentLoaded', () => { canvas.addEventListener('mousemove', onMouseMove); canvas.addEventListener('mouseup', onMouseUp); canvas.addEventListener('dblclick', onDblClick); - canvas.addEventListener('contextmenu', onContextMenu); + canvas.addEventListener('contextmenu', (e) => e.preventDefault()); window.addEventListener('resize', () => { resizeCanvas(); renderNetwork(); }); - // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Delete' || e.key === 'Backspace') { - if (document.activeElement === canvas || document.activeElement?.tagName !== 'INPUT') { + if (!document.activeElement || document.activeElement.tagName !== 'INPUT') { if (selectedNodeId) deleteSelectedNode(); else if (selectedShapeId) deleteSelectedShape(); } @@ -63,15 +60,13 @@ document.addEventListener('DOMContentLoaded', () => { selectedShapeId = null; renderNetwork(); renderNodeList(); + renderShapeList(); } }); document.querySelectorAll('[data-bs-toggle="tab"]').forEach(tab => { tab.addEventListener('shown.bs.tab', () => { - if (tab.id === 'network-tab') { - resizeCanvas(); - renderNetwork(); - } + if (tab.id === 'network-tab') { resizeCanvas(); renderNetwork(); } }); }); @@ -95,17 +90,17 @@ async function apiFetch(path, options = {}) { // ==================== TEAMS ==================== async function loadTeams() { teams = await apiFetch('teams'); - const selTeam = document.getElementById('eventTeam'); + const sel = document.getElementById('eventTeam'); const filter = document.getElementById('teamFilter'); - selTeam.innerHTML = ''; + sel.innerHTML = ''; filter.innerHTML = ''; teams.forEach(t => { - selTeam.innerHTML += ``; + sel.innerHTML += ``; filter.innerHTML += ``; }); } -// ==================== EVENTS / TIMELINE ==================== +// ==================== EVENTS ==================== async function loadEvents() { events = await apiFetch('events'); renderTimeline(); @@ -124,21 +119,20 @@ function renderTimeline() { ); if (!filtered.length) { - container.innerHTML = `

No events yet. Create your first incident entry!

`; + container.innerHTML = `

No events yet. Create your first incident entry!

`; return; } container.innerHTML = filtered.map(e => { - const sevClass = 'severity-' + e.severity; const date = new Date(e.occurred_at).toLocaleString(); return `
-
+
- ${e.team_name} + ${esc(e.team_name)} ${e.severity} ${e.event_type}
@@ -147,15 +141,11 @@ function renderTimeline() {
${esc(e.title)}
${e.description ? `

${esc(e.description)}

` : ''}
-
- Comments ${e.comments && e.comments.length ? `(${e.comments.length})` : ''} -
-
- ${renderComments(e)} -
+
Comments ${e.comments && e.comments.length ? `(${e.comments.length})` : ''}
+
${renderComments(e)}
- +
@@ -165,10 +155,10 @@ function renderTimeline() { } function renderComments(event) { - if (!event.comments || !event.comments.length) return '
No comments yet
'; + if (!event.comments || !event.comments.length) return '
No comments yet
'; return event.comments.map(c => `
-
+
${esc(c.author)} @@ -186,10 +176,7 @@ async function addComment(eventId, el) { const body = input.value.trim(); if (!body) return; const author = prompt('Your name:') || 'Anonymous'; - await apiFetch('comments', { - method: 'POST', - body: JSON.stringify({ event_id: eventId, author, body }) - }); + await apiFetch('comments', { method: 'POST', body: JSON.stringify({ event_id: eventId, author, body }) }); input.value = ''; loadEvents(); } @@ -210,7 +197,7 @@ async function saveEvent() { loadEvents(); } -// ==================== NETWORK MAP ==================== +// ==================== NETWORK MAP DATA ==================== async function loadNetworkData() { const [n, l, s] = await Promise.all([ apiFetch('nodes'), @@ -223,7 +210,7 @@ async function loadNetworkData() { populateNodeSelects(); renderNodeList(); renderShapeList(); - if (shapes.length) nextShapeZ = Math.max(...shapes.map(s => s.z_index)) + 1; + if (shapes.length) nextShapeZ = Math.max(...shapes.map(x => x.z_index)) + 1; renderNetwork(); } @@ -235,17 +222,20 @@ function populateNodeSelects() { function renderNodeList() { const list = document.getElementById('nodeList'); - list.innerHTML = nodes.map(n => ` -
+ list.innerHTML = nodes.map(n => { + const iconMap = { host:'fa-desktop', server:'fa-server', router:'fa-route', firewall:'fa-shield-halved', switch:'fa-network-wired', cloud:'fa-cloud', endpoint:'fa-laptop', other:'fa-circle' }; + return ` +
+
${esc(n.label)}
${n.ip_address || '—'} · ${n.node_type}
-
- `).join(''); +
`; + }).join(''); } function renderShapeList() { @@ -275,7 +265,7 @@ function selectNode(id) {
Type: ${n.node_type}
Status: ${n.status}
Group: ${n.group_name}
- +
`; } @@ -360,56 +350,46 @@ async function saveShape() { loadNetworkData(); } +function getNodeColorVal(type) { + const c = { host:'#3b82f6', server:'#8b5cf6', router:'#f59e0b', firewall:'#ef4444', switch:'#06b6d4', cloud:'#22c55e', endpoint:'#ec4899', other:'#6b7280' }; + return c[type] || '#6b7280'; +} + // ==================== CANVAS RENDERING ==================== -const NODE_ICONS = { - host: ['M7,4 L17,4 L20,9 L20,18 L4,18 L4,9 Z', '#3b82f6'], - server: ['M6,3 L18,3 L20,6 L20,20 L4,20 L4,6 Z M8,10 L16,10 M8,14 L16,14 M8,17 L12,17', '#8b5cf6'], - router: ['M12,3 L21,10 L18,20 L6,20 L3,10 Z M12,6 L12,17 M8,10 L16,10', '#f59e0b'], - firewall: ['M6,3 L18,3 L21,8 L21,16 L18,21 L6,21 L3,16 L3,8 Z M9,10 L15,10 L15,14 L9,14 Z', '#ef4444'], - switch: ['M4,8 L20,8 L20,16 L4,16 Z M7,11 L7,13 M10,11 L10,13 M13,11 L13,13 M16,11 L16,13', '#06b6d4'], - cloud: ['M10,4 C6,4 4,7 5,10 C3,11 2,14 4,16 L8,16 C10,18 14,18 16,16 L20,16 C22,14 21,10 19,9 C20,6 17,4 15,5 C14,4 12,4 10,4 Z', '#22c55e'], - endpoint: ['M8,4 L16,4 L18,10 L18,16 L16,20 L8,20 L6,16 L6,10 Z M10,12 L14,12 M12,10 L12,14', '#ec4899'], - other: ['M8,6 L16,6 L18,12 L16,18 L8,18 L6,12 Z', '#6b7280'] +const NODE_FA_ICONS = { + host: { path: 'M7,4 L17,4 L20,9 L20,18 L4,18 L4,9 Z', color: '#3b82f6' }, + server: { path: 'M6,3 L18,3 L20,6 L20,20 L4,20 L4,6 Z M8,10 L16,10 M8,14 L16,14 M8,17 L12,17', color: '#8b5cf6' }, + router: { path: 'M12,3 L21,10 L18,20 L6,20 L3,10 Z M12,6 L12,17 M8,10 L16,10', color: '#f59e0b' }, + firewall: { path: 'M6,3 L18,3 L21,8 L21,16 L18,21 L6,21 L3,16 L3,8 Z M9,10 L15,10 L15,14 L9,14 Z', color: '#ef4444' }, + switch: { path: 'M4,8 L20,8 L20,16 L4,16 Z M7,11 L7,13 M10,11 L10,13 M13,11 L13,13 M16,11 L16,13', color: '#06b6d4' }, + cloud: { path: 'M10,4 C6,4 4,7 5,10 C3,11 2,14 4,16 L8,16 C10,18 14,18 16,16 L20,16 C22,14 21,10 19,9 C20,6 17,4 15,5 C14,4 12,4 10,4 Z', color: '#22c55e' }, + endpoint: { path: 'M8,4 L16,4 L18,10 L18,16 L16,20 L8,20 L6,16 L6,10 Z M10,12 L14,12 M12,10 L12,14', color: '#ec4899' }, + other: { path: 'M8,6 L16,6 L18,12 L16,18 L8,18 L6,12 Z', color: '#6b7280' } }; function buildCanvasGraph() { canvasNodes = nodes.map(n => { - const icon = NODE_ICONS[n.node_type] || NODE_ICONS.other; + const fa = NODE_FA_ICONS[n.node_type] || NODE_FA_ICONS.other; return { - id: n.id, - label: n.label, - ip: n.ip_address, - type: n.node_type, - status: n.status, - group: n.group_name, - x: parseFloat(n.pos_x) || 100, - y: parseFloat(n.pos_y) || 100, - iconPath: icon[0], - color: icon[1], - w: 36, - h: 36 + id: n.id, label: n.label, ip: n.ip_address, + type: n.node_type, status: n.status, group: n.group_name, + x: parseFloat(n.pos_x) || 100, y: parseFloat(n.pos_y) || 100, + iconPath: fa.path, color: fa.color, w: 36, h: 36 }; }); canvasLinks = links.map(l => ({ source: canvasNodes.find(n => n.id == l.source_id), target: canvasNodes.find(n => n.id == l.target_id), - type: l.link_type, - label: l.label + type: l.link_type, label: l.label })).filter(l => l.source && l.target); canvasShapes = shapes.map(s => ({ - id: s.id, - label: s.label, - type: s.shape_type, - x: parseFloat(s.pos_x), - y: parseFloat(s.pos_y), - w: parseFloat(s.width), - h: parseFloat(s.height), - color: s.color, - borderColor: s.border_color, - opacity: parseFloat(s.opacity), - z: parseInt(s.z_index) + id: s.id, label: s.label, type: s.shape_type, + x: parseFloat(s.pos_x), y: parseFloat(s.pos_y), + w: parseFloat(s.width), h: parseFloat(s.height), + color: s.color, borderColor: s.border_color, + opacity: parseFloat(s.opacity), z: parseInt(s.z_index) })); } @@ -419,74 +399,54 @@ function renderNetwork() { ctx.save(); ctx.translate(panX, panY); - // Draw shapes (lowest layer) canvasShapes.sort((a, b) => a.z - b.z).forEach(drawShape); - // Draw links canvasLinks.forEach(l => { ctx.beginPath(); ctx.moveTo(l.source.x, l.source.y); ctx.lineTo(l.target.x, l.target.y); - const colors = { direct: '#334155', vpn: '#eab308', wireless: '#22c55e', monitored: '#3b82f6' }; ctx.strokeStyle = colors[l.type] || '#334155'; ctx.lineWidth = l.type === 'vpn' ? 2.5 : 1.5; - if (l.type === 'vpn' || l.type === 'wireless') ctx.setLineDash([6, 4]); else ctx.setLineDash([]); - ctx.stroke(); ctx.setLineDash([]); - if (l.label) { - const mx = (l.source.x + l.target.x) / 2; - const my = (l.source.y + l.target.y) / 2; ctx.fillStyle = '#94a3b8'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText(l.label, mx, my - 8); + ctx.fillText(l.label, (l.source.x + l.target.x) / 2, (l.source.y + l.target.y) / 2 - 8); } }); - // Draw nodes canvasNodes.forEach(drawCanvasNode); ctx.restore(); } function drawShape(s) { ctx.save(); - const isSelected = selectedShapeId == s.id; - + const sel = selectedShapeId == s.id; ctx.globalAlpha = s.opacity; if (s.type === 'ellipse') { ctx.beginPath(); ctx.ellipse(s.x + s.w / 2, s.y + s.h / 2, s.w / 2, s.h / 2, 0, 0, Math.PI * 2); - ctx.fillStyle = s.color; - ctx.fill(); - ctx.globalAlpha = 1; - ctx.strokeStyle = isSelected ? '#ffffff' : s.borderColor; - ctx.lineWidth = isSelected ? 2.5 : 1.5; - ctx.setLineDash([5, 3]); - ctx.stroke(); - ctx.setLineDash([]); } else { ctx.beginPath(); ctx.roundRect(s.x, s.y, s.w, s.h, 8); - ctx.fillStyle = s.color; - ctx.fill(); - ctx.globalAlpha = 1; - ctx.strokeStyle = isSelected ? '#ffffff' : s.borderColor; - ctx.lineWidth = isSelected ? 2.5 : 1.5; - ctx.setLineDash([5, 3]); - ctx.stroke(); - ctx.setLineDash([]); } + ctx.fillStyle = s.color; + ctx.fill(); + ctx.globalAlpha = 1; + ctx.strokeStyle = sel ? '#ffffff' : s.borderColor; + ctx.lineWidth = sel ? 2.5 : 1.5; + ctx.setLineDash([5, 3]); + ctx.stroke(); + ctx.setLineDash([]); - // Resize handles - if (isSelected) { - const handles = getShapeHandles(s); - handles.forEach(h => { + if (sel) { + getShapeHandles(s).forEach(h => { ctx.beginPath(); ctx.arc(h.x, h.y, 5, 0, Math.PI * 2); ctx.fillStyle = '#ffffff'; @@ -497,54 +457,30 @@ function drawShape(s) { }); } - // Label - ctx.globalAlpha = 1; ctx.fillStyle = '#94a3b8'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText(s.label, s.x + s.w / 2, s.y - 8); - + ctx.fillText(s.label || '', s.x + s.w / 2, s.y - 8); ctx.restore(); } function drawCanvasNode(n) { - const isSelected = selectedNodeId == n.id; - const s = 1.8; - + const sel = selectedNodeId == n.id; ctx.save(); - if (isSelected) { - ctx.shadowColor = n.color; - ctx.shadowBlur = 18; - } - // Fill background - ctx.translate(n.x, n.y); - ctx.scale(1, 1); + if (sel) { ctx.shadowColor = n.color; ctx.shadowBlur = 18; } + ctx.beginPath(); - - // Icon background circle/box - if (n.type === 'switch') { - ctx.rect(-20, -20, 40, 40); - } else { - ctx.arc(0, 0, 22, 0, Math.PI * 2); - } + ctx.arc(n.x, n.y, 22, 0, Math.PI * 2); ctx.fillStyle = n.color + '18'; ctx.fill(); - - // Border - ctx.strokeStyle = isSelected ? '#ffffff' : n.color; - ctx.lineWidth = isSelected ? 2.5 : 1.5; + ctx.strokeStyle = sel ? '#ffffff' : n.color; + ctx.lineWidth = sel ? 2.5 : 1.5; ctx.stroke(); ctx.shadowBlur = 0; - // Icon path - const scalePath = (path, cx, cy, sc) => { - return path.replace(/([\d.]+)/g, (m) => { - const v = parseFloat(m); - return ((v - 12) * sc + cx).toFixed(1); - }); - }; - const scaled = scalePath(n.iconPath, 0, 0, 0.85); + // Icon path scaled + const scaled = n.iconPath.replace(/([\d.]+)/g, m => ((parseFloat(m) - 12) * 0.85 + 0).toFixed(1)); const path = new Path2D(scaled); ctx.fillStyle = n.color; ctx.fill(path); @@ -552,20 +488,15 @@ function drawCanvasNode(n) { // Status dot ctx.beginPath(); ctx.arc(17, -17, 5, 0, Math.PI * 2); - const statusColors = { online: '#22c55e', offline: '#6b7280', unknown: '#9ca3af', compromised: '#ef4444', monitoring: '#eab308' }; - ctx.fillStyle = statusColors[n.status] || '#9ca3af'; + const sc = { online: '#22c55e', offline: '#6b7280', unknown: '#9ca3af', compromised: '#ef4444', monitoring: '#eab308' }; + ctx.fillStyle = sc[n.status] || '#9ca3af'; ctx.fill(); if (n.status === 'compromised') { - ctx.strokeStyle = '#ef4444'; - ctx.lineWidth = 2; - ctx.stroke(); + ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 2; ctx.stroke(); } - ctx.translate(-n.x, -n.y); - - // Label ctx.fillStyle = '#e2e8f0'; - ctx.font = isSelected ? 'bold 11px sans-serif' : '10px sans-serif'; + ctx.font = sel ? 'bold 11px sans-serif' : '10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(n.label, n.x, n.y + 34); if (n.ip) { @@ -573,25 +504,21 @@ function drawCanvasNode(n) { ctx.font = '9px sans-serif'; ctx.fillText(n.ip, n.x, n.y + 46); } - ctx.restore(); } function getShapeHandles(s) { return [ - { x: s.x, y: s.y, cx: 0, cy: 0 }, - { x: s.x + s.w, y: s.y, cx: 1, cy: 0 }, - { x: s.x + s.w, y: s.y + s.h, cx: 1, cy: 1 }, - { x: s.x, y: s.y + s.h, cx: 0, cy: 1 } + { x: s.x, y: s.y }, + { x: s.x + s.w, y: s.y }, + { x: s.x + s.w, y: s.y + s.h }, + { x: s.x, y: s.y + s.h } ]; } // ==================== CANVAS EVENTS ==================== -function getCanvasNodeRaw(mx, my) { - return canvasNodes.find(n => { - const dx = mx - n.x, dy = my - n.y; - return Math.sqrt(dx * dx + dy * dy) < 28; - }); +function getCanvasNodeAt(mx, my) { + return canvasNodes.find(n => Math.hypot(mx - n.x, my - n.y) < 28); } function getShapeAt(mx, my) { @@ -602,13 +529,11 @@ function getShapeAt(mx, my) { return null; } -function getShapeResizeHandle(mx, my) { +function getShapeResizeHandleAt(mx, my) { for (const s of canvasShapes) { if (selectedShapeId != s.id) continue; - const handles = getShapeHandles(s); - for (const h of handles) { - const dx = mx - h.x, dy = my - h.y; - if (Math.sqrt(dx * dx + dy * dy) < 8) return { shape: s, handle: h }; + for (const h of getShapeHandles(s)) { + if (Math.hypot(mx - h.x, my - h.y) < 8) return { shape: s, hx: h.x, hy: h.y }; } } return null; @@ -619,51 +544,36 @@ function onMouseDown(e) { const mx = e.clientX - rect.left - panX; const my = e.clientY - rect.top - panY; - // Check shape resize handle first (only for selected shape) - const resize = getShapeResizeHandle(mx, my); + const resize = getShapeResizeHandleAt(mx, my); if (resize) { - dragType = 'resize'; - dragTarget = resize.shape; - dragHandle = resize.handle; - dragOrig = { x: resize.shape.x, y: resize.shape.y, w: resize.shape.w, h: resize.shape.h }; - dragOffX = mx; - dragOffY = my; + const s = resize.shape; + dragType = 'resize'; dragTarget = s; + dragOffX = mx; dragOffY = my; + dragOrig = { x: s.x, y: s.y, w: s.w, h: s.h }; return; } - // Check node - const node = getCanvasNodeRaw(mx, my); + const node = getCanvasNodeAt(mx, my); if (node) { - dragType = 'node'; - dragTarget = node; - dragOffX = mx - node.x; - dragOffY = my - node.y; + dragType = 'node'; dragTarget = node; + dragOffX = mx - node.x; dragOffY = my - node.y; selectNode(node.id); return; } - // Check shape body const shape = getShapeAt(mx, my); if (shape) { - selectedNodeId = null; - selectedShapeId = shape.id; - dragType = 'shape'; - dragTarget = shape; - dragOffX = mx - shape.x; - dragOffY = my - shape.y; - renderShapeList(); - renderNetwork(); + selectedNodeId = null; selectedShapeId = shape.id; + dragType = 'shape'; dragTarget = shape; + dragOffX = mx - shape.x; dragOffY = my - shape.y; + renderNodeList(); renderShapeList(); renderNetwork(); return; } - // Pan - selectedNodeId = null; - selectedShapeId = null; - renderNodeList(); - renderShapeList(); + selectedNodeId = null; selectedShapeId = null; + renderNodeList(); renderShapeList(); isPanning = true; - panStartX = e.clientX - panX; - panStartY = e.clientY - panY; + panStartX = e.clientX - panX; panStartY = e.clientY - panY; canvas.style.cursor = 'grabbing'; } @@ -678,57 +588,38 @@ function onMouseMove(e) { dragTarget.x = e.clientX - rect.left - panX - dragOffX; dragTarget.y = e.clientY - rect.top - panY - dragOffY; renderNetwork(); - } else if (dragType === 'resize' && dragTarget && dragHandle) { + } else if (dragType === 'resize' && dragTarget) { const dx = e.clientX - rect.left - panX - dragOffX; const dy = e.clientY - rect.top - panY - dragOffY; - const h = dragHandle; - if (h.cx === 0) { dragTarget.x = dragOrig.x + dx; dragTarget.w = dragOrig.w - dx; } - else { dragTarget.w = dragOrig.w + dx; } - if (h.cy === 0) { dragTarget.y = dragOrig.y + dy; dragTarget.h = dragOrig.h - dy; } - else { dragTarget.h = dragOrig.h + dy; } - if (dragTarget.w < 50) dragTarget.w = 50; - if (dragTarget.h < 50) dragTarget.h = 50; + const s = dragTarget; + let nx = dragOrig.x, ny = dragOrig.y, nw = dragOrig.w, nh = dragOrig.h; + if (dragOffX < dragOrig.x + dragOrig.w / 2) { nx = dragOrig.x + dx; nw = dragOrig.w - dx; } + else { nw = dragOrig.w + dx; } + if (dragOffY < dragOrig.y + dragOrig.h / 2) { ny = dragOrig.y + dy; nh = dragOrig.h - dy; } + else { nh = dragOrig.h + dy; } + if (nw < 50) nw = 50; + if (nh < 50) nh = 50; + s.x = nx; s.y = ny; s.w = nw; s.h = nh; renderNetwork(); } else if (isPanning) { - panX = e.clientX - panStartX; - panY = e.clientY - panStartY; + panX = e.clientX - panStartX; panY = e.clientY - panStartY; renderNetwork(); } else { const mx = e.clientX - rect.left - panX; const my = e.clientY - rect.top - panY; - const node = getCanvasNodeRaw(mx, my); - const shape = getShapeAt(mx, my); - const resize = getShapeResizeHandle(mx, my); - if (resize) canvas.style.cursor = 'nwse-resize'; - else if (node || shape) canvas.style.cursor = 'pointer'; + if (getShapeResizeHandleAt(mx, my)) canvas.style.cursor = 'nwse-resize'; + else if (getCanvasNodeAt(mx, my) || getShapeAt(mx, my)) canvas.style.cursor = 'pointer'; else canvas.style.cursor = 'grab'; } } function onMouseUp(e) { - if (dragTarget) { - if (dragType === 'node') { - apiFetch(`nodes/${dragTarget.id}`, { - method: 'PUT', - body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y }) - }); - } else if (dragType === 'shape' || dragType === 'resize') { - apiFetch(`shapes/${dragTarget.id}`, { - method: 'PUT', - body: JSON.stringify({ - pos_x: dragTarget.x, - pos_y: dragTarget.y, - width: dragTarget.w, - height: dragTarget.h - }) - }); - } + if (dragTarget && dragType === 'node') { + apiFetch(`nodes/${dragTarget.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y }) }); + } else if (dragTarget && (dragType === 'shape' || dragType === 'resize')) { + apiFetch(`shapes/${dragTarget.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y, width: dragTarget.w, height: dragTarget.h }) }); } - - dragType = null; - dragTarget = null; - dragHandle = null; - dragOrig = null; + dragType = null; dragTarget = null; dragOrig = null; isPanning = false; canvas.style.cursor = 'grab'; } @@ -737,24 +628,10 @@ function onDblClick(e) { const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left - panX; const my = e.clientY - rect.top - panY; - - const node = getCanvasNodeRaw(mx, my); - if (node) { - selectNode(node.id); - return; - } - + const node = getCanvasNodeAt(mx, my); + if (node) { selectNode(node.id); return; } const shape = getShapeAt(mx, my); - if (shape) { - selectedNodeId = null; - selectedShapeId = shape.id; - renderShapeList(); - renderNetwork(); - } -} - -function onContextMenu(e) { - e.preventDefault(); + if (shape) { selectedNodeId = null; selectedShapeId = shape.id; renderShapeList(); renderNetwork(); } } function esc(s) { @@ -764,7 +641,6 @@ function esc(s) { return div.innerHTML; } -// roundRect polyfill for older browsers if (!CanvasRenderingContext2D.prototype.roundRect) { CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { if (r > w / 2) r = w / 2;