diff --git a/backend/api/index.php b/backend/api/index.php
index c74a4c9..033a984 100644
--- a/backend/api/index.php
+++ b/backend/api/index.php
@@ -506,7 +506,7 @@ function handleDocuments($method, $id, $db) {
$typeLabels = ['deployment' => 'Deployment', 'attack' => 'Attack', 'incident-report' => 'Incident Report', 'remediation' => 'Remediation', 'exercise' => 'Exercise'];
$typeLabel = $typeLabels[$docType] ?? ucfirst($docType);
$eventTitle = $typeLabel . ': ' . $data['title'];
- $eventDesc = $username . ' created document "' . $data['title'] . '" (' . $typeLabel . ')';
+ $eventDesc = $username . ' created [doc:' . $docId . ']' . $data['title'] . '[/doc] (' . $typeLabel . ')';
$stmt2 = $db->prepare("
INSERT INTO events (team_id, title, description, severity, event_type, occurred_at)
VALUES (?, ?, ?, 'info', 'document', ?)
@@ -538,7 +538,7 @@ function handleDocuments($method, $id, $db) {
$typeLabels = ['deployment' => 'Deployment', 'attack' => 'Attack', 'incident-report' => 'Incident Report', 'remediation' => 'Remediation', 'exercise' => 'Exercise'];
$typeLabel = $typeLabels[$docType] ?? ucfirst($docType);
$eventTitle = 'Updated ' . $typeLabel . ': ' . $docTitle;
- $eventDesc = $username . ' updated document "' . $docTitle . '" (' . $typeLabel . ')';
+ $eventDesc = $username . ' updated [doc:' . $id . ']' . $docTitle . '[/doc] (' . $typeLabel . ')';
$stmt2 = $db->prepare("
INSERT INTO events (team_id, title, description, severity, event_type, occurred_at)
VALUES (?, ?, ?, 'info', 'document', ?)
diff --git a/frontend/assets/js/app.js b/frontend/assets/js/app.js
index 2edc3a3..9db8bf4 100644
--- a/frontend/assets/js/app.js
+++ b/frontend/assets/js/app.js
@@ -268,7 +268,7 @@ function renderTimeline() {
${date}
${esc(e.title)}
- ${e.description ? '' + esc(e.description) + '
' : ''}
+ ${e.description ? '' + renderDocLinks(e.description) + '
' : ''}
@@ -286,6 +286,58 @@ function renderTimeline() {
}).join('');
}
+function renderDocLinks(text) {
+ const parts = text.split(/(\[doc:\d+\].*?\[\/doc\])/g);
+ return parts.map(p => {
+ const m = p.match(/\[doc:(\d+)\](.*?)\[\/doc\]/);
+ if (m) {
+ return `${esc(m[2])}`;
+ }
+ return esc(p);
+ }).join('');
+}
+
+function openDocument(id) {
+ const tab = document.getElementById('documents-tab');
+ if (tab) tab.click();
+ setTimeout(() => {
+ const container = document.getElementById('documentContainer');
+ if (!container) return;
+ const cards = container.querySelectorAll('.doc-card');
+ cards.forEach(c => {
+ c.style.transition = 'box-shadow .3s, border-color .3s';
+ c.style.boxShadow = '';
+ c.style.borderColor = '';
+ });
+ const target = container.querySelector(`[data-doc-id="${id}"]`);
+ if (target) {
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ target.style.boxShadow = '0 0 20px rgba(59,130,246,.5)';
+ target.style.borderColor = 'var(--neptune-accent)';
+ setTimeout(() => {
+ target.style.boxShadow = '';
+ target.style.borderColor = '';
+ }, 3000);
+ } else {
+ loadDocuments().then(() => {
+ setTimeout(() => {
+ const el = document.getElementById('documentContainer').querySelector(`[data-doc-id="${id}"]`);
+ if (el) {
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ el.style.boxShadow = '0 0 20px rgba(59,130,246,.5)';
+ el.style.borderColor = 'var(--neptune-accent)';
+ setTimeout(() => { el.style.boxShadow = ''; el.style.borderColor = ''; }, 3000);
+ }
+ }, 100);
+ });
+ }
+ const filter = document.getElementById('searchDocs');
+ if (filter) filter.value = '';
+ document.getElementById('docTeamFilter').value = '';
+ document.getElementById('docTypeFilter').value = '';
+ }, 300);
+}
+
function renderComments(event) {
if (!event.comments || !event.comments.length) return 'No comments yet
';
return event.comments.map(c => `
@@ -1030,7 +1082,7 @@ function renderDocuments() {
const contentPreview = d.content ? d.content.substring(0, 150) + (d.content.length > 150 ? '...' : '') : '';
return `