diff --git a/frontend/assets/js/app.js b/frontend/assets/js/app.js index 4fd8fdc..6b0fe8d 100644 --- a/frontend/assets/js/app.js +++ b/frontend/assets/js/app.js @@ -3,41 +3,69 @@ let teams = []; let events = []; let nodes = []; let links = []; +let shapes = []; let selectedNodeId = null; +let selectedShapeId = null; -// Network canvas state +// Canvas state let canvas, ctx; let canvasNodes = []; let canvasLinks = []; -let isDragging = false; -let dragNode = null; -let offsetX, offsetY; +let canvasShapes = []; let panX = 0, panY = 0; let isPanning = false; let panStartX, panStartY; +// Drag state +let dragTarget = null; +let dragType = null; +let dragOffX, dragOffY; +let dragHandle = null; +let dragOrig = null; + +let nextShapeZ = 0; + document.addEventListener('DOMContentLoaded', () => { canvas = document.getElementById('networkCanvas'); ctx = canvas.getContext('2d'); resizeCanvas(); - loadTeams().then(() => { - loadEvents(); - }); + loadTeams().then(() => loadEvents()); loadNetworkData(); document.getElementById('saveEvent').addEventListener('click', saveEvent); document.getElementById('saveNode').addEventListener('click', saveNode); document.getElementById('saveLink').addEventListener('click', saveLink); + document.getElementById('saveShape').addEventListener('click', saveShape); document.getElementById('teamFilter').addEventListener('change', renderTimeline); document.getElementById('searchEvents').addEventListener('input', renderTimeline); + document.getElementById('shapeOpacity').addEventListener('input', (e) => { + document.getElementById('opacityVal').textContent = parseFloat(e.target.value).toFixed(2); + }); - canvas.addEventListener('mousedown', onCanvasMouseDown); - canvas.addEventListener('mousemove', onCanvasMouseMove); - canvas.addEventListener('mouseup', onCanvasMouseUp); - canvas.addEventListener('dblclick', onCanvasDblClick); + canvas.addEventListener('mousedown', onMouseDown); + canvas.addEventListener('mousemove', onMouseMove); + canvas.addEventListener('mouseup', onMouseUp); + canvas.addEventListener('dblclick', onDblClick); + canvas.addEventListener('contextmenu', onContextMenu); 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 (selectedNodeId) deleteSelectedNode(); + else if (selectedShapeId) deleteSelectedShape(); + } + } + if (e.key === 'Escape') { + selectedNodeId = null; + selectedShapeId = null; + renderNetwork(); + renderNodeList(); + } + }); + document.querySelectorAll('[data-bs-toggle="tab"]').forEach(tab => { tab.addEventListener('shown.bs.tab', () => { if (tab.id === 'network-tab') { @@ -47,7 +75,6 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // Bootstrap dark mode default document.documentElement.setAttribute('data-bs-theme', 'dark'); }); @@ -57,7 +84,6 @@ function resizeCanvas() { canvas.height = wrapper.clientHeight; } -// ==================== API HELPERS ==================== async function apiFetch(path, options = {}) { const res = await fetch(API + path, { headers: { 'Content-Type': 'application/json' }, @@ -128,7 +154,7 @@ function renderTimeline() { ${renderComments(e)}
- +
@@ -186,10 +212,18 @@ async function saveEvent() { // ==================== NETWORK MAP ==================== async function loadNetworkData() { - nodes = await apiFetch('nodes'); - links = await apiFetch('links'); + const [n, l, s] = await Promise.all([ + apiFetch('nodes'), + apiFetch('links'), + apiFetch('shapes') + ]); + nodes = n; + links = l; + shapes = s; populateNodeSelects(); renderNodeList(); + renderShapeList(); + if (shapes.length) nextShapeZ = Math.max(...shapes.map(s => s.z_index)) + 1; renderNetwork(); } @@ -207,15 +241,31 @@ function renderNodeList() {
${esc(n.label)} -
${n.ip_address || '—'} · ${n.node_type} · ${n.group_name}
+
${n.ip_address || '—'} · ${n.node_type}
`).join(''); } +function renderShapeList() { + const list = document.getElementById('shapeList'); + list.innerHTML = shapes.map(s => ` +
+
+
+ + ${esc(s.label) || (s.shape_type === 'rectangle' ? 'Box' : 'Ellipse')} +
+ ${s.shape_type} +
+
+ `).join(''); +} + function selectNode(id) { selectedNodeId = id; + selectedShapeId = null; const n = nodes.find(x => x.id == id); if (n) { document.getElementById('nodeDetails').innerHTML = ` @@ -225,13 +275,41 @@ function selectNode(id) {
Type: ${n.node_type}
Status: ${n.status}
Group: ${n.group_name}
+ `; } renderNodeList(); + renderShapeList(); renderNetwork(); } +function selectShape(id) { + selectedShapeId = id; + selectedNodeId = null; + renderNodeList(); + renderShapeList(); + renderNetwork(); +} + +async function deleteSelectedNode() { + if (!selectedNodeId) return; + if (!confirm('Delete this node and its connections?')) return; + const id = selectedNodeId; + selectedNodeId = null; + await apiFetch(`nodes/${id}`, { method: 'DELETE' }); + loadNetworkData(); +} + +async function deleteSelectedShape() { + if (!selectedShapeId) return; + if (!confirm('Delete this shape?')) return; + const id = selectedShapeId; + selectedShapeId = null; + await apiFetch(`shapes/${id}`, { method: 'DELETE' }); + loadNetworkData(); +} + async function saveNode() { const data = { label: document.getElementById('nodeLabel').value, @@ -239,8 +317,8 @@ async function saveNode() { node_type: document.getElementById('nodeType').value, status: document.getElementById('nodeStatus').value, group_name: document.getElementById('nodeGroup').value || 'default', - pos_x: Math.random() * canvas.width * 0.6 + canvas.width * 0.2, - pos_y: Math.random() * canvas.height * 0.6 + canvas.height * 0.2 + pos_x: Math.random() * canvas.width * 0.6 + canvas.width * 0.2 - panX, + pos_y: Math.random() * canvas.height * 0.6 + canvas.height * 0.2 - panY }; if (!data.label) return alert('Label required'); await apiFetch('nodes', { method: 'POST', body: JSON.stringify(data) }); @@ -263,19 +341,55 @@ async function saveLink() { loadNetworkData(); } +async function saveShape() { + const data = { + label: document.getElementById('shapeLabel').value, + shape_type: document.getElementById('shapeType').value, + pos_x: canvas.width / 2 - 100 - panX, + pos_y: canvas.height / 2 - 75 - panY, + width: 200, + height: 150, + color: document.getElementById('shapeColor').value, + border_color: document.getElementById('shapeBorderColor').value, + opacity: parseFloat(document.getElementById('shapeOpacity').value), + z_index: nextShapeZ++ + }; + await apiFetch('shapes', { method: 'POST', body: JSON.stringify(data) }); + bootstrap.Modal.getInstance(document.getElementById('shapeModal')).hide(); + document.getElementById('shapeForm').reset(); + loadNetworkData(); +} + // ==================== 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'] +}; + function buildCanvasGraph() { - canvasNodes = nodes.map(n => ({ - 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) || (Math.random() * canvas.width * 0.6 + canvas.width * 0.2), - y: parseFloat(n.pos_y) || (Math.random() * canvas.height * 0.6 + canvas.height * 0.2), - radius: n.node_type === 'router' || n.node_type === 'firewall' ? 22 : n.node_type === 'server' ? 28 : 18 - })); + canvasNodes = nodes.map(n => { + const icon = NODE_ICONS[n.node_type] || NODE_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 + }; + }); canvasLinks = links.map(l => ({ source: canvasNodes.find(n => n.id == l.source_id), @@ -283,15 +397,20 @@ function buildCanvasGraph() { type: l.link_type, label: l.label })).filter(l => l.source && l.target); -} -function getNodeColor(node) { - const colors = { - host: '#3b82f6', server: '#8b5cf6', router: '#f59e0b', - firewall: '#ef4444', switch: '#06b6d4', cloud: '#22c55e', - endpoint: '#ec4899', other: '#6b7280' - }; - return colors[node.type] || '#6b7280'; + 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) + })); } function renderNetwork() { @@ -300,6 +419,9 @@ 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(); @@ -308,120 +430,264 @@ function renderNetwork() { const colors = { direct: '#334155', vpn: '#eab308', wireless: '#22c55e', monitored: '#3b82f6' }; ctx.strokeStyle = colors[l.type] || '#334155'; - ctx.lineWidth = l.type === 'vpn' ? 2.5 : l.type === 'monitored' ? 1.5 : 1.5; + ctx.lineWidth = l.type === 'vpn' ? 2.5 : 1.5; + + if (l.type === 'vpn' || l.type === 'wireless') ctx.setLineDash([6, 4]); + else ctx.setLineDash([]); - if (l.type === 'vpn' || l.type === 'wireless') { - ctx.setLineDash([6, 4]); - } else { - ctx.setLineDash([]); - } ctx.stroke(); ctx.setLineDash([]); - // Link label 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 - 6); + ctx.fillText(l.label, mx, my - 8); } }); // Draw nodes - canvasNodes.forEach(n => { - const isSelected = selectedNodeId == n.id; - const color = getNodeColor(n); + canvasNodes.forEach(drawCanvasNode); + ctx.restore(); +} - // Glow for selected - if (isSelected) { - ctx.shadowColor = color; - ctx.shadowBlur = 20; - } +function drawShape(s) { + ctx.save(); + const isSelected = selectedShapeId == s.id; - // Shape by type + ctx.globalAlpha = s.opacity; + + if (s.type === 'ellipse') { ctx.beginPath(); - if (n.type === 'router' || n.type === 'firewall') { - // Diamond - ctx.moveTo(n.x, n.y - n.radius); - ctx.lineTo(n.x + n.radius, n.y); - ctx.lineTo(n.x, n.y + n.radius); - ctx.lineTo(n.x - n.radius, n.y); - ctx.closePath(); - } else if (n.type === 'cloud') { - // Cloud-like - ctx.arc(n.x, n.y, n.radius, 0, Math.PI * 2); - } else if (n.type === 'server') { - // Square - ctx.rect(n.x - n.radius * 0.8, n.y - n.radius * 0.8, n.radius * 1.6, n.radius * 1.6); - } else { - // Circle (host, endpoint, etc) - ctx.arc(n.x, n.y, n.radius, 0, Math.PI * 2); - } - - ctx.fillStyle = color + '30'; + 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.strokeStyle = isSelected ? '#fff' : color; - ctx.lineWidth = isSelected ? 3 : 2; + ctx.globalAlpha = 1; + ctx.strokeStyle = isSelected ? '#ffffff' : s.borderColor; + ctx.lineWidth = isSelected ? 2.5 : 1.5; + ctx.setLineDash([5, 3]); ctx.stroke(); - - // Status indicator - ctx.shadowBlur = 0; + ctx.setLineDash([]); + } else { ctx.beginPath(); - ctx.arc(n.x + n.radius * 0.6, n.y - n.radius * 0.6, 5, 0, Math.PI * 2); - const statusColors = { online: '#22c55e', offline: '#6b7280', unknown: '#9ca3af', compromised: '#ef4444', monitoring: '#eab308' }; - ctx.fillStyle = statusColors[n.status] || '#9ca3af'; + 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([]); + } - // Label - ctx.fillStyle = '#e2e8f0'; - ctx.font = isSelected ? 'bold 11px sans-serif' : '10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(n.label, n.x, n.y + n.radius + 14); - if (n.ip) { - ctx.fillStyle = '#64748b'; - ctx.font = '9px sans-serif'; - ctx.fillText(n.ip, n.x, n.y + n.radius + 26); - } - }); + // Resize handles + if (isSelected) { + const handles = getShapeHandles(s); + handles.forEach(h => { + ctx.beginPath(); + ctx.arc(h.x, h.y, 5, 0, Math.PI * 2); + ctx.fillStyle = '#ffffff'; + ctx.fill(); + ctx.strokeStyle = '#3b82f6'; + ctx.lineWidth = 2; + ctx.stroke(); + }); + } + + // 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.restore(); } +function drawCanvasNode(n) { + const isSelected = selectedNodeId == n.id; + const s = 1.8; + + ctx.save(); + if (isSelected) { + ctx.shadowColor = n.color; + ctx.shadowBlur = 18; + } + + // Fill background + ctx.translate(n.x, n.y); + ctx.scale(1, 1); + 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.fillStyle = n.color + '18'; + ctx.fill(); + + // Border + ctx.strokeStyle = isSelected ? '#ffffff' : n.color; + ctx.lineWidth = isSelected ? 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); + const path = new Path2D(scaled); + ctx.fillStyle = n.color; + ctx.fill(path); + + // 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'; + ctx.fill(); + if (n.status === 'compromised') { + 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.textAlign = 'center'; + ctx.fillText(n.label, n.x, n.y + 34); + if (n.ip) { + ctx.fillStyle = '#64748b'; + 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 } + ]; +} + // ==================== CANVAS EVENTS ==================== -function getCanvasNode(mx, my) { +function getCanvasNodeRaw(mx, my) { return canvasNodes.find(n => { const dx = mx - n.x, dy = my - n.y; - return Math.sqrt(dx * dx + dy * dy) < n.radius + 8; + return Math.sqrt(dx * dx + dy * dy) < 28; }); } -function onCanvasMouseDown(e) { +function getShapeAt(mx, my) { + for (let i = canvasShapes.length - 1; i >= 0; i--) { + const s = canvasShapes[i]; + if (mx >= s.x && mx <= s.x + s.w && my >= s.y && my <= s.y + s.h) return s; + } + return null; +} + +function getShapeResizeHandle(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 }; + } + } + return null; +} + +function onMouseDown(e) { const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left - panX; const my = e.clientY - rect.top - panY; - const node = getCanvasNode(mx, my); - if (node) { - isDragging = true; - dragNode = node; - offsetX = mx - node.x; - offsetY = my - node.y; - canvas.style.cursor = 'grabbing'; - } else { - isPanning = true; - panStartX = e.clientX - panX; - panStartY = e.clientY - panY; - canvas.style.cursor = 'grabbing'; + // Check shape resize handle first (only for selected shape) + const resize = getShapeResizeHandle(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; + return; } + + // Check node + const node = getCanvasNodeRaw(mx, my); + if (node) { + 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(); + return; + } + + // Pan + selectedNodeId = null; + selectedShapeId = null; + renderNodeList(); + renderShapeList(); + isPanning = true; + panStartX = e.clientX - panX; + panStartY = e.clientY - panY; + canvas.style.cursor = 'grabbing'; } -function onCanvasMouseMove(e) { +function onMouseMove(e) { const rect = canvas.getBoundingClientRect(); - if (isDragging && dragNode) { - dragNode.x = e.clientX - rect.left - panX - offsetX; - dragNode.y = e.clientY - rect.top - panY - offsetY; + + if (dragType === 'node' && dragTarget) { + dragTarget.x = e.clientX - rect.left - panX - dragOffX; + dragTarget.y = e.clientY - rect.top - panY - dragOffY; + renderNetwork(); + } else if (dragType === 'shape' && dragTarget) { + dragTarget.x = e.clientX - rect.left - panX - dragOffX; + dragTarget.y = e.clientY - rect.top - panY - dragOffY; + renderNetwork(); + } else if (dragType === 'resize' && dragTarget && dragHandle) { + 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; renderNetwork(); } else if (isPanning) { panX = e.clientX - panStartX; @@ -430,32 +696,65 @@ function onCanvasMouseMove(e) { } else { const mx = e.clientX - rect.left - panX; const my = e.clientY - rect.top - panY; - canvas.style.cursor = getCanvasNode(mx, my) ? 'pointer' : 'grab'; + 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'; + else canvas.style.cursor = 'grab'; } } -function onCanvasMouseUp(e) { - if (isDragging && dragNode) { - // Save position - apiFetch(`nodes/${dragNode.id}`, { - method: 'PUT', - body: JSON.stringify({ pos_x: dragNode.x, pos_y: dragNode.y }) - }); +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 + }) + }); + } } - isDragging = false; - dragNode = null; + + dragType = null; + dragTarget = null; + dragHandle = null; + dragOrig = null; isPanning = false; canvas.style.cursor = 'grab'; } -function onCanvasDblClick(e) { +function onDblClick(e) { const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left - panX; const my = e.clientY - rect.top - panY; - const node = getCanvasNode(mx, my); + + const node = getCanvasNodeRaw(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(); } function esc(s) { @@ -463,4 +762,22 @@ function esc(s) { const div = document.createElement('div'); div.textContent = 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; + if (r > h / 2) r = h / 2; + this.moveTo(x + r, y); + this.lineTo(x + w - r, y); + this.quadraticCurveTo(x + w, y, x + w, y + r); + this.lineTo(x + w, y + h - r); + this.quadraticCurveTo(x + w, y + h, x + w - r, y + h); + this.lineTo(x + r, y + h); + this.quadraticCurveTo(x, y + h, x, y + h - r); + this.lineTo(x, y + r); + this.quadraticCurveTo(x, y, x + r, y); + return this; + }; } \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 3e82e7e..54ddc20 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,7 @@ Neptune - Cybersecurity Incident Journal - + @@ -13,7 +13,7 @@