+166
-263
@@ -29,7 +29,77 @@ let copyBuffer = null;
|
|||||||
let editingNodeId = null;
|
let editingNodeId = null;
|
||||||
let editingShapeId = null;
|
let editingShapeId = null;
|
||||||
|
|
||||||
|
// ==================== AUTH / SESSION ====================
|
||||||
|
let currentUser = null;
|
||||||
|
let currentRole = null;
|
||||||
|
|
||||||
|
async function checkSession() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/session');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.loggedin) {
|
||||||
|
currentUser = data.username;
|
||||||
|
currentRole = data.role;
|
||||||
|
document.getElementById('userDisplay').textContent = data.username;
|
||||||
|
if (data.role === 'admin') document.getElementById('settingsBtn').classList.remove('d-none');
|
||||||
|
document.getElementById('loginOverlay').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
document.getElementById('loginOverlay').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performLogin(authToken) {
|
||||||
|
const errEl = document.getElementById('loginError');
|
||||||
|
const sucEl = document.getElementById('loginSuccess');
|
||||||
|
errEl.style.display = 'none';
|
||||||
|
sucEl.style.display = 'none';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ auth_token: authToken })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.status === 'success') {
|
||||||
|
currentUser = data.username;
|
||||||
|
currentRole = data.role;
|
||||||
|
document.getElementById('userDisplay').textContent = data.username;
|
||||||
|
if (data.role === 'admin') document.getElementById('settingsBtn').classList.remove('d-none');
|
||||||
|
document.getElementById('loginOverlay').style.display = 'none';
|
||||||
|
window.history.replaceState({}, '', '/');
|
||||||
|
loadTeams().then(() => loadEvents());
|
||||||
|
loadNetworkData();
|
||||||
|
} else {
|
||||||
|
errEl.textContent = data.error || 'Login failed';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errEl.textContent = 'Connection error';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await apiFetch('logout', { method: 'POST' });
|
||||||
|
currentUser = null;
|
||||||
|
currentRole = null;
|
||||||
|
document.getElementById('settingsBtn').classList.add('d-none');
|
||||||
|
document.getElementById('userDisplay').textContent = '';
|
||||||
|
document.getElementById('loginOverlay').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const authToken = params.get('auth');
|
||||||
|
if (authToken) {
|
||||||
|
document.getElementById('loginOverlay').style.display = 'flex';
|
||||||
|
const btn = document.querySelector('#loginOverlay .btn');
|
||||||
|
if (btn) btn.textContent = 'Authenticating...';
|
||||||
|
performLogin(authToken);
|
||||||
|
}
|
||||||
|
|
||||||
checkSession().then(() => {
|
checkSession().then(() => {
|
||||||
canvas = document.getElementById('networkCanvas');
|
canvas = document.getElementById('networkCanvas');
|
||||||
ctx = canvas.getContext('2d');
|
ctx = canvas.getContext('2d');
|
||||||
@@ -38,6 +108,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
loadTeams().then(() => loadEvents());
|
loadTeams().then(() => loadEvents());
|
||||||
loadNetworkData();
|
loadNetworkData();
|
||||||
|
|
||||||
|
document.getElementById('loginBtn').addEventListener('click', () => {
|
||||||
|
const callbackUrl = window.location.origin + '/?auth_callback=1';
|
||||||
|
window.location.href = 'https://auth.jakach.ch/?send_to=' + encodeURIComponent(callbackUrl);
|
||||||
|
});
|
||||||
document.getElementById('saveEvent').addEventListener('click', saveEvent);
|
document.getElementById('saveEvent').addEventListener('click', saveEvent);
|
||||||
document.getElementById('saveNode').addEventListener('click', saveNode);
|
document.getElementById('saveNode').addEventListener('click', saveNode);
|
||||||
document.getElementById('saveLink').addEventListener('click', saveLink);
|
document.getElementById('saveLink').addEventListener('click', saveLink);
|
||||||
@@ -88,7 +162,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
selectedNodeId = null;
|
selectedNodeId = null; selectedNodeIds = [];
|
||||||
selectedShapeId = null;
|
selectedShapeId = null;
|
||||||
renderNetwork();
|
renderNetwork();
|
||||||
renderNodeList();
|
renderNodeList();
|
||||||
@@ -120,7 +194,6 @@ async function apiFetch(path, options = {}) {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== TEAMS ====================
|
|
||||||
async function loadTeams() {
|
async function loadTeams() {
|
||||||
teams = await apiFetch('teams');
|
teams = await apiFetch('teams');
|
||||||
const sel = document.getElementById('eventTeam');
|
const sel = document.getElementById('eventTeam');
|
||||||
@@ -133,7 +206,6 @@ async function loadTeams() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== EVENTS ====================
|
|
||||||
async function loadEvents() {
|
async function loadEvents() {
|
||||||
events = await apiFetch('events');
|
events = await apiFetch('events');
|
||||||
renderTimeline();
|
renderTimeline();
|
||||||
@@ -152,7 +224,7 @@ function renderTimeline() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!filtered.length) {
|
if (!filtered.length) {
|
||||||
container.innerHTML = `<div class="text-center text-secondary py-5"><i class="fas fa-book-open fs-1 mb-2"></i><p>No events yet. Create your first incident entry!</p></div>`;
|
container.innerHTML = '<div class="text-center text-secondary py-5"><i class="fas fa-book-open fs-1 mb-2"></i><p>No events yet. Create your first incident entry!</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,12 +244,12 @@ function renderTimeline() {
|
|||||||
<small class="event-meta">${date}</small>
|
<small class="event-meta">${date}</small>
|
||||||
</div>
|
</div>
|
||||||
<h6 class="event-title mt-1 mb-1">${esc(e.title)}</h6>
|
<h6 class="event-title mt-1 mb-1">${esc(e.title)}</h6>
|
||||||
${e.description ? `<p class="mb-1 small text-secondary">${esc(e.description)}</p>` : ''}
|
${e.description ? '<p class="mb-1 small text-secondary">' + esc(e.description) + '</p>' : ''}
|
||||||
<div class="mt-2" id="comments-${e.id}">
|
<div class="mt-2" id="comments-${e.id}">
|
||||||
<div class="d-flex align-items-center mb-1"><small class="text-secondary fw-bold"><i class="fas fa-comment-dots me-1"></i>Comments ${e.comments && e.comments.length ? `(${e.comments.length})` : ''}</small></div>
|
<div class="d-flex align-items-center mb-1"><small class="text-secondary fw-bold"><i class="fas fa-comment-dots me-1"></i>Comments ${e.comments && e.comments.length ? '(' + e.comments.length + ')' : ''}</small></div>
|
||||||
<div class="comment-log">${renderComments(e)}</div>
|
<div class="comment-log">${renderComments(e)}</div>
|
||||||
<div class="input-group input-group-sm comment-input-group mt-1">
|
<div class="input-group input-group-sm comment-input-group mt-1">
|
||||||
<input type="text" class="form-control form-control-sm comment-input" placeholder="Write a comment..." onkeydown="if(event.key==='Enter') addComment(${e.id}, this)">
|
<input type="text" class="form-control form-control-sm comment-input" placeholder="Write a comment..." onkeydown="if(event.key===\'Enter\') addComment(${e.id}, this)">
|
||||||
<button class="btn btn-outline-secondary btn-sm" onclick="addComment(${e.id}, this)"><i class="fas fa-paper-plane"></i></button>
|
<button class="btn btn-outline-secondary btn-sm" onclick="addComment(${e.id}, this)"><i class="fas fa-paper-plane"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,48 +321,47 @@ async function loadNetworkData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function populateNodeSelects() {
|
function populateNodeSelects() {
|
||||||
const html = nodes.map(n => `<option value="${n.id}">${esc(n.label)} (${n.ip_address || 'no IP'})</option>`).join('');
|
const html = nodes.map(n => '<option value="' + n.id + '">' + esc(n.label) + ' (' + (n.ip_address || 'no IP') + ')</option>').join('');
|
||||||
document.getElementById('linkSource').innerHTML = html;
|
document.getElementById('linkSource').innerHTML = html;
|
||||||
document.getElementById('linkTarget').innerHTML = html;
|
document.getElementById('linkTarget').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNodeList() {
|
function renderNodeList() {
|
||||||
const list = document.getElementById('nodeList');
|
const list = document.getElementById('nodeList');
|
||||||
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' };
|
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 `
|
list.innerHTML = nodes.map(n => {
|
||||||
<div class="list-group-item bg-dark border-secondary node-list-item py-2 ${selectedNodeIds.includes(n.id) ? 'active' : ''}" onclick="selectNode(${n.id}, event.shiftKey)">
|
return '<div class="list-group-item bg-dark border-secondary node-list-item py-2 ' + (selectedNodeIds.includes(n.id) ? 'active' : '') + '" onclick="selectNode(' + n.id + ', event.shiftKey)">' +
|
||||||
<div class="d-flex align-items-center">
|
'<div class="d-flex align-items-center">' +
|
||||||
<span class="status-dot status-${n.status}"></span>
|
'<span class="status-dot status-' + n.status + '"></span>' +
|
||||||
<i class="fas ${iconMap[n.node_type] || 'fa-circle'} me-2" style="color:${getNodeColorVal(n.node_type)};font-size:.85rem;"></i>
|
'<i class="fas ' + (iconMap[n.node_type] || 'fa-circle') + ' me-2" style="color:' + getNodeColorVal(n.node_type) + ';font-size:.85rem;"></i>' +
|
||||||
<div>
|
'<div>' +
|
||||||
<strong class="small">${esc(n.label)}</strong>
|
'<strong class="small">' + esc(n.label) + '</strong>' +
|
||||||
<div class="text-secondary" style="font-size:.7rem;">${n.ip_address || '—'} · ${n.node_type}</div>
|
'<div class="text-secondary" style="font-size:.7rem;">' + (n.ip_address || '—') + ' · ' + n.node_type + '</div>' +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>`;
|
'</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderShapeList() {
|
function renderShapeList() {
|
||||||
const list = document.getElementById('shapeList');
|
const list = document.getElementById('shapeList');
|
||||||
list.innerHTML = shapes.map(s => `
|
list.innerHTML = shapes.map(s => {
|
||||||
<div class="list-group-item bg-dark border-secondary py-2 ${selectedShapeId == s.id ? 'active' : ''}" onclick="selectShape(${s.id})">
|
return '<div class="list-group-item bg-dark border-secondary py-2 ' + (selectedShapeId == s.id ? 'active' : '') + '" onclick="selectShape(' + s.id + ')">' +
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
'<div class="d-flex align-items-center justify-content-between">' +
|
||||||
<div>
|
'<div>' +
|
||||||
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:${s.border_color};margin-right:.4rem;"></span>
|
'<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + s.border_color + ';margin-right:.4rem;"></span>' +
|
||||||
<strong class="small">${esc(s.label) || (s.shape_type === 'rectangle' ? 'Box' : 'Ellipse')}</strong>
|
'<strong class="small">' + esc(s.label || (s.shape_type === 'rectangle' ? 'Box' : 'Ellipse')) + '</strong>' +
|
||||||
</div>
|
'</div>' +
|
||||||
<div>
|
'<div>' +
|
||||||
<small class="text-secondary me-2">${s.shape_type}</small>
|
'<small class="text-secondary me-2">' + s.shape_type + '</small>' +
|
||||||
<button class="btn btn-sm btn-outline-primary py-0 px-1" onclick="event.stopPropagation();editSelectedShape(${s.id})" title="Edit"><i class="fas fa-pen" style="font-size:.7rem;"></i></button>
|
'<button class="btn btn-sm btn-outline-primary py-0 px-1" onclick="event.stopPropagation();editSelectedShape(' + s.id + ')" title="Edit"><i class="fas fa-pen" style="font-size:.7rem;"></i></button>' +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>
|
'</div>';
|
||||||
`).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectNode(id, add = false) {
|
function selectNode(id, add) {
|
||||||
if (add) {
|
if (add) {
|
||||||
const idx = selectedNodeIds.indexOf(id);
|
const idx = selectedNodeIds.indexOf(id);
|
||||||
if (idx >= 0) { selectedNodeIds.splice(idx, 1); if (selectedNodeIds.length === 0) selectedNodeId = null; else selectedNodeId = selectedNodeIds[0]; }
|
if (idx >= 0) { selectedNodeIds.splice(idx, 1); if (selectedNodeIds.length === 0) selectedNodeId = null; else selectedNodeId = selectedNodeIds[0]; }
|
||||||
@@ -302,20 +373,19 @@ function selectNode(id, add = false) {
|
|||||||
selectedShapeId = null;
|
selectedShapeId = null;
|
||||||
const n = nodes.find(x => x.id == id);
|
const n = nodes.find(x => x.id == id);
|
||||||
if (n) {
|
if (n) {
|
||||||
document.getElementById('nodeDetails').innerHTML = `
|
document.getElementById('nodeDetails').innerHTML =
|
||||||
<div class="small">
|
'<div class="small">' +
|
||||||
<div class="d-flex align-items-center mb-1"><span class="status-dot status-${n.status} me-2"></span><strong>${esc(n.label)}</strong></div>
|
'<div class="d-flex align-items-center mb-1"><span class="status-dot status-' + n.status + ' me-2"></span><strong>' + esc(n.label) + '</strong></div>' +
|
||||||
<div><span class="text-secondary">IP:</span> ${n.ip_address || '—'}</div>
|
'<div><span class="text-secondary">IP:</span> ' + (n.ip_address || '—') + '</div>' +
|
||||||
<div><span class="text-secondary">Type:</span> ${n.node_type}</div>
|
'<div><span class="text-secondary">Type:</span> ' + n.node_type + '</div>' +
|
||||||
<div><span class="text-secondary">Status:</span> ${n.status}</div>
|
'<div><span class="text-secondary">Status:</span> ' + n.status + '</div>' +
|
||||||
<div><span class="text-secondary">Group:</span> ${n.group_name}</div>
|
'<div><span class="text-secondary">Group:</span> ' + n.group_name + '</div>' +
|
||||||
${selectedNodeIds.length > 1 ? `<small class="text-secondary">+${selectedNodeIds.length - 1} more selected</small>` : ''}
|
(selectedNodeIds.length > 1 ? '<small class="text-secondary">+' + (selectedNodeIds.length - 1) + ' more selected</small>' : '') +
|
||||||
<div class="mt-2 d-flex gap-1">
|
'<div class="mt-2 d-flex gap-1">' +
|
||||||
<button class="btn btn-outline-primary btn-sm" onclick="editSelectedNode(${n.id})"><i class="fas fa-pen me-1"></i>Edit</button>
|
'<button class="btn btn-outline-primary btn-sm" onclick="editSelectedNode(' + n.id + ')"><i class="fas fa-pen me-1"></i>Edit</button>' +
|
||||||
<button class="btn btn-outline-danger btn-sm" onclick="deleteSelectedNodes()"><i class="fas fa-trash me-1"></i>Delete</button>
|
'<button class="btn btn-outline-danger btn-sm" onclick="deleteSelectedNodes()"><i class="fas fa-trash me-1"></i>Delete</button>' +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>
|
'</div>';
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
renderNodeList();
|
renderNodeList();
|
||||||
renderShapeList();
|
renderShapeList();
|
||||||
@@ -334,11 +404,11 @@ function selectShape(id) {
|
|||||||
async function deleteSelectedNodes() {
|
async function deleteSelectedNodes() {
|
||||||
const ids = [...selectedNodeIds];
|
const ids = [...selectedNodeIds];
|
||||||
if (!ids.length) return;
|
if (!ids.length) return;
|
||||||
const ok = await showConfirm(`Delete ${ids.length} node(s) and their connections?`);
|
const ok = await showConfirm('Delete ' + ids.length + ' node(s) and their connections?');
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
selectedNodeId = null;
|
selectedNodeId = null;
|
||||||
selectedNodeIds = [];
|
selectedNodeIds = [];
|
||||||
for (const id of ids) await apiFetch(`nodes/${id}`, { method: 'DELETE' });
|
for (const id of ids) await apiFetch('nodes/' + id, { method: 'DELETE' });
|
||||||
loadNetworkData();
|
loadNetworkData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,20 +417,20 @@ async function deleteSelectedShape(id) {
|
|||||||
const ok = await showConfirm('Delete this shape?');
|
const ok = await showConfirm('Delete this shape?');
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
selectedShapeId = null;
|
selectedShapeId = null;
|
||||||
await apiFetch(`shapes/${id}`, { method: 'DELETE' });
|
await apiFetch('shapes/' + id, { method: 'DELETE' });
|
||||||
loadNetworkData();
|
loadNetworkData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyNode(id) {
|
function copyNode(id) {
|
||||||
const n = nodes.find(x => x.id == id);
|
const n = nodes.find(x => x.id == id);
|
||||||
if (!n) return;
|
if (!n) return;
|
||||||
copyBuffer = { type: 'node', data: { ...n } };
|
copyBuffer = { type: 'node', data: Object.assign({}, n) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyShape(id) {
|
function copyShape(id) {
|
||||||
const s = shapes.find(x => x.id == id);
|
const s = shapes.find(x => x.id == id);
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
copyBuffer = { type: 'shape', data: { ...s } };
|
copyBuffer = { type: 'shape', data: Object.assign({}, s) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pasteItem() {
|
async function pasteItem() {
|
||||||
@@ -368,36 +438,23 @@ async function pasteItem() {
|
|||||||
const offset = 30;
|
const offset = 30;
|
||||||
if (copyBuffer.type === 'node') {
|
if (copyBuffer.type === 'node') {
|
||||||
const d = copyBuffer.data;
|
const d = copyBuffer.data;
|
||||||
await apiFetch('nodes', {
|
await apiFetch('nodes', { method: 'POST', body: JSON.stringify({
|
||||||
method: 'POST',
|
label: d.label + ' (copy)', ip_address: d.ip_address,
|
||||||
body: JSON.stringify({
|
node_type: d.node_type, status: d.status, group_name: d.group_name,
|
||||||
label: d.label + ' (copy)',
|
|
||||||
ip_address: d.ip_address,
|
|
||||||
node_type: d.node_type,
|
|
||||||
status: d.status,
|
|
||||||
group_name: d.group_name,
|
|
||||||
pos_x: (parseFloat(d.pos_x) || 100) + offset,
|
pos_x: (parseFloat(d.pos_x) || 100) + offset,
|
||||||
pos_y: (parseFloat(d.pos_y) || 100) + offset
|
pos_y: (parseFloat(d.pos_y) || 100) + offset
|
||||||
})
|
})});
|
||||||
});
|
|
||||||
loadNetworkData();
|
loadNetworkData();
|
||||||
} else if (copyBuffer.type === 'shape') {
|
} else if (copyBuffer.type === 'shape') {
|
||||||
const d = copyBuffer.data;
|
const d = copyBuffer.data;
|
||||||
await apiFetch('shapes', {
|
await apiFetch('shapes', { method: 'POST', body: JSON.stringify({
|
||||||
method: 'POST',
|
label: d.label + ' (copy)', shape_type: d.shape_type,
|
||||||
body: JSON.stringify({
|
|
||||||
label: d.label + ' (copy)',
|
|
||||||
shape_type: d.shape_type,
|
|
||||||
pos_x: (parseFloat(d.pos_x) || 100) + offset,
|
pos_x: (parseFloat(d.pos_x) || 100) + offset,
|
||||||
pos_y: (parseFloat(d.pos_y) || 100) + offset,
|
pos_y: (parseFloat(d.pos_y) || 100) + offset,
|
||||||
width: d.width || 200,
|
width: d.width || 200, height: d.height || 150,
|
||||||
height: d.height || 150,
|
color: d.color || '#1e3a5f', border_color: d.border_color || '#3b82f6',
|
||||||
color: d.color || '#1e3a5f',
|
opacity: parseFloat(d.opacity) || 0.15, z_index: nextShapeZ++
|
||||||
border_color: d.border_color || '#3b82f6',
|
})});
|
||||||
opacity: parseFloat(d.opacity) || 0.15,
|
|
||||||
z_index: nextShapeZ++
|
|
||||||
})
|
|
||||||
});
|
|
||||||
loadNetworkData();
|
loadNetworkData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,14 +487,10 @@ async function saveNode() {
|
|||||||
};
|
};
|
||||||
if (!data.label) return alert('Label required');
|
if (!data.label) return alert('Label required');
|
||||||
if (editingNodeId) {
|
if (editingNodeId) {
|
||||||
const updates = {
|
await apiFetch('nodes/' + editingNodeId, { method: 'PUT', body: JSON.stringify({
|
||||||
label: data.label,
|
label: data.label, ip_address: data.ip_address,
|
||||||
ip_address: data.ip_address,
|
node_type: data.node_type, status: data.status, group_name: data.group_name
|
||||||
node_type: data.node_type,
|
})});
|
||||||
status: data.status,
|
|
||||||
group_name: data.group_name
|
|
||||||
};
|
|
||||||
await apiFetch(`nodes/${editingNodeId}`, { method: 'PUT', body: JSON.stringify(updates) });
|
|
||||||
editingNodeId = null;
|
editingNodeId = null;
|
||||||
} else {
|
} else {
|
||||||
await apiFetch('nodes', { method: 'POST', body: JSON.stringify(data) });
|
await apiFetch('nodes', { method: 'POST', body: JSON.stringify(data) });
|
||||||
@@ -503,13 +556,12 @@ async function saveShape() {
|
|||||||
if (editingShapeId) {
|
if (editingShapeId) {
|
||||||
const s = shapes.find(x => x.id == editingShapeId);
|
const s = shapes.find(x => x.id == editingShapeId);
|
||||||
data.pos_x = s.pos_x; data.pos_y = s.pos_y; data.width = s.width; data.height = s.height; data.z_index = s.z_index;
|
data.pos_x = s.pos_x; data.pos_y = s.pos_y; data.width = s.width; data.height = s.height; data.z_index = s.z_index;
|
||||||
await apiFetch(`shapes/${editingShapeId}`, { method: 'PUT', body: JSON.stringify(data) });
|
await apiFetch('shapes/' + editingShapeId, { method: 'PUT', body: JSON.stringify(data) });
|
||||||
editingShapeId = null;
|
editingShapeId = null;
|
||||||
} else {
|
} else {
|
||||||
data.pos_x = canvas.width / 2 - 100 - panX;
|
data.pos_x = canvas.width / 2 - 100 - panX;
|
||||||
data.pos_y = canvas.height / 2 - 75 - panY;
|
data.pos_y = canvas.height / 2 - 75 - panY;
|
||||||
data.width = 200;
|
data.width = 200; data.height = 150;
|
||||||
data.height = 150;
|
|
||||||
data.z_index = nextShapeZ++;
|
data.z_index = nextShapeZ++;
|
||||||
await apiFetch('shapes', { method: 'POST', body: JSON.stringify(data) });
|
await apiFetch('shapes', { method: 'POST', body: JSON.stringify(data) });
|
||||||
}
|
}
|
||||||
@@ -525,7 +577,6 @@ function getNodeColorVal(type) {
|
|||||||
return c[type] || '#6b7280';
|
return c[type] || '#6b7280';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== CANVAS RENDERING ====================
|
|
||||||
const NODE_FA_ICONS = {
|
const NODE_FA_ICONS = {
|
||||||
host: { icon: '\uf108', color: '#3b82f6' },
|
host: { icon: '\uf108', color: '#3b82f6' },
|
||||||
server: { icon: '\uf233', color: '#8b5cf6' },
|
server: { icon: '\uf233', color: '#8b5cf6' },
|
||||||
@@ -540,20 +591,16 @@ const NODE_FA_ICONS = {
|
|||||||
function buildCanvasGraph() {
|
function buildCanvasGraph() {
|
||||||
canvasNodes = nodes.map(n => {
|
canvasNodes = nodes.map(n => {
|
||||||
const fa = NODE_FA_ICONS[n.node_type] || NODE_FA_ICONS.other;
|
const fa = NODE_FA_ICONS[n.node_type] || NODE_FA_ICONS.other;
|
||||||
return {
|
return { id: n.id, label: n.label, ip: n.ip_address, type: n.node_type,
|
||||||
id: n.id, label: n.label, ip: n.ip_address,
|
status: n.status, group: n.group_name,
|
||||||
type: n.node_type, status: n.status, group: n.group_name,
|
|
||||||
x: parseFloat(n.pos_x) || 100, y: parseFloat(n.pos_y) || 100,
|
x: parseFloat(n.pos_x) || 100, y: parseFloat(n.pos_y) || 100,
|
||||||
icon: fa.icon, color: fa.color, w: 36, h: 36
|
icon: fa.icon, color: fa.color, w: 36, h: 36 };
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
canvasLinks = links.map(l => ({
|
canvasLinks = links.map(l => ({
|
||||||
source: canvasNodes.find(n => n.id == l.source_id),
|
source: canvasNodes.find(n => n.id == l.source_id),
|
||||||
target: canvasNodes.find(n => n.id == l.target_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);
|
})).filter(l => l.source && l.target);
|
||||||
|
|
||||||
canvasShapes = shapes.map(s => ({
|
canvasShapes = shapes.map(s => ({
|
||||||
id: s.id, label: s.label, type: s.shape_type,
|
id: s.id, label: s.label, type: s.shape_type,
|
||||||
x: parseFloat(s.pos_x), y: parseFloat(s.pos_y),
|
x: parseFloat(s.pos_x), y: parseFloat(s.pos_y),
|
||||||
@@ -567,10 +614,7 @@ function renderNetwork() {
|
|||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(panX, panY);
|
ctx.translate(panX, panY);
|
||||||
|
|
||||||
// Use canvasShapes / canvasNodes / canvasLinks arrays directly (no rebuild)
|
|
||||||
canvasShapes.sort((a, b) => a.z - b.z).forEach(drawShape);
|
canvasShapes.sort((a, b) => a.z - b.z).forEach(drawShape);
|
||||||
|
|
||||||
canvasLinks.forEach(l => {
|
canvasLinks.forEach(l => {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(l.source.x, l.source.y);
|
ctx.moveTo(l.source.x, l.source.y);
|
||||||
@@ -589,10 +633,7 @@ function renderNetwork() {
|
|||||||
ctx.fillText(l.label, (l.source.x + l.target.x) / 2, (l.source.y + l.target.y) / 2 - 8);
|
ctx.fillText(l.label, (l.source.x + l.target.x) / 2, (l.source.y + l.target.y) / 2 - 8);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
canvasNodes.forEach(drawCanvasNode);
|
canvasNodes.forEach(drawCanvasNode);
|
||||||
|
|
||||||
// Selection rectangle
|
|
||||||
if (selectRect && (selectRect.w > 0 || selectRect.h > 0)) {
|
if (selectRect && (selectRect.w > 0 || selectRect.h > 0)) {
|
||||||
ctx.strokeStyle = '#3b82f6';
|
ctx.strokeStyle = '#3b82f6';
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
@@ -602,7 +643,6 @@ function renderNetwork() {
|
|||||||
ctx.fillRect(selectRect.x, selectRect.y, selectRect.w, selectRect.h);
|
ctx.fillRect(selectRect.x, selectRect.y, selectRect.w, selectRect.h);
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +650,6 @@ function drawShape(s) {
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
const sel = selectedShapeId == s.id;
|
const sel = selectedShapeId == s.id;
|
||||||
ctx.globalAlpha = s.opacity;
|
ctx.globalAlpha = s.opacity;
|
||||||
|
|
||||||
if (s.type === 'ellipse') {
|
if (s.type === 'ellipse') {
|
||||||
ctx.beginPath();
|
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.ellipse(s.x + s.w / 2, s.y + s.h / 2, s.w / 2, s.h / 2, 0, 0, Math.PI * 2);
|
||||||
@@ -626,7 +665,6 @@ function drawShape(s) {
|
|||||||
ctx.setLineDash([5, 3]);
|
ctx.setLineDash([5, 3]);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
if (sel) {
|
if (sel) {
|
||||||
getShapeHandles(s).forEach(h => {
|
getShapeHandles(s).forEach(h => {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -638,7 +676,6 @@ function drawShape(s) {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillStyle = '#94a3b8';
|
ctx.fillStyle = '#94a3b8';
|
||||||
ctx.font = '12px sans-serif';
|
ctx.font = '12px sans-serif';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
@@ -649,9 +686,7 @@ function drawShape(s) {
|
|||||||
function drawCanvasNode(n) {
|
function drawCanvasNode(n) {
|
||||||
const sel = selectedNodeIds.includes(n.id);
|
const sel = selectedNodeIds.includes(n.id);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
if (sel) { ctx.shadowColor = n.color; ctx.shadowBlur = 18; }
|
if (sel) { ctx.shadowColor = n.color; ctx.shadowBlur = 18; }
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(n.x, n.y, 22, 0, Math.PI * 2);
|
ctx.arc(n.x, n.y, 22, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = n.color + '18';
|
ctx.fillStyle = n.color + '18';
|
||||||
@@ -660,8 +695,6 @@ function drawCanvasNode(n) {
|
|||||||
ctx.lineWidth = sel ? 2.5 : 1.5;
|
ctx.lineWidth = sel ? 2.5 : 1.5;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
|
|
||||||
// Draw icon using Font Awesome
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = '900 22px "Font Awesome 6 Free", "FontAwesome", "Font Awesome 5 Free"';
|
ctx.font = '900 22px "Font Awesome 6 Free", "FontAwesome", "Font Awesome 5 Free"';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
@@ -669,17 +702,12 @@ function drawCanvasNode(n) {
|
|||||||
ctx.fillStyle = n.color;
|
ctx.fillStyle = n.color;
|
||||||
ctx.fillText(n.icon, n.x, n.y);
|
ctx.fillText(n.icon, n.x, n.y);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// Status dot
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(n.x + 17, n.y - 17, 5, 0, Math.PI * 2);
|
ctx.arc(n.x + 17, n.y - 17, 5, 0, Math.PI * 2);
|
||||||
const sc = { online: '#22c55e', offline: '#6b7280', unknown: '#9ca3af', compromised: '#ef4444', monitoring: '#eab308' };
|
const sc = { online: '#22c55e', offline: '#6b7280', unknown: '#9ca3af', compromised: '#ef4444', monitoring: '#eab308' };
|
||||||
ctx.fillStyle = sc[n.status] || '#9ca3af';
|
ctx.fillStyle = sc[n.status] || '#9ca3af';
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
if (n.status === 'compromised') {
|
if (n.status === 'compromised') { ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 2; ctx.stroke(); }
|
||||||
ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 2; ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fillStyle = '#e2e8f0';
|
ctx.fillStyle = '#e2e8f0';
|
||||||
ctx.font = sel ? 'bold 11px sans-serif' : '10px sans-serif';
|
ctx.font = sel ? 'bold 11px sans-serif' : '10px sans-serif';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
@@ -701,7 +729,6 @@ function getShapeHandles(s) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== CANVAS EVENTS ====================
|
|
||||||
function getCanvasNodeAt(mx, my) {
|
function getCanvasNodeAt(mx, my) {
|
||||||
return canvasNodes.find(n => Math.hypot(mx - n.x, my - n.y) < 28);
|
return canvasNodes.find(n => Math.hypot(mx - n.x, my - n.y) < 28);
|
||||||
}
|
}
|
||||||
@@ -728,7 +755,6 @@ function onMouseDown(e) {
|
|||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const mx = e.clientX - rect.left - panX;
|
const mx = e.clientX - rect.left - panX;
|
||||||
const my = e.clientY - rect.top - panY;
|
const my = e.clientY - rect.top - panY;
|
||||||
|
|
||||||
const resize = getShapeResizeHandleAt(mx, my);
|
const resize = getShapeResizeHandleAt(mx, my);
|
||||||
if (resize) {
|
if (resize) {
|
||||||
dragType = 'resize'; dragTarget = resize.shape;
|
dragType = 'resize'; dragTarget = resize.shape;
|
||||||
@@ -736,31 +762,20 @@ function onMouseDown(e) {
|
|||||||
dragOrig = { x: resize.shape.x, y: resize.shape.y, w: resize.shape.w, h: resize.shape.h, cx: resize.cx, cy: resize.cy };
|
dragOrig = { x: resize.shape.x, y: resize.shape.y, w: resize.shape.w, h: resize.shape.h, cx: resize.cx, cy: resize.cy };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = getCanvasNodeAt(mx, my);
|
const node = getCanvasNodeAt(mx, my);
|
||||||
if (node) {
|
if (node) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) { selectNode(node.id, true); }
|
||||||
selectNode(node.id, true);
|
else if (!selectedNodeIds.includes(node.id)) { selectNode(node.id); }
|
||||||
// Start multi-drag for all selected
|
|
||||||
dragType = 'node'; dragTarget = node;
|
dragType = 'node'; dragTarget = node;
|
||||||
dragOffX = mx - node.x; dragOffY = my - node.y;
|
dragOffX = mx - node.x; dragOffY = my - node.y;
|
||||||
} else {
|
|
||||||
if (!selectedNodeIds.includes(node.id)) {
|
|
||||||
selectNode(node.id);
|
|
||||||
}
|
|
||||||
dragType = 'node'; dragTarget = node;
|
|
||||||
dragOffX = mx - node.x; dragOffY = my - node.y;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shape = getShapeAt(mx, my);
|
const shape = getShapeAt(mx, my);
|
||||||
if (shape) {
|
if (shape) {
|
||||||
selectedNodeId = null; selectedNodeIds = []; selectedShapeId = shape.id;
|
selectedNodeId = null; selectedNodeIds = []; selectedShapeId = shape.id;
|
||||||
dragType = 'shape'; dragTarget = shape;
|
dragType = 'shape'; dragTarget = shape;
|
||||||
dragOffX = mx - shape.x; dragOffY = my - shape.y;
|
dragOffX = mx - shape.x; dragOffY = my - shape.y;
|
||||||
renderNodeList(); renderShapeList();
|
renderNodeList(); renderShapeList();
|
||||||
// Re-check resize handles now that shape is selected
|
|
||||||
const resizeNow = getShapeResizeHandleAt(mx, my);
|
const resizeNow = getShapeResizeHandleAt(mx, my);
|
||||||
if (resizeNow) {
|
if (resizeNow) {
|
||||||
dragType = 'resize'; dragTarget = resizeNow.shape;
|
dragType = 'resize'; dragTarget = resizeNow.shape;
|
||||||
@@ -769,7 +784,6 @@ function onMouseDown(e) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedNodeId = null; selectedNodeIds = []; selectedShapeId = null;
|
selectedNodeId = null; selectedNodeIds = []; selectedShapeId = null;
|
||||||
renderNodeList(); renderShapeList();
|
renderNodeList(); renderShapeList();
|
||||||
dragType = 'select';
|
dragType = 'select';
|
||||||
@@ -779,38 +793,28 @@ function onMouseDown(e) {
|
|||||||
|
|
||||||
function onMouseMove(e) {
|
function onMouseMove(e) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
if (dragType === 'node' && dragTarget) {
|
if (dragType === 'node' && dragTarget) {
|
||||||
const dx = e.clientX - rect.left - panX - dragOffX - dragTarget.x;
|
const dx = e.clientX - rect.left - panX - dragOffX - dragTarget.x;
|
||||||
const dy = e.clientY - rect.top - panY - dragOffY - dragTarget.y;
|
const dy = e.clientY - rect.top - panY - dragOffY - dragTarget.y;
|
||||||
// Move all selected nodes by delta
|
for (const c of canvasNodes) { if (selectedNodeIds.includes(c.id)) { c.x += dx; c.y += dy; } }
|
||||||
for (const c of canvasNodes) {
|
renderNetwork(); return;
|
||||||
if (selectedNodeIds.includes(c.id)) { c.x += dx; c.y += dy; }
|
|
||||||
}
|
|
||||||
renderNetwork();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (dragType === 'shape' && dragTarget) {
|
if (dragType === 'shape' && dragTarget) {
|
||||||
dragTarget.x = e.clientX - rect.left - panX - dragOffX;
|
dragTarget.x = e.clientX - rect.left - panX - dragOffX;
|
||||||
dragTarget.y = e.clientY - rect.top - panY - dragOffY;
|
dragTarget.y = e.clientY - rect.top - panY - dragOffY;
|
||||||
renderNetwork();
|
renderNetwork(); return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (dragType === 'resize' && dragTarget) {
|
if (dragType === 'resize' && dragTarget) {
|
||||||
const dx = e.clientX - rect.left - panX - dragOffX;
|
const dx = e.clientX - rect.left - panX - dragOffX;
|
||||||
const dy = e.clientY - rect.top - panY - dragOffY;
|
const dy = e.clientY - rect.top - panY - dragOffY;
|
||||||
const s = dragTarget;
|
const s = dragTarget; const o = dragOrig;
|
||||||
const o = dragOrig;
|
|
||||||
let nx = o.x, ny = o.y, nw = o.w, nh = o.h;
|
let nx = o.x, ny = o.y, nw = o.w, nh = o.h;
|
||||||
if (o.cx === 0) { nx = o.x + dx; nw = o.w - dx; }
|
if (o.cx === 0) { nx = o.x + dx; nw = o.w - dx; } else { nw = o.w + dx; }
|
||||||
else { nw = o.w + dx; }
|
if (o.cy === 0) { ny = o.y + dy; nh = o.h - dy; } else { nh = o.h + dy; }
|
||||||
if (o.cy === 0) { ny = o.y + dy; nh = o.h - dy; }
|
|
||||||
else { nh = o.h + dy; }
|
|
||||||
if (nw < 50) { if (o.cx === 0) nx = o.x + o.w - 50; nw = 50; }
|
if (nw < 50) { if (o.cx === 0) nx = o.x + o.w - 50; nw = 50; }
|
||||||
if (nh < 50) { if (o.cy === 0) ny = o.y + o.h - 50; nh = 50; }
|
if (nh < 50) { if (o.cy === 0) ny = o.y + o.h - 50; nh = 50; }
|
||||||
s.x = nx; s.y = ny; s.w = nw; s.h = nh;
|
s.x = nx; s.y = ny; s.w = nw; s.h = nh;
|
||||||
renderNetwork();
|
renderNetwork(); return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (dragType === 'select') {
|
if (dragType === 'select') {
|
||||||
const mx = e.clientX - rect.left - panX;
|
const mx = e.clientX - rect.left - panX;
|
||||||
@@ -819,15 +823,12 @@ function onMouseMove(e) {
|
|||||||
selectRect.y = Math.min(selectStartY, my);
|
selectRect.y = Math.min(selectStartY, my);
|
||||||
selectRect.w = Math.abs(mx - selectStartX);
|
selectRect.w = Math.abs(mx - selectStartX);
|
||||||
selectRect.h = Math.abs(my - selectStartY);
|
selectRect.h = Math.abs(my - selectStartY);
|
||||||
renderNetwork();
|
renderNetwork(); return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (isPanning) {
|
if (isPanning) {
|
||||||
panX = e.clientX - panStartX; panY = e.clientY - panStartY;
|
panX = e.clientX - panStartX; panY = e.clientY - panStartY;
|
||||||
renderNetwork();
|
renderNetwork(); return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mx = e.clientX - rect.left - panX;
|
const mx = e.clientX - rect.left - panX;
|
||||||
const my = e.clientY - rect.top - panY;
|
const my = e.clientY - rect.top - panY;
|
||||||
if (getShapeResizeHandleAt(mx, my)) canvas.style.cursor = 'nwse-resize';
|
if (getShapeResizeHandleAt(mx, my)) canvas.style.cursor = 'nwse-resize';
|
||||||
@@ -837,13 +838,9 @@ function onMouseMove(e) {
|
|||||||
|
|
||||||
function onMouseUp(e) {
|
function onMouseUp(e) {
|
||||||
if (dragType === 'node') {
|
if (dragType === 'node') {
|
||||||
for (const c of canvasNodes) {
|
for (const c of canvasNodes) { if (selectedNodeIds.includes(c.id)) apiFetch('nodes/' + c.id, { method: 'PUT', body: JSON.stringify({ pos_x: c.x, pos_y: c.y }) }); }
|
||||||
if (selectedNodeIds.includes(c.id)) {
|
|
||||||
apiFetch(`nodes/${c.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: c.x, pos_y: c.y }) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (dragTarget && (dragType === 'shape' || dragType === 'resize')) {
|
} 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 }) });
|
apiFetch('shapes/' + dragTarget.id, { method: 'PUT', body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y, width: dragTarget.w, height: dragTarget.h }) });
|
||||||
} else if (dragType === 'select') {
|
} else if (dragType === 'select') {
|
||||||
selectRect = null;
|
selectRect = null;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
@@ -854,9 +851,7 @@ function onMouseUp(e) {
|
|||||||
const rw = Math.abs(mx - selectStartX);
|
const rw = Math.abs(mx - selectStartX);
|
||||||
const rh = Math.abs(my - selectStartY);
|
const rh = Math.abs(my - selectStartY);
|
||||||
if (rw > 5 || rh > 5) {
|
if (rw > 5 || rh > 5) {
|
||||||
const found = canvasNodes.filter(n =>
|
const found = canvasNodes.filter(n => n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh);
|
||||||
n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh
|
|
||||||
);
|
|
||||||
selectedNodeIds = found.map(n => n.id);
|
selectedNodeIds = found.map(n => n.id);
|
||||||
selectedNodeId = found.length > 0 ? found[0].id : null;
|
selectedNodeId = found.length > 0 ? found[0].id : null;
|
||||||
selectedShapeId = null;
|
selectedShapeId = null;
|
||||||
@@ -886,110 +881,18 @@ function esc(s) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== AUTH / SESSION ====================
|
|
||||||
let currentUser = null;
|
|
||||||
let currentRole = null;
|
|
||||||
|
|
||||||
async function checkSession() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/session');
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.loggedin) {
|
|
||||||
currentUser = data.username;
|
|
||||||
currentRole = data.role;
|
|
||||||
document.getElementById('userDisplay').textContent = data.username;
|
|
||||||
if (data.role === 'admin') {
|
|
||||||
document.getElementById('settingsBtn').classList.remove('d-none');
|
|
||||||
}
|
|
||||||
document.getElementById('loginOverlay').style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
// Show login overlay
|
|
||||||
document.getElementById('loginOverlay').style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function performLogin(authToken) {
|
|
||||||
const errEl = document.getElementById('loginError');
|
|
||||||
const sucEl = document.getElementById('loginSuccess');
|
|
||||||
errEl.style.display = 'none';
|
|
||||||
sucEl.style.display = 'none';
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ auth_token: authToken })
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.status === 'success') {
|
|
||||||
currentUser = data.username;
|
|
||||||
currentRole = data.role;
|
|
||||||
document.getElementById('userDisplay').textContent = data.username;
|
|
||||||
if (data.role === 'admin') document.getElementById('settingsBtn').classList.remove('d-none');
|
|
||||||
document.getElementById('loginOverlay').style.display = 'none';
|
|
||||||
// Clean URL
|
|
||||||
window.history.replaceState({}, '', '/');
|
|
||||||
// Reload data
|
|
||||||
loadTeams().then(() => loadEvents());
|
|
||||||
loadNetworkData();
|
|
||||||
} else {
|
|
||||||
errEl.textContent = data.error || 'Login failed';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
errEl.textContent = 'Connection error';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
await apiFetch('logout', { method: 'POST' });
|
|
||||||
currentUser = null;
|
|
||||||
currentRole = null;
|
|
||||||
document.getElementById('settingsBtn').classList.add('d-none');
|
|
||||||
document.getElementById('userDisplay').textContent = '';
|
|
||||||
document.getElementById('loginOverlay').style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for auth token in URL on page load
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
const authToken = params.get('auth');
|
|
||||||
if (authToken) {
|
|
||||||
// Show a loading state on overlay
|
|
||||||
document.getElementById('loginOverlay').style.display = 'flex';
|
|
||||||
document.querySelector('#loginOverlay .btn').textContent = 'Authenticating...';
|
|
||||||
performLogin(authToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkSession().then(() => {
|
|
||||||
// Init canvas and load data
|
|
||||||
canvas = document.getElementById('networkCanvas');
|
|
||||||
ctx = canvas.getContext('2d');
|
|
||||||
resizeCanvas();
|
|
||||||
|
|
||||||
loadTeams().then(() => loadEvents());
|
|
||||||
loadNetworkData();
|
|
||||||
|
|
||||||
document.getElementById('loginBtn').addEventListener('click', () => {
|
|
||||||
const callbackUrl = window.location.origin + '/?auth_callback=1';
|
|
||||||
window.location.href = 'https://auth.jakach.ch/?send_to=' + encodeURIComponent(callbackUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
const list = document.getElementById('userList');
|
const list = document.getElementById('userList');
|
||||||
try {
|
try {
|
||||||
const users = await apiFetch('settings');
|
const users = await apiFetch('settings');
|
||||||
list.innerHTML = users.map(u => `
|
list.innerHTML = users.map(u => {
|
||||||
<div class="d-flex justify-content-between align-items-center py-1 border-bottom border-secondary">
|
return '<div class="d-flex justify-content-between align-items-center py-1 border-bottom border-secondary">' +
|
||||||
<div>
|
'<div><strong class="small">' + esc(u.username) + '</strong>' +
|
||||||
<strong class="small">${esc(u.username)}</strong>
|
'<span class="badge bg-' + (u.role === 'admin' ? 'warning' : 'secondary') + ' ms-1" style="font-size:.6rem;">' + u.role + '</span>' +
|
||||||
<span class="badge bg-${u.role === 'admin' ? 'warning' : 'secondary'} ms-1" style="font-size:.6rem;">${u.role}</span>
|
'<div class="text-secondary" style="font-size:.7rem;">' + u.user_token.substring(0, 16) + '...</div></div>' +
|
||||||
<div class="text-secondary" style="font-size:.7rem;">${u.user_token.substring(0, 16)}...</div>
|
(u.role !== 'admin' ? '<button class="btn btn-outline-danger btn-sm py-0 px-1" onclick="removeUser(' + u.id + ')"><i class="fas fa-times" style="font-size:.7rem;"></i></button>' : '') +
|
||||||
</div>
|
'</div>';
|
||||||
${u.role !== 'admin' ? `<button class="btn btn-outline-danger btn-sm py-0 px-1" onclick="removeUser(${u.id})"><i class="fas fa-times" style="font-size:.7rem;"></i></button>` : ''}
|
}).join('');
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
list.innerHTML = '<div class="text-danger small">Failed to load users</div>';
|
list.innerHTML = '<div class="text-danger small">Failed to load users</div>';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user