diff --git a/frontend/assets/js/app.js b/frontend/assets/js/app.js
index 9806df0..3e505e4 100644
--- a/frontend/assets/js/app.js
+++ b/frontend/assets/js/app.js
@@ -732,7 +732,7 @@ async function loadNetworkData() {
renderNodeList();
renderShapeList();
renderNodeToolbar();
- setupCanvasDrop();
+ setupCanvasClickCreate();
startSync();
if (shapes.length) nextShapeZ = Math.max(...shapes.map(x => x.z_index)) + 1;
buildCanvasGraph();
@@ -1167,39 +1167,37 @@ function renderNodeToolbar() {
{ type: 'shape:rectangle', icon: 'fa-vector-square', color: '#3b82f6', label: 'Box', isShape: true },
{ type: 'shape:ellipse', icon: 'fa-circle', color: '#3b82f6', label: 'Ellipse', isShape: true },
];
- bar.innerHTML = 'Drag to canvas:';
+ bar.innerHTML = 'Click item then click canvas:';
iconMap.forEach(t => {
const el = document.createElement('span');
el.className = 'd-inline-flex align-items-center gap-1 px-2 py-1 rounded';
- el.draggable = true;
- el.style.cssText = 'cursor:grab;font-size:.75rem;color:' + t.color + ';background:' + t.color + '12;border:1px solid ' + t.color + '30;';
+ el.style.cssText = 'cursor:pointer;font-size:.75rem;color:' + t.color + ';background:' + t.color + '12;border:1px solid ' + t.color + '30;';
el.innerHTML = '' + t.label + '';
- el.addEventListener('dragstart', (e) => {
- e.dataTransfer.setData('text/plain', t.type);
- e.dataTransfer.effectAllowed = 'copy';
+ el.addEventListener('click', () => {
+ bar.querySelectorAll('.d-inline-flex').forEach(s => s.style.outline = 'none');
+ el.style.outline = '2px solid ' + t.color;
+ pendingCanvasCreate = t.type;
});
bar.appendChild(el);
});
}
-function setupCanvasDrop() {
+let pendingCanvasCreate = null;
+
+function setupCanvasClickCreate() {
const wrapper = document.getElementById('networkCanvasWrapper');
- let dropPending = false;
- wrapper.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });
- wrapper.addEventListener('drop', async (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (dropPending) return;
- dropPending = true;
- suppressNextMouse = Date.now() + 500;
- const data = e.dataTransfer.getData('text/plain');
- if (!data) { dropPending = false; return; }
+ const bar = document.getElementById('nodeToolbar');
+ wrapper.addEventListener('click', async (e) => {
+ if (!pendingCanvasCreate) return;
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left - panX;
const my = e.clientY - rect.top - panY;
+ const type = pendingCanvasCreate;
+ pendingCanvasCreate = null;
+ bar.querySelectorAll('.d-inline-flex').forEach(s => s.style.outline = 'none');
- if (data.startsWith('shape:')) {
- const shapeType = data.split(':')[1];
+ if (type.startsWith('shape:')) {
+ const shapeType = type.split(':')[1];
await apiFetch('shapes', { method: 'POST', body: JSON.stringify({
label: shapeType === 'rectangle' ? 'Box' : 'Ellipse',
shape_type: shapeType,
@@ -1210,67 +1208,16 @@ function setupCanvasDrop() {
})});
} else {
await apiFetch('nodes', { method: 'POST', body: JSON.stringify({
- label: data.charAt(0).toUpperCase() + data.slice(1),
- ip_address: '', node_type: data || 'host', status: 'unknown',
+ label: type.charAt(0).toUpperCase() + type.slice(1),
+ ip_address: '', node_type: type || 'host', status: 'unknown',
group_name: 'default', notes: '',
pos_x: mx, pos_y: my
})});
}
await loadNetworkData();
- dropPending = false;
});
}
-// Real-time sync: poll backend every 5 seconds for all data
-let syncInterval = null;
-let syncHashes = { events: null, documents: null, network: null };
-
-function startSync() {
- if (syncInterval) return;
- syncInterval = setInterval(async () => {
- if (dragType || dragTarget || isPanning) return;
- const openModals = document.querySelectorAll('.modal.show');
- try {
- const [eventsData, docsData, nodesData, linksData, shapesData] = await Promise.all([
- apiFetch('events'),
- apiFetch('documents'),
- apiFetch('nodes'),
- apiFetch('links'),
- apiFetch('shapes'),
- ]);
-
- const eventsHash = JSON.stringify(eventsData);
- if (eventsHash !== syncHashes.events) {
- syncHashes.events = eventsHash;
- events = Array.isArray(eventsData) ? eventsData : [];
- loadAllTags();
- renderTimeline();
- }
-
- const docsHash = JSON.stringify(docsData);
- if (docsHash !== syncHashes.documents) {
- syncHashes.documents = docsHash;
- documents = Array.isArray(docsData) ? docsData : [];
- renderDocuments();
- }
-
- const netHash = JSON.stringify({ n: nodesData, l: linksData, s: shapesData });
- if (netHash !== syncHashes.network) {
- syncHashes.network = netHash;
- nodes = Array.isArray(nodesData) ? nodesData : [];
- links = Array.isArray(linksData) ? linksData : [];
- shapes = Array.isArray(shapesData) ? shapesData : [];
- populateNodeSelects();
- renderNodeList();
- renderShapeList();
- if (shapes.length) nextShapeZ = Math.max(...shapes.map(x => x.z_index)) + 1;
- buildCanvasGraph();
- renderNetwork();
- }
- } catch (e) {}
- }, 5000);
-}
-
function getCanvasNodeAt(mx, my) {
return canvasNodes.find(n => Math.hypot(mx - n.x, my - n.y) < 28);
}
@@ -1416,10 +1363,14 @@ function onMouseUp(e) {
const rw = Math.abs(mx - selectStartX);
const rh = Math.abs(my - selectStartY);
if (rw > 5 || rh > 5) {
- const found = canvasNodes.filter(n => n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh);
- selectedNodeIds = found.map(n => n.id);
- selectedNodeId = found.length > 0 ? found[0].id : null;
- selectedShapeId = null;
+ const foundNodes = canvasNodes.filter(n => n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh);
+ const foundShapes = canvasShapes.filter(s => {
+ const sx = s.x, sy = s.y, ex = s.x + s.w, ey = s.y + s.h;
+ return sx < rx + rw && ex > rx && sy < ry + rh && ey > ry;
+ });
+ selectedNodeIds = foundNodes.map(n => n.id);
+ selectedNodeId = foundNodes.length > 0 ? foundNodes[0].id : null;
+ selectedShapeId = foundShapes.length > 0 ? foundShapes[foundShapes.length - 1].id : null;
renderNodeList(); renderShapeList(); renderNetwork();
}
}