adding file upload & tags
This commit is contained in:
+128
-2
@@ -73,6 +73,16 @@ try {
|
||||
case 'registration':
|
||||
handleRegistration($method, $db);
|
||||
break;
|
||||
case 'attachments':
|
||||
handleAttachments($method, $id, $db);
|
||||
break;
|
||||
case 'tags':
|
||||
if ($method === 'GET') {
|
||||
echo json_encode($db->query("SELECT DISTINCT tag FROM event_tags ORDER BY tag")->fetchAll(PDO::FETCH_COLUMN));
|
||||
} else {
|
||||
http_response_code(405);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Not found']);
|
||||
@@ -315,6 +325,23 @@ function handleTeams($method, $id, $db) {
|
||||
}
|
||||
}
|
||||
|
||||
function loadEventTags($eventId, $db) {
|
||||
$tstmt = $db->prepare("SELECT tag FROM event_tags WHERE event_id = ? ORDER BY tag");
|
||||
$tstmt->execute([$eventId]);
|
||||
return array_column($tstmt->fetchAll(PDO::FETCH_ASSOC), 'tag');
|
||||
}
|
||||
|
||||
function saveEventTags($eventId, $tags, $db) {
|
||||
$db->prepare("DELETE FROM event_tags WHERE event_id = ?")->execute([$eventId]);
|
||||
if (!empty($tags)) {
|
||||
$istmt = $db->prepare("INSERT IGNORE INTO event_tags (event_id, tag) VALUES (?, ?)");
|
||||
foreach ($tags as $tag) {
|
||||
$tag = trim($tag);
|
||||
if ($tag !== '') $istmt->execute([$eventId, $tag]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvents($method, $id, $db) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
@@ -330,19 +357,28 @@ function handleEvents($method, $id, $db) {
|
||||
$cstmt = $db->prepare("SELECT * FROM comments WHERE event_id = ? ORDER BY created_at ASC");
|
||||
$cstmt->execute([$id]);
|
||||
$event['comments'] = $cstmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$event['tags'] = loadEventTags($id, $db);
|
||||
}
|
||||
echo json_encode($event);
|
||||
} else {
|
||||
$teamFilter = $_GET['team_id'] ?? null;
|
||||
$tagFilter = $_GET['tag'] ?? null;
|
||||
$sql = "
|
||||
SELECT e.*, t.name AS team_name, t.color AS team_color
|
||||
FROM events e JOIN teams t ON e.team_id = t.id
|
||||
";
|
||||
$params = [];
|
||||
$conditions = [];
|
||||
if ($teamFilter) {
|
||||
$sql .= " WHERE e.team_id = ?";
|
||||
$conditions[] = "e.team_id = ?";
|
||||
$params[] = $teamFilter;
|
||||
}
|
||||
if ($tagFilter) {
|
||||
$sql .= " JOIN event_tags et ON e.id = et.event_id";
|
||||
$conditions[] = "et.tag = ?";
|
||||
$params[] = $tagFilter;
|
||||
}
|
||||
if ($conditions) $sql .= " WHERE " . implode(' AND ', $conditions);
|
||||
$sql .= " ORDER BY e.occurred_at DESC";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
@@ -351,6 +387,7 @@ function handleEvents($method, $id, $db) {
|
||||
$cstmt = $db->prepare("SELECT * FROM comments WHERE event_id = ? ORDER BY created_at ASC");
|
||||
$cstmt->execute([$event['id']]);
|
||||
$event['comments'] = $cstmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$event['tags'] = loadEventTags($event['id'], $db);
|
||||
}
|
||||
echo json_encode($events);
|
||||
}
|
||||
@@ -369,7 +406,11 @@ function handleEvents($method, $id, $db) {
|
||||
$data['event_type'] ?? 'general',
|
||||
$data['occurred_at'] ?? date('Y-m-d H:i:s')
|
||||
]);
|
||||
echo json_encode(['id' => $db->lastInsertId()]);
|
||||
$eventId = $db->lastInsertId();
|
||||
if (isset($data['tags']) && is_array($data['tags'])) {
|
||||
saveEventTags($eventId, $data['tags'], $db);
|
||||
}
|
||||
echo json_encode(['id' => $eventId]);
|
||||
break;
|
||||
case 'PUT':
|
||||
if ($id) {
|
||||
@@ -387,11 +428,21 @@ function handleEvents($method, $id, $db) {
|
||||
$stmt = $db->prepare("UPDATE events SET " . implode(', ', $fields) . " WHERE id = ?");
|
||||
$stmt->execute($params);
|
||||
}
|
||||
if (isset($data['tags']) && is_array($data['tags'])) {
|
||||
saveEventTags($id, $data['tags'], $db);
|
||||
}
|
||||
echo json_encode(['updated' => true]);
|
||||
}
|
||||
break;
|
||||
case 'DELETE':
|
||||
if ($id) {
|
||||
// Delete attachments from disk
|
||||
$stmt = $db->prepare("SELECT stored_name FROM file_attachments WHERE event_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $att) {
|
||||
$path = '/var/www/uploads/' . $att['stored_name'];
|
||||
if (file_exists($path)) unlink($path);
|
||||
}
|
||||
$stmt = $db->prepare("DELETE FROM events WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['deleted' => true]);
|
||||
@@ -612,6 +663,81 @@ function handleDocuments($method, $id, $db) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleAttachments($method, $id, $db) {
|
||||
$username = $_SESSION['neptune_username'] ?? 'Unknown';
|
||||
$uploadDir = '/var/www/uploads/';
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$eventId = $_GET['event_id'] ?? null;
|
||||
if ($eventId) {
|
||||
$stmt = $db->prepare("SELECT id, event_id, original_name, stored_name, mime_type, file_size, uploaded_by, created_at FROM file_attachments WHERE event_id = ? ORDER BY created_at ASC");
|
||||
$stmt->execute([$eventId]);
|
||||
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'event_id required']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Upload failed']);
|
||||
return;
|
||||
}
|
||||
$eventId = $_POST['event_id'] ?? null;
|
||||
if (!$eventId) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'event_id required']);
|
||||
return;
|
||||
}
|
||||
$file = $_FILES['file'];
|
||||
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
$storedName = uniqid('att_', true) . '.' . $ext;
|
||||
$dest = $uploadDir . $storedName;
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $dest)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Failed to save file']);
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO file_attachments (event_id, original_name, stored_name, mime_type, file_size, uploaded_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$eventId,
|
||||
$file['name'],
|
||||
$storedName,
|
||||
$file['type'] ?: 'application/octet-stream',
|
||||
$file['size'],
|
||||
$username
|
||||
]);
|
||||
echo json_encode(['id' => $db->lastInsertId()]);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
if ($id) {
|
||||
$stmt = $db->prepare("SELECT stored_name FROM file_attachments WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($att) {
|
||||
$path = $uploadDir . $att['stored_name'];
|
||||
if (file_exists($path)) unlink($path);
|
||||
$db->prepare("DELETE FROM file_attachments WHERE id = ?")->execute([$id]);
|
||||
}
|
||||
echo json_encode(['deleted' => true]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleShapes($method, $id, $db) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
|
||||
@@ -58,6 +58,22 @@ function migrate($db) {
|
||||
setting_key VARCHAR(100) PRIMARY KEY,
|
||||
setting_value TEXT NOT NULL
|
||||
)");
|
||||
try { $db->exec("CREATE TABLE IF NOT EXISTS file_attachments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_id INT NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
stored_name VARCHAR(255) NOT NULL,
|
||||
mime_type VARCHAR(100),
|
||||
file_size INT,
|
||||
uploaded_by VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)"); } catch (Exception $e) {}
|
||||
try { $db->exec("CREATE TABLE IF NOT EXISTS event_tags (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_id INT NOT NULL,
|
||||
tag VARCHAR(50) NOT NULL,
|
||||
UNIQUE KEY unique_event_tag (event_id, tag)
|
||||
)"); } catch (Exception $e) {}
|
||||
try {
|
||||
$stmt = $db->prepare("SELECT COUNT(*) as c FROM neptune_settings WHERE setting_key = 'registration_enabled'");
|
||||
$stmt->execute();
|
||||
|
||||
@@ -6,6 +6,7 @@ services:
|
||||
volumes:
|
||||
- ./frontend:/var/www/html
|
||||
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
- uploads_data:/var/www/uploads
|
||||
depends_on:
|
||||
- php
|
||||
networks:
|
||||
@@ -19,6 +20,7 @@ services:
|
||||
MYSQL_ROOT_PASSWORD: neptune_root_pass
|
||||
volumes:
|
||||
- ./backend:/var/www/backend
|
||||
- uploads_data:/var/www/uploads
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
@@ -44,6 +46,7 @@ services:
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
uploads_data:
|
||||
|
||||
networks:
|
||||
neptune:
|
||||
@@ -3,7 +3,10 @@ FROM php:8.2-fpm
|
||||
RUN apt-get update && apt-get install -y libcurl4-openssl-dev && \
|
||||
docker-php-ext-install pdo pdo_mysql curl && \
|
||||
mkdir -p /tmp/sessions && \
|
||||
chmod 777 /tmp/sessions
|
||||
chmod 777 /tmp/sessions && \
|
||||
mkdir -p /var/www/uploads && \
|
||||
chmod 755 /var/www/uploads && \
|
||||
chown www-data:www-data /var/www/uploads
|
||||
|
||||
COPY docker/php.ini /usr/local/etc/php/conf.d/neptune.ini
|
||||
|
||||
|
||||
@@ -76,6 +76,26 @@ CREATE TABLE IF NOT EXISTS documents (
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS file_attachments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_id INT NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
stored_name VARCHAR(255) NOT NULL,
|
||||
mime_type VARCHAR(100),
|
||||
file_size INT,
|
||||
uploaded_by VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS event_tags (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_id INT NOT NULL,
|
||||
tag VARCHAR(50) NOT NULL,
|
||||
UNIQUE KEY unique_event_tag (event_id, tag),
|
||||
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO teams (name, color) VALUES
|
||||
('Blue Team', '#0d6efd'),
|
||||
('Red Team', '#dc3545'),
|
||||
|
||||
@@ -14,6 +14,11 @@ server {
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location /uploads/ {
|
||||
alias /var/www/uploads/;
|
||||
add_header Content-Disposition 'inline';
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
@@ -4,3 +4,5 @@ session.cookie_lifetime = 0
|
||||
session.use_strict_mode = 1
|
||||
session.cookie_httponly = 1
|
||||
session.cookie_samesite = Lax
|
||||
upload_max_filesize = 50M
|
||||
post_max_size = 50M
|
||||
@@ -257,3 +257,27 @@ kbd {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .03em;
|
||||
}
|
||||
|
||||
/* Attachments */
|
||||
.attachments-list {
|
||||
border-top: 1px solid var(--neptune-border);
|
||||
padding-top: .35rem;
|
||||
margin-top: .35rem;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
border-bottom: 1px solid rgba(30, 42, 69, 0.5);
|
||||
}
|
||||
|
||||
.attachment-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.attachment-item a {
|
||||
color: var(--neptune-accent);
|
||||
}
|
||||
|
||||
.attachment-item a:hover {
|
||||
color: #60a5fa;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
+145
-3
@@ -152,6 +152,7 @@ function initApp() {
|
||||
document.getElementById('addUserBtn').addEventListener('click', addUser);
|
||||
document.getElementById('logoutBtn').addEventListener('click', logout);
|
||||
document.getElementById('teamFilter').addEventListener('change', renderTimeline);
|
||||
document.getElementById('tagFilter').addEventListener('change', renderTimeline);
|
||||
document.getElementById('searchEvents').addEventListener('input', renderTimeline);
|
||||
document.getElementById('saveDocument').addEventListener('click', saveDocument);
|
||||
document.getElementById('documentModal').addEventListener('hidden.bs.modal', () => {
|
||||
@@ -166,6 +167,22 @@ function initApp() {
|
||||
document.getElementById('opacityVal').textContent = parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
|
||||
document.getElementById('eventFileInput').addEventListener('change', (e) => {
|
||||
const preview = document.getElementById('eventFilePreview');
|
||||
const files = e.target.files;
|
||||
if (!files.length) { preview.innerHTML = ''; return; }
|
||||
preview.innerHTML = Array.from(files).map(f =>
|
||||
`<div class="small text-secondary"><i class="fas ${getFileIcon(f.type, f.name)} me-1"></i>${esc(f.name)} (${formatFileSize(f.size)})</div>`
|
||||
).join('');
|
||||
});
|
||||
|
||||
document.getElementById('eventTagsInput').addEventListener('input', renderEventTagsPreview);
|
||||
|
||||
document.getElementById('eventModal').addEventListener('hidden.bs.modal', () => {
|
||||
document.getElementById('eventTagsDisplay').innerHTML = '';
|
||||
document.getElementById('eventTagsInput').value = '';
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousedown', onMouseDown);
|
||||
canvas.addEventListener('mousemove', onMouseMove);
|
||||
canvas.addEventListener('mouseup', onMouseUp);
|
||||
@@ -250,19 +267,36 @@ async function loadTeams() {
|
||||
|
||||
async function loadEvents() {
|
||||
events = await apiFetch('events');
|
||||
loadAllTags();
|
||||
renderTimeline();
|
||||
}
|
||||
|
||||
let allTags = [];
|
||||
|
||||
async function loadAllTags() {
|
||||
try {
|
||||
allTags = await apiFetch('tags');
|
||||
const sel = document.getElementById('tagFilter');
|
||||
if (!sel) return;
|
||||
const current = sel.value;
|
||||
sel.innerHTML = '<option value="">All Tags</option>' + allTags.map(t => `<option value="${esc(t)}">${esc(t)}</option>`).join('');
|
||||
sel.value = current;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function renderTimeline() {
|
||||
const container = document.getElementById('timelineContainer');
|
||||
const teamFilter = document.getElementById('teamFilter').value;
|
||||
const tagFilter = document.getElementById('tagFilter').value;
|
||||
const search = document.getElementById('searchEvents').value.toLowerCase();
|
||||
|
||||
let filtered = events;
|
||||
if (teamFilter) filtered = filtered.filter(e => e.team_id == teamFilter);
|
||||
if (tagFilter) filtered = filtered.filter(e => e.tags && e.tags.includes(tagFilter));
|
||||
if (search) filtered = filtered.filter(e =>
|
||||
e.title.toLowerCase().includes(search) ||
|
||||
(e.description && e.description.toLowerCase().includes(search))
|
||||
(e.description && e.description.toLowerCase().includes(search)) ||
|
||||
(e.tags && e.tags.some(t => t.includes(search)))
|
||||
);
|
||||
|
||||
if (!filtered.length) {
|
||||
@@ -287,6 +321,7 @@ function renderTimeline() {
|
||||
</div>
|
||||
<h6 class="event-title mt-1 mb-1">${esc(e.title)}</h6>
|
||||
${e.description ? '<p class="mb-1 small text-secondary">' + renderDocLinks(e.description) + '</p>' : ''}
|
||||
${renderTimelineTags(e.tags)}
|
||||
<div class="d-flex justify-content-between align-items-center mt-1">
|
||||
<div class="doc-links-inline" id="docLinks-${e.id}">${renderInlineDocLinks(e.description || '')}</div>
|
||||
<div>
|
||||
@@ -294,6 +329,7 @@ function renderTimeline() {
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1" onclick="deleteEvent(${e.id}, this)" title="Delete event" style="font-size:.7rem;"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachments-list mt-1" id="attachments-${e.id}" data-event-id="${e.id}"></div>
|
||||
<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="comment-log">${renderComments(e)}</div>
|
||||
@@ -306,6 +342,11 @@ function renderTimeline() {
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
filtered.forEach(e => {
|
||||
const container = document.getElementById('attachments-' + e.id);
|
||||
if (container) renderAttachments(e.id, container);
|
||||
});
|
||||
}
|
||||
|
||||
function renderDocLinks(text) {
|
||||
@@ -450,22 +491,123 @@ async function deleteEvent(id, btn) {
|
||||
loadEvents();
|
||||
}
|
||||
|
||||
async function uploadAttachment(eventId, file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('event_id', eventId);
|
||||
await fetch('/api/attachments', { method: 'POST', body: formData });
|
||||
}
|
||||
|
||||
async function loadAttachments(eventId) {
|
||||
try {
|
||||
const res = await fetch('/api/attachments?event_id=' + eventId);
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAttachment(id, el) {
|
||||
if (!confirm('Delete this file?')) return;
|
||||
await apiFetch('attachments/' + id, { method: 'DELETE' });
|
||||
const container = el.closest('.attachments-list');
|
||||
if (container) {
|
||||
const eventId = container.dataset.eventId;
|
||||
renderAttachments(eventId, container);
|
||||
}
|
||||
}
|
||||
|
||||
async function renderAttachments(eventId, container) {
|
||||
container.innerHTML = '<small class="text-secondary"><i class="fas fa-spinner fa-spin me-1"></i>Loading...</small>';
|
||||
const attachments = await loadAttachments(eventId);
|
||||
if (!attachments.length) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = attachments.map(a => {
|
||||
const icon = getFileIcon(a.mime_type, a.original_name);
|
||||
const size = formatFileSize(a.file_size);
|
||||
return `<div class="attachment-item d-flex align-items-center justify-content-between py-1">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas ${icon} me-2" style="font-size:.85rem;"></i>
|
||||
<a href="/uploads/${a.stored_name}" target="_blank" class="small text-decoration-none" style="word-break:break-all;">${esc(a.original_name)}</a>
|
||||
<small class="text-secondary ms-2">(${size})</small>
|
||||
</div>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1 ms-2" onclick="deleteAttachment(${a.id}, this)" title="Delete file" style="font-size:.65rem;"><i class="fas fa-times"></i></button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (!bytes) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let i = 0;
|
||||
let size = bytes;
|
||||
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; }
|
||||
return size.toFixed(1) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function getFileIcon(mime, name) {
|
||||
if (mime.startsWith('image/')) return 'fa-file-image';
|
||||
if (mime === 'application/pdf') return 'fa-file-pdf';
|
||||
if (mime.startsWith('text/')) return 'fa-file-alt';
|
||||
if (mime.includes('spreadsheet') || mime.includes('excel') || name.endsWith('.csv') || name.endsWith('.xls') || name.endsWith('.xlsx')) return 'fa-file-excel';
|
||||
if (mime.includes('document') || mime.includes('word') || name.endsWith('.doc') || name.endsWith('.docx')) return 'fa-file-word';
|
||||
if (name.endsWith('.md')) return 'fa-file-alt';
|
||||
return 'fa-file';
|
||||
}
|
||||
|
||||
async function saveEvent() {
|
||||
const tags = parseEventTags();
|
||||
const data = {
|
||||
team_id: document.getElementById('eventTeam').value,
|
||||
title: document.getElementById('eventTitle').value,
|
||||
description: document.getElementById('eventDescription').value,
|
||||
severity: document.getElementById('eventSeverity').value,
|
||||
event_type: document.getElementById('eventType').value,
|
||||
occurred_at: new Date().toISOString().slice(0, 16)
|
||||
occurred_at: new Date().toISOString().slice(0, 16),
|
||||
tags: tags
|
||||
};
|
||||
if (!data.title) return alert('Title required');
|
||||
await apiFetch('events', { method: 'POST', body: JSON.stringify(data) });
|
||||
|
||||
const result = await apiFetch('events', { method: 'POST', body: JSON.stringify(data) });
|
||||
const eventId = result.id;
|
||||
|
||||
const fileInput = document.getElementById('eventFileInput');
|
||||
if (fileInput.files.length > 0) {
|
||||
for (const file of fileInput.files) {
|
||||
await uploadAttachment(eventId, file);
|
||||
}
|
||||
}
|
||||
bootstrap.Modal.getInstance(document.getElementById('eventModal')).hide();
|
||||
document.getElementById('eventForm').reset();
|
||||
document.getElementById('eventFilePreview').innerHTML = '';
|
||||
document.getElementById('eventTagsDisplay').innerHTML = '';
|
||||
document.getElementById('eventTagsInput').value = '';
|
||||
fileInput.value = '';
|
||||
loadEvents();
|
||||
}
|
||||
|
||||
function parseEventTags() {
|
||||
const input = document.getElementById('eventTagsInput');
|
||||
if (!input) return [];
|
||||
return input.value.split(',').map(t => t.trim().toLowerCase()).filter(t => t.length > 0);
|
||||
}
|
||||
|
||||
function renderEventTagsPreview() {
|
||||
const tags = parseEventTags();
|
||||
const container = document.getElementById('eventTagsDisplay');
|
||||
if (!container) return;
|
||||
container.innerHTML = tags.map(t => `<span class="badge bg-secondary me-1" style="font-size:.7rem;">${esc(t)}</span>`).join('');
|
||||
}
|
||||
|
||||
function renderTimelineTags(tags) {
|
||||
if (!tags || !tags.length) return '';
|
||||
return '<div class="d-flex flex-wrap gap-1 mb-1">' + tags.map(t =>
|
||||
`<span class="badge" style="font-size:.6rem;background:#1e2a45;color:#94a3b8;border:1px solid #2a3a60;">${esc(t)}</span>`
|
||||
).join('') + '</div>';
|
||||
}
|
||||
|
||||
// ==================== NETWORK MAP DATA ====================
|
||||
async function loadNetworkData() {
|
||||
const [n, l, s] = await Promise.all([
|
||||
|
||||
+18
-2
@@ -63,12 +63,17 @@
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<select class="form-select form-select-sm" id="teamFilter">
|
||||
<option value="">All Teams</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-3">
|
||||
<select class="form-select form-select-sm" id="tagFilter">
|
||||
<option value="">All Tags</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control form-control-sm" id="searchEvents" placeholder="Search events...">
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,6 +245,17 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Tags</label>
|
||||
<input type="text" class="form-control form-control-sm" id="eventTagsInput" placeholder="e.g. phishing, ransomware, ioc">
|
||||
<small class="text-secondary">Comma-separated tags</small>
|
||||
<div id="eventTagsDisplay" class="d-flex flex-wrap gap-1 mt-1"></div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">File Attachments</label>
|
||||
<input type="file" class="form-control form-control-sm" id="eventFileInput" multiple>
|
||||
<div id="eventFilePreview" class="mt-1"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary">
|
||||
|
||||
Reference in New Issue
Block a user