adding documents
Deploy / deploy (push) Successful in 38s

This commit is contained in:
2026-05-08 00:33:37 +02:00
parent 4f18b193c9
commit b70e8cd6e4
6 changed files with 530 additions and 1 deletions
+24
View File
@@ -232,4 +232,28 @@ kbd {
.comment-input-group .btn:hover {
background: var(--neptune-accent);
color: #fff;
}
/* Document Cards */
.doc-card {
transition: border-color .2s, transform .15s;
}
.doc-card:hover {
border-color: var(--neptune-accent);
transform: translateY(-2px);
}
.doc-card .card-header {
background: rgba(59,130,246,.05);
}
.doc-card .card-footer {
background: transparent;
}
.doc-card .badge {
font-size: .65rem;
text-transform: uppercase;
letter-spacing: .03em;
}
+269 -1
View File
@@ -1,6 +1,8 @@
const API = '/api/';
let teams = [];
let events = [];
let documents = [];
let editingDocId = null;
let nodes = [];
let links = [];
let shapes = [];
@@ -119,7 +121,10 @@ function initApp() {
ctx = canvas.getContext('2d');
resizeCanvas();
loadTeams().then(() => loadEvents());
loadTeams().then(() => {
loadEvents();
loadDocuments();
});
loadNetworkData();
document.getElementById('saveEvent').addEventListener('click', saveEvent);
@@ -130,6 +135,10 @@ function initApp() {
document.getElementById('logoutBtn').addEventListener('click', logout);
document.getElementById('teamFilter').addEventListener('change', renderTimeline);
document.getElementById('searchEvents').addEventListener('input', renderTimeline);
document.getElementById('saveDocument').addEventListener('click', saveDocument);
document.getElementById('docTeamFilter').addEventListener('change', renderDocuments);
document.getElementById('docTypeFilter').addEventListener('change', renderDocuments);
document.getElementById('searchDocs').addEventListener('input', renderDocuments);
document.getElementById('shapeOpacity').addEventListener('input', (e) => {
document.getElementById('opacityVal').textContent = parseFloat(e.target.value).toFixed(2);
});
@@ -949,6 +958,265 @@ async function removeUser(id) {
document.getElementById('settingsModal').addEventListener('show.bs.modal', loadUsers);
// ==================== DOCUMENTS ====================
const DOC_TYPE_ICONS = {
deployment: { icon: 'fa-server', color: '#06b6d4' },
attack: { icon: 'fa-bolt', color: '#ef4444' },
'incident-report': { icon: 'fa-exclamation-triangle', color: '#f59e0b' },
remediation: { icon: 'fa-wrench', color: '#22c55e' },
exercise: { icon: 'fa-dumbbell', color: '#3b82f6' }
};
const DOC_TYPE_LABELS = {
deployment: 'Deployment',
attack: 'Attack',
'incident-report': 'Incident Report',
remediation: 'Remediation',
exercise: 'Exercise'
};
async function loadDocuments() {
documents = await apiFetch('documents');
populateDocTeamFilter();
renderDocuments();
}
function populateDocTeamFilter() {
const sel = document.getElementById('docTeamFilter');
const teamSel = document.getElementById('docTeam');
sel.innerHTML = '<option value="">All Teams</option>';
teamSel.innerHTML = '';
teams.forEach(t => {
sel.innerHTML += `<option value="${t.id}">${t.name}</option>`;
teamSel.innerHTML += `<option value="${t.id}">${t.name}</option>`;
});
}
function renderDocuments() {
const container = document.getElementById('documentContainer');
const teamFilter = document.getElementById('docTeamFilter').value;
const typeFilter = document.getElementById('docTypeFilter').value;
const search = document.getElementById('searchDocs').value.toLowerCase();
let filtered = documents;
if (teamFilter) filtered = filtered.filter(d => d.team_id == teamFilter);
if (typeFilter) filtered = filtered.filter(d => d.doc_type == typeFilter);
if (search) filtered = filtered.filter(d =>
d.title.toLowerCase().includes(search) ||
(d.content && d.content.toLowerCase().includes(search))
);
if (!filtered.length) {
container.innerHTML = '<div class="text-center text-secondary py-5"><i class="fas fa-file-alt fs-1 mb-2"></i><p>No documents yet. Create a deployment, attack report, or more!</p></div>';
return;
}
container.innerHTML = '<div class="row">' + filtered.map(d => {
const meta = DOC_TYPE_ICONS[d.doc_type] || DOC_TYPE_ICONS.deployment;
const label = DOC_TYPE_LABELS[d.doc_type] || d.doc_type;
const date = new Date(d.occurred_at).toLocaleString();
const contentPreview = d.content ? d.content.substring(0, 150) + (d.content.length > 150 ? '...' : '') : '';
return `
<div class="col-md-6 col-lg-4 mb-3">
<div class="card bg-dark border-secondary h-100 doc-card">
<div class="card-header border-secondary py-2 d-flex justify-content-between align-items-center">
<div>
<span class="badge me-1" style="background:${meta.color}20;color:${meta.color};border:1px solid ${meta.color}40;">
<i class="fas ${meta.icon} me-1"></i>${label}
</span>
<span class="badge bg-secondary" style="font-size:.6rem;">${esc(d.team_name)}</span>
</div>
<small class="text-secondary" style="font-size:.7rem;">${date}</small>
</div>
<div class="card-body py-2">
<h6 class="mb-1">${esc(d.title)}</h6>
${contentPreview ? '<p class="small text-secondary mb-0">' + esc(contentPreview) + '</p>' : ''}
</div>
<div class="card-footer border-secondary py-1 text-end">
<button class="btn btn-outline-primary btn-sm py-0 px-1" onclick="editDocument(${d.id})" title="Edit" style="font-size:.7rem;"><i class="fas fa-pen"></i></button>
<button class="btn btn-outline-danger btn-sm py-0 px-1" onclick="deleteDocument(${d.id})" title="Delete" style="font-size:.7rem;"><i class="fas fa-trash"></i></button>
</div>
</div>
</div>`;
}).join('') + '</div>';
}
const DOC_FIELD_TEMPLATES = {
deployment: [
{ id: 'docTarget', label: 'Target System / Host', type: 'text', placeholder: 'e.g. web-server-01, firewall-cluster' },
{ id: 'docVersion', label: 'Version / Build', type: 'text', placeholder: 'e.g. v2.1.0, build 42' },
{ id: 'docStatus', label: 'Status', type: 'select', options: ['Planned', 'In Progress', 'Completed', 'Rolled Back', 'Failed'] }
],
attack: [
{ id: 'docSource', label: 'Attack Source / IP', type: 'text', placeholder: 'e.g. 10.0.0.5, External C2' },
{ id: 'docVector', label: 'Attack Vector', type: 'select', options: ['Phishing', 'Brute Force', 'Malware', 'DDoS', 'SQL Injection', 'XSS', 'Social Engineering', 'Physical', 'Other'] },
{ id: 'docImpact', label: 'Impact', type: 'select', options: ['None', 'Low', 'Medium', 'High', 'Critical'] }
],
'incident-report': [
{ id: 'docSeverity', label: 'Severity', type: 'select', options: ['Info', 'Low', 'Medium', 'High', 'Critical'] },
{ id: 'docDetectedBy', label: 'Detected By', type: 'text', placeholder: 'e.g. SOC, Automated Alert, User Report' },
{ id: 'docContainment', label: 'Containment Status', type: 'select', options: ['Not Contained', 'In Progress', 'Contained', 'Resolved'] }
],
remediation: [
{ id: 'docAffected', label: 'Affected Systems', type: 'text', placeholder: 'e.g. mail-server-02, all endpoints' },
{ id: 'docAction', label: 'Action Taken', type: 'select', options: ['Patch Applied', 'Config Change', 'Rule Updated', 'Access Revoked', 'Network Isolated', 'Other'] },
{ id: 'docVerified', label: 'Verification', type: 'select', options: ['Pending', 'Verified', 'Failed Verification'] }
],
exercise: [
{ id: 'docScenario', label: 'Scenario', type: 'text', placeholder: 'e.g. Ransomware simulation, phishing drill' },
{ id: 'docParticipants', label: 'Participants', type: 'text', placeholder: 'e.g. Blue Team, SOC, external red team' },
{ id: 'docOutcome', label: 'Outcome', type: 'select', options: ['Pass', 'Partial Pass', 'Fail', 'Inconclusive', 'Not Evaluated'] }
]
};
function updateDocFormFields() {
const docType = document.getElementById('docType').value;
const container = document.getElementById('dynamicDocFields');
const templates = DOC_FIELD_TEMPLATES[docType] || [];
container.innerHTML = templates.map(f => {
if (f.type === 'select') {
const opts = f.options.map(o => `<option value="${o}">${o}</option>`).join('');
return `
<div class="mb-2">
<label class="form-label small">${f.label}</label>
<select class="form-select form-select-sm" id="${f.id}">
${opts}
</select>
</div>`;
}
return `
<div class="mb-2">
<label class="form-label small">${f.label}</label>
<input type="text" class="form-control form-control-sm" id="${f.id}" placeholder="${f.placeholder || ''}">
</div>`;
}).join('');
// Restore values if editing
if (editingDocId && window._editDocData) {
const data = window._editDocData;
templates.forEach(f => {
const el = document.getElementById(f.id);
if (el) el.value = data._fields?.[f.id] || '';
});
}
}
function openNewDocument(type) {
editingDocId = null;
document.getElementById('documentModalLabel').textContent = 'New ' + (DOC_TYPE_LABELS[type] || type) + ' Document';
document.getElementById('saveDocument').innerHTML = '<i class="fas fa-save me-1"></i> Save Document';
document.getElementById('docType').value = type;
document.getElementById('docTitle').value = '';
document.getElementById('docContent').value = '';
window._editDocData = null;
updateDocFormFields();
new bootstrap.Modal(document.getElementById('documentModal')).show();
}
function collectDocFields() {
const docType = document.getElementById('docType').value;
const templates = DOC_FIELD_TEMPLATES[docType] || [];
const fields = {};
templates.forEach(f => {
const el = document.getElementById(f.id);
if (el) fields[f.id] = el.value;
});
return fields;
}
async function saveDocument() {
const docType = document.getElementById('docType').value;
const teamId = document.getElementById('docTeam').value;
const title = document.getElementById('docTitle').value.trim();
const content = document.getElementById('docContent').value.trim();
const fields = collectDocFields();
if (!title) return alert('Title is required');
if (!teamId) return alert('Team is required');
// Build structured content from fields
const label = DOC_TYPE_LABELS[docType] || docType;
let fullContent = content;
const fieldEntries = Object.entries(fields).filter(([, v]) => v);
if (fieldEntries.length) {
const fieldPrefix = fieldEntries.map(([k, v]) => {
const tmpl = (DOC_FIELD_TEMPLATES[docType] || []).find(t => t.id === k);
return (tmpl ? tmpl.label + ': ' : k + ': ') + v;
}).join('\n');
fullContent = fieldPrefix + (content ? '\n\n' + content : '');
}
const data = {
doc_type: docType,
team_id: teamId,
title: title,
content: fullContent,
occurred_at: new Date().toISOString().slice(0, 16)
};
if (editingDocId) {
data._fields = fields;
await apiFetch('documents/' + editingDocId, { method: 'PUT', body: JSON.stringify(data) });
editingDocId = null;
} else {
await apiFetch('documents', { method: 'POST', body: JSON.stringify(data) });
}
bootstrap.Modal.getInstance(document.getElementById('documentModal')).hide();
document.getElementById('documentForm').reset();
window._editDocData = null;
loadDocuments();
loadEvents();
}
async function editDocument(id) {
const d = documents.find(x => x.id == id);
if (!d) return;
editingDocId = id;
window._editDocData = d;
// Parse structured content back into fields
const docType = d.doc_type;
const templates = DOC_FIELD_TEMPLATES[docType] || [];
const parsedFields = {};
if (d.content) {
const lines = d.content.split('\n');
let inContent = false;
let contentParts = [];
for (const line of lines) {
if (!inContent && line.includes(': ')) {
const colonIdx = line.indexOf(': ');
const fieldLabel = line.substring(0, colonIdx);
const fieldVal = line.substring(colonIdx + 2);
const tmpl = templates.find(t => t.label === fieldLabel);
if (tmpl) {
parsedFields[tmpl.id] = fieldVal;
continue;
}
}
if (!line.trim() && !inContent) { inContent = true; continue; }
if (inContent) contentParts.push(line);
}
d._parsedContent = contentParts.join('\n').trim();
}
document.getElementById('documentModalLabel').textContent = 'Edit ' + (DOC_TYPE_LABELS[d.doc_type] || d.doc_type) + ' Document';
document.getElementById('saveDocument').innerHTML = '<i class="fas fa-save me-1"></i> Update Document';
document.getElementById('docType').value = d.doc_type;
document.getElementById('docTeam').value = d.team_id;
document.getElementById('docTitle').value = d.title;
document.getElementById('docContent').value = d._parsedContent || '';
updateDocFormFields();
new bootstrap.Modal(document.getElementById('documentModal')).show();
}
async function deleteDocument(id) {
if (!confirm('Delete this document?')) return;
await apiFetch('documents/' + id, { method: 'DELETE' });
loadDocuments();
loadEvents();
}
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
if (r > w / 2) r = w / 2;