Update UI components and add drive-sentient.js

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-16 11:29:47 -03:00
parent eb785b9a69
commit e32e9b793a
6 changed files with 3635 additions and 1417 deletions

File diff suppressed because it is too large Load diff

View file

@ -1065,6 +1065,251 @@
}
}
// =============================================================================
// SHARING & COLLABORATION
// =============================================================================
function shareCanvas() {
if (!state.canvasId) {
// Save canvas first if not saved
saveCanvas().then(() => {
showShareDialog();
});
} else {
showShareDialog();
}
}
function showShareDialog() {
const modal = document.getElementById("share-modal");
if (modal) {
if (modal.showModal) {
modal.showModal();
} else {
modal.classList.add("open");
modal.style.display = "flex";
}
// Generate share link
const shareUrl = `${window.location.origin}/canvas?id=${state.canvasId}`;
const shareLinkInput = document.getElementById("share-link");
if (shareLinkInput) {
shareLinkInput.value = shareUrl;
}
} else {
// Fallback: copy link to clipboard
const shareUrl = `${window.location.origin}/canvas?id=${state.canvasId || "new"}`;
navigator.clipboard
.writeText(shareUrl)
.then(() => {
showNotification("Share link copied to clipboard", "success");
})
.catch(() => {
showNotification(
"Canvas ID: " + (state.canvasId || "unsaved"),
"info",
);
});
}
}
// =============================================================================
// PROPERTIES PANEL
// =============================================================================
function togglePropertiesPanel() {
const panel = document.getElementById("properties-panel");
if (panel) {
panel.classList.toggle("collapsed");
const isCollapsed = panel.classList.contains("collapsed");
// Update toggle button icon if needed
const toggleBtn = panel.querySelector(".panel-toggle span");
if (toggleBtn) {
toggleBtn.textContent = isCollapsed ? "⚙️" : "✕";
}
}
}
// =============================================================================
// LAYERS MANAGEMENT
// =============================================================================
let layers = [
{ id: "layer_1", name: "Layer 1", visible: true, locked: false },
];
let activeLayerId = "layer_1";
function addLayer() {
const newId = "layer_" + (layers.length + 1);
const newLayer = {
id: newId,
name: "Layer " + (layers.length + 1),
visible: true,
locked: false,
};
layers.push(newLayer);
activeLayerId = newId;
renderLayers();
showNotification("Layer added", "success");
}
function renderLayers() {
const layersList = document.getElementById("layers-list");
if (!layersList) return;
layersList.innerHTML = layers
.map(
(layer) => `
<div class="layer-item ${layer.id === activeLayerId ? "active" : ""}"
data-layer-id="${layer.id}"
onclick="selectLayer('${layer.id}')">
<span class="layer-visibility" onclick="event.stopPropagation(); toggleLayerVisibility('${layer.id}')">${layer.visible ? "👁️" : "👁️‍🗨️"}</span>
<span class="layer-name">${layer.name}</span>
<span class="layer-lock" onclick="event.stopPropagation(); toggleLayerLock('${layer.id}')">${layer.locked ? "🔒" : "🔓"}</span>
</div>
`,
)
.join("");
}
function selectLayer(layerId) {
activeLayerId = layerId;
renderLayers();
}
function toggleLayerVisibility(layerId) {
const layer = layers.find((l) => l.id === layerId);
if (layer) {
layer.visible = !layer.visible;
renderLayers();
render();
}
}
function toggleLayerLock(layerId) {
const layer = layers.find((l) => l.id === layerId);
if (layer) {
layer.locked = !layer.locked;
renderLayers();
}
}
// =============================================================================
// CLIPBOARD & DUPLICATE
// =============================================================================
function duplicateSelected() {
if (!state.selectedElement) {
showNotification("No element selected", "warning");
return;
}
const original = state.selectedElement;
const duplicate = JSON.parse(JSON.stringify(original));
duplicate.id = generateId();
// Offset the duplicate slightly
if (duplicate.x !== undefined) duplicate.x += 20;
if (duplicate.y !== undefined) duplicate.y += 20;
state.elements.push(duplicate);
state.selectedElement = duplicate;
saveToHistory();
render();
showNotification("Element duplicated", "success");
}
function copySelected() {
if (!state.selectedElement) {
showNotification("No element selected", "warning");
return;
}
state.clipboard = JSON.parse(JSON.stringify(state.selectedElement));
showNotification("Element copied", "success");
}
function pasteClipboard() {
if (!state.clipboard) {
showNotification("Nothing to paste", "warning");
return;
}
const pasted = JSON.parse(JSON.stringify(state.clipboard));
pasted.id = generateId();
// Offset the pasted element
if (pasted.x !== undefined) pasted.x += 20;
if (pasted.y !== undefined) pasted.y += 20;
state.elements.push(pasted);
state.selectedElement = pasted;
saveToHistory();
render();
showNotification("Element pasted", "success");
}
// =============================================================================
// ELEMENT ORDERING
// =============================================================================
function bringToFront() {
if (!state.selectedElement) return;
const index = state.elements.findIndex(
(e) => e.id === state.selectedElement.id,
);
if (index !== -1 && index < state.elements.length - 1) {
state.elements.splice(index, 1);
state.elements.push(state.selectedElement);
saveToHistory();
render();
}
}
function sendToBack() {
if (!state.selectedElement) return;
const index = state.elements.findIndex(
(e) => e.id === state.selectedElement.id,
);
if (index > 0) {
state.elements.splice(index, 1);
state.elements.unshift(state.selectedElement);
saveToHistory();
render();
}
}
// =============================================================================
// EXPORT MODAL
// =============================================================================
function showExportModal() {
const modal = document.getElementById("export-modal");
if (modal) {
if (modal.showModal) {
modal.showModal();
} else {
modal.classList.add("open");
modal.style.display = "flex";
}
}
}
function closeExportModal() {
const modal = document.getElementById("export-modal");
if (modal) {
if (modal.close) {
modal.close();
} else {
modal.classList.remove("open");
modal.style.display = "none";
}
}
}
function doExport() {
const formatSelect = document.getElementById("export-format");
const format = formatSelect ? formatSelect.value : "png";
exportCanvas(format);
closeExportModal();
}
// =============================================================================
// UTILITIES
// =============================================================================
@ -1108,6 +1353,32 @@
window.cutElement = cutElement;
window.pasteElement = pasteElement;
// Sharing & Collaboration
window.shareCanvas = shareCanvas;
// Properties Panel
window.togglePropertiesPanel = togglePropertiesPanel;
// Layers
window.addLayer = addLayer;
window.selectLayer = selectLayer;
window.toggleLayerVisibility = toggleLayerVisibility;
window.toggleLayerLock = toggleLayerLock;
// Clipboard & Duplicate
window.duplicateSelected = duplicateSelected;
window.copySelected = copySelected;
window.pasteClipboard = pasteClipboard;
// Element Ordering
window.bringToFront = bringToFront;
window.sendToBack = sendToBack;
// Export Modal
window.showExportModal = showExportModal;
window.closeExportModal = closeExportModal;
window.doExport = doExport;
// =============================================================================
// INITIALIZE
// =============================================================================

View file

@ -110,12 +110,97 @@
/* Messages Area */
#messages {
flex: 1;
overflow-y: auto;
padding: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
overflow-y: auto;
padding: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
scrollbar-width: thin;
scrollbar-color: var(--accent, #3b82f6) var(--surface, #1a1a24);
}
/* Custom scrollbar for markers */
#messages::-webkit-scrollbar {
width: 6px;
}
#messages::-webkit-scrollbar-track {
background: var(--surface, #1a1a24);
border-radius: 3px;
}
#messages::-webkit-scrollbar-thumb {
background: var(--accent, #3b82f6);
border-radius: 3px;
border: 1px solid var(--surface, #1a1a24);
}
#messages::-webkit-scrollbar-thumb:hover {
background: var(--accent-hover, #2563eb);
}
/* Scrollbar markers container */
.scrollbar-markers {
position: absolute;
top: 0;
right: 2px;
width: 8px;
height: 100%;
pointer-events: none;
z-index: 10;
}
.scrollbar-marker {
position: absolute;
right: 0;
width: 8px;
height: 8px;
background: var(--accent, #3b82f6);
border-radius: 50%;
cursor: pointer;
pointer-events: auto;
transition: all 0.2s ease;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
z-index: 11;
}
.scrollbar-marker:hover {
transform: scale(1.5);
background: var(--accent-hover, #2563eb);
box-shadow: 0 0 8px var(--accent, #3b82f6);
}
.scrollbar-marker.user-marker {
background: var(--accent, #3b82f6);
}
.scrollbar-marker.bot-marker {
background: var(--success, #22c55e);
}
.scrollbar-marker-tooltip {
position: absolute;
right: 12px;
transform: translateY(-50%);
background: var(--surface, #1a1a24);
border: 1px solid var(--border, #2a2a2a);
border-radius: 6px;
padding: 4px 8px;
font-size: 11px;
color: var(--text, #ffffff);
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
pointer-events: none;
z-index: 12;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.scrollbar-marker:hover .scrollbar-marker-tooltip {
opacity: 1;
visibility: visible;
}
/* Message Styles */
@ -314,8 +399,28 @@ footer {
}
.mention-results {
overflow-y: auto;
max-height: 250px;
overflow-y: auto;
max-height: 250px;
scrollbar-width: thin;
scrollbar-color: var(--accent, #3b82f6) transparent;
}
.mention-results::-webkit-scrollbar {
width: 4px;
}
.mention-results::-webkit-scrollbar-track {
background: transparent;
border-radius: 2px;
}
.mention-results::-webkit-scrollbar-thumb {
background: var(--accent, #3b82f6);
border-radius: 2px;
}
.mention-results::-webkit-scrollbar-thumb:hover {
background: var(--accent-hover, #2563eb);
}
.mention-item {

View file

@ -0,0 +1,296 @@
(function () {
"use strict";
let currentView = "grid";
let selectedFile = null;
let aiPanelOpen = true;
function toggleView(type) {
currentView = type;
const fileView = document.getElementById("file-view");
if (fileView) {
fileView.classList.remove("grid-view", "list-view");
fileView.classList.add(type + "-view");
}
document.querySelectorAll(".app-btn-secondary").forEach((btn) => {
btn.classList.remove("active");
});
event.target.classList.add("active");
}
function openFolder(el) {
const folderName = el.querySelector(".file-name").textContent;
const breadcrumb = document.querySelector(".breadcrumb");
if (breadcrumb) {
const separator = document.createElement("span");
separator.className = "breadcrumb-separator";
separator.textContent = "";
breadcrumb.appendChild(separator);
const item = document.createElement("span");
item.className = "breadcrumb-item current";
item.textContent = folderName;
breadcrumb.appendChild(item);
breadcrumb.querySelectorAll(".breadcrumb-item").forEach((i) => {
i.classList.remove("current");
});
item.classList.add("current");
}
addAIMessage("assistant", `Opened folder: ${folderName}`);
}
function selectFile(el) {
document.querySelectorAll(".file-item").forEach((item) => {
item.classList.remove("selected");
});
el.classList.add("selected");
selectedFile = {
name: el.querySelector(".file-name").textContent,
meta: el.querySelector(".file-meta").textContent,
};
}
function toggleAIPanel() {
const panel = document.getElementById("ai-panel");
if (panel) {
aiPanelOpen = !aiPanelOpen;
panel.classList.toggle("hidden", !aiPanelOpen);
}
const toggle = document.querySelector(".ai-toggle");
if (toggle) {
toggle.classList.toggle("active", aiPanelOpen);
}
}
function aiAction(action) {
const actions = {
organize: "Analyzing folder structure to suggest organization...",
find: "What file are you looking for?",
analyze: "Select a file and I'll analyze its content.",
share: "Select a file to set up sharing options.",
};
addAIMessage("assistant", actions[action] || "How can I help?");
}
function sendAIMessage() {
const input = document.getElementById("ai-input");
if (!input || !input.value.trim()) return;
const message = input.value.trim();
addAIMessage("user", message);
input.value = "";
setTimeout(() => {
processAIQuery(message);
}, 500);
}
function addAIMessage(type, content) {
const container = document.getElementById("ai-messages");
if (!container) return;
const div = document.createElement("div");
div.className = "ai-message " + type;
div.innerHTML = '<div class="ai-message-bubble">' + escapeHtml(content) + "</div>";
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function processAIQuery(query) {
const lowerQuery = query.toLowerCase();
let response = "I can help you manage your files. Try asking me to find, organize, or analyze files.";
if (lowerQuery.includes("find") || lowerQuery.includes("search") || lowerQuery.includes("buscar")) {
response = "I'll search for files matching your query. What type of file are you looking for?";
} else if (lowerQuery.includes("organize") || lowerQuery.includes("organizar")) {
response = "I can help organize your files by type, date, or project. Which method would you prefer?";
} else if (lowerQuery.includes("share") || lowerQuery.includes("compartilhar")) {
if (selectedFile) {
response = `Setting up sharing for "${selectedFile.name}". Who would you like to share it with?`;
} else {
response = "Please select a file first, then I can help you share it.";
}
} else if (lowerQuery.includes("delete") || lowerQuery.includes("excluir")) {
if (selectedFile) {
response = `Are you sure you want to delete "${selectedFile.name}"? This action cannot be undone.`;
} else {
response = "Please select a file first before deleting.";
}
} else if (lowerQuery.includes("storage") || lowerQuery.includes("space") || lowerQuery.includes("espaço")) {
response = "You're using 12.4 GB of your 50 GB storage. Would you like me to find large files to free up space?";
}
addAIMessage("assistant", response);
}
function uploadFile() {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.onchange = function (e) {
const files = Array.from(e.target.files);
if (files.length > 0) {
const names = files.map((f) => f.name).join(", ");
addAIMessage("assistant", `Uploading ${files.length} file(s): ${names}`);
simulateUpload(files);
}
};
input.click();
}
function simulateUpload(files) {
setTimeout(() => {
addAIMessage("assistant", `Successfully uploaded ${files.length} file(s)!`);
files.forEach((file) => {
addFileToView(file.name, formatFileSize(file.size));
});
}, 1500);
}
function addFileToView(name, size) {
const fileView = document.getElementById("file-view");
if (!fileView) return;
const icon = getFileIcon(name);
const div = document.createElement("div");
div.className = "file-item";
div.onclick = function () {
selectFile(this);
};
div.innerHTML = `
<div class="file-icon">${icon}</div>
<div class="file-name">${escapeHtml(name)}</div>
<div class="file-meta">${size}</div>
`;
fileView.appendChild(div);
}
function getFileIcon(filename) {
const ext = filename.split(".").pop().toLowerCase();
const icons = {
pdf: "📄",
doc: "📝",
docx: "📝",
xls: "📊",
xlsx: "📊",
ppt: "📽️",
pptx: "📽️",
jpg: "🖼️",
jpeg: "🖼️",
png: "🖼️",
gif: "🖼️",
mp4: "🎬",
mov: "🎬",
mp3: "🎵",
wav: "🎵",
zip: "📦",
rar: "📦",
txt: "📝",
md: "📝",
js: "💻",
ts: "💻",
rs: "💻",
py: "💻",
};
return icons[ext] || "📄";
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB";
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + " GB";
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function initKeyboardShortcuts() {
document.addEventListener("keydown", function (e) {
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
if (e.key === "Delete" && selectedFile) {
addAIMessage("assistant", `Delete "${selectedFile.name}"? Press Delete again to confirm.`);
}
if (e.ctrlKey && e.key === "u") {
e.preventDefault();
uploadFile();
}
if (e.key === "Escape") {
document.querySelectorAll(".file-item").forEach((item) => {
item.classList.remove("selected");
});
selectedFile = null;
}
});
}
function initSearch() {
const searchInput = document.querySelector(".search-input");
if (searchInput) {
searchInput.addEventListener("input", function (e) {
const query = e.target.value.toLowerCase();
document.querySelectorAll(".file-item").forEach((item) => {
const name = item.querySelector(".file-name").textContent.toLowerCase();
item.style.display = name.includes(query) ? "" : "none";
});
});
}
}
function initTabs() {
document.querySelectorAll(".topbar-tab").forEach((tab) => {
tab.addEventListener("click", function () {
document.querySelectorAll(".topbar-tab").forEach((t) => t.classList.remove("active"));
this.classList.add("active");
const tabName = this.textContent.trim();
addAIMessage("assistant", `Switched to ${tabName} view.`);
});
});
}
function initAppLauncher() {
document.querySelectorAll(".app-icon").forEach((icon) => {
icon.addEventListener("click", function () {
const app = this.dataset.app;
if (app && app !== "drive") {
window.location.href = "/suite/" + app + "/";
}
});
});
}
function init() {
initKeyboardShortcuts();
initSearch();
initTabs();
initAppLauncher();
const aiInput = document.getElementById("ai-input");
if (aiInput) {
aiInput.addEventListener("keypress", function (e) {
if (e.key === "Enter") {
sendAIMessage();
}
});
}
}
window.toggleView = toggleView;
window.openFolder = openFolder;
window.selectFile = selectFile;
window.toggleAIPanel = toggleAIPanel;
window.aiAction = aiAction;
window.sendAIMessage = sendAIMessage;
window.uploadFile = uploadFile;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

File diff suppressed because it is too large Load diff

View file

@ -2,274 +2,650 @@
* Tools Module JavaScript
* Compliance, Analytics, and Developer Tools
*/
(function() {
'use strict';
(function () {
"use strict";
/**
* Initialize the Tools module
*/
function init() {
setupBotSelector();
setupFilters();
setupKeyboardShortcuts();
setupHTMXEvents();
/**
* Initialize the Tools module
*/
function init() {
setupBotSelector();
setupFilters();
setupKeyboardShortcuts();
setupHTMXEvents();
updateStats();
}
/**
* Setup bot chip selection
*/
function setupBotSelector() {
document.addEventListener("click", function (e) {
const chip = e.target.closest(".bot-chip");
if (chip) {
// Toggle selection
chip.classList.toggle("selected");
// Update hidden checkbox
const checkbox = chip.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = chip.classList.contains("selected");
}
// Handle "All Bots" logic
if (chip.querySelector('input[value="all"]')) {
if (chip.classList.contains("selected")) {
// Deselect all other chips
document
.querySelectorAll(".bot-chip:not([data-all])")
.forEach((c) => {
c.classList.remove("selected");
const cb = c.querySelector('input[type="checkbox"]');
if (cb) cb.checked = false;
});
}
} else {
// Deselect "All Bots" when selecting individual bots
const allChip = document
.querySelector('.bot-chip input[value="all"]')
?.closest(".bot-chip");
if (allChip) {
allChip.classList.remove("selected");
const cb = allChip.querySelector('input[type="checkbox"]');
if (cb) cb.checked = false;
}
}
}
});
}
/**
* Setup filter controls
*/
function setupFilters() {
// Filter select changes
document.querySelectorAll(".filter-select").forEach((select) => {
select.addEventListener("change", function () {
applyFilters();
});
});
// Search input
const searchInput = document.querySelector(
'.filter-input[name="filter-search"]',
);
if (searchInput) {
let debounceTimer;
searchInput.addEventListener("input", function () {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => applyFilters(), 300);
});
}
}
/**
* Apply filters to results
*/
function applyFilters() {
const severity = document.getElementById("filter-severity")?.value || "all";
const type = document.getElementById("filter-type")?.value || "all";
const search =
document
.querySelector('.filter-input[name="filter-search"]')
?.value.toLowerCase() || "";
const rows = document.querySelectorAll("#results-body tr");
let visibleCount = 0;
rows.forEach((row) => {
let visible = true;
// Filter by severity
if (severity !== "all") {
const badge = row.querySelector(".severity-badge");
if (badge && !badge.classList.contains(severity)) {
visible = false;
}
}
// Filter by type
if (type !== "all" && visible) {
const issueIcon = row.querySelector(".issue-icon");
if (issueIcon && !issueIcon.classList.contains(type)) {
visible = false;
}
}
// Filter by search
if (search && visible) {
const text = row.textContent.toLowerCase();
if (!text.includes(search)) {
visible = false;
}
}
row.style.display = visible ? "" : "none";
if (visible) visibleCount++;
});
// Update results count
const countEl = document.getElementById("results-count");
if (countEl) {
countEl.textContent = `${visibleCount} issues found`;
}
}
/**
* Setup keyboard shortcuts
*/
function setupKeyboardShortcuts() {
document.addEventListener("keydown", function (e) {
// Ctrl+Enter to run scan
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
document.getElementById("scan-btn")?.click();
}
// Escape to close any open modals
if (e.key === "Escape") {
closeModals();
}
// Ctrl+E to export report
if ((e.ctrlKey || e.metaKey) && e.key === "e") {
e.preventDefault();
exportReport();
}
});
}
/**
* Setup HTMX events
*/
function setupHTMXEvents() {
if (typeof htmx === "undefined") return;
document.body.addEventListener("htmx:afterSwap", function (e) {
if (e.detail.target.id === "scan-results") {
updateStats();
}
}
});
}
/**
* Setup bot chip selection
*/
function setupBotSelector() {
document.addEventListener('click', function(e) {
const chip = e.target.closest('.bot-chip');
if (chip) {
// Toggle selection
chip.classList.toggle('selected');
/**
* Update statistics from results
*/
function updateStats() {
const rows = document.querySelectorAll("#results-body tr");
let stats = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
// Update hidden checkbox
const checkbox = chip.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = chip.classList.contains('selected');
}
rows.forEach((row) => {
if (row.style.display === "none") return;
// Handle "All Bots" logic
if (chip.querySelector('input[value="all"]')) {
if (chip.classList.contains('selected')) {
// Deselect all other chips
document.querySelectorAll('.bot-chip:not([data-all])').forEach(c => {
c.classList.remove('selected');
const cb = c.querySelector('input[type="checkbox"]');
if (cb) cb.checked = false;
});
}
} else {
// Deselect "All Bots" when selecting individual bots
const allChip = document.querySelector('.bot-chip input[value="all"]')?.closest('.bot-chip');
if (allChip) {
allChip.classList.remove('selected');
const cb = allChip.querySelector('input[type="checkbox"]');
if (cb) cb.checked = false;
}
}
}
});
}
const badge = row.querySelector(".severity-badge");
if (badge) {
if (badge.classList.contains("critical")) stats.critical++;
else if (badge.classList.contains("high")) stats.high++;
else if (badge.classList.contains("medium")) stats.medium++;
else if (badge.classList.contains("low")) stats.low++;
else if (badge.classList.contains("info")) stats.info++;
}
});
/**
* Setup filter controls
*/
function setupFilters() {
// Filter select changes
document.querySelectorAll('.filter-select').forEach(select => {
select.addEventListener('change', function() {
applyFilters();
});
});
// Search input
const searchInput = document.querySelector('.filter-input[name="filter-search"]');
if (searchInput) {
let debounceTimer;
searchInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => applyFilters(), 300);
});
}
}
/**
* Apply filters to results
*/
function applyFilters() {
const severity = document.getElementById('filter-severity')?.value || 'all';
const type = document.getElementById('filter-type')?.value || 'all';
const search = document.querySelector('.filter-input[name="filter-search"]')?.value.toLowerCase() || '';
const rows = document.querySelectorAll('#results-body tr');
let visibleCount = 0;
rows.forEach(row => {
let visible = true;
// Filter by severity
if (severity !== 'all') {
const badge = row.querySelector('.severity-badge');
if (badge && !badge.classList.contains(severity)) {
visible = false;
}
}
// Filter by type
if (type !== 'all' && visible) {
const issueIcon = row.querySelector('.issue-icon');
if (issueIcon && !issueIcon.classList.contains(type)) {
visible = false;
}
}
// Filter by search
if (search && visible) {
const text = row.textContent.toLowerCase();
if (!text.includes(search)) {
visible = false;
}
}
row.style.display = visible ? '' : 'none';
if (visible) visibleCount++;
});
// Update results count
const countEl = document.getElementById('results-count');
if (countEl) {
countEl.textContent = `${visibleCount} issues found`;
}
}
/**
* Setup keyboard shortcuts
*/
function setupKeyboardShortcuts() {
document.addEventListener('keydown', function(e) {
// Ctrl+Enter to run scan
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
document.getElementById('scan-btn')?.click();
}
// Escape to close any open modals
if (e.key === 'Escape') {
closeModals();
}
// Ctrl+E to export report
if ((e.ctrlKey || e.metaKey) && e.key === 'e') {
e.preventDefault();
exportReport();
}
});
}
/**
* Setup HTMX events
*/
function setupHTMXEvents() {
if (typeof htmx === 'undefined') return;
document.body.addEventListener('htmx:afterSwap', function(e) {
if (e.detail.target.id === 'scan-results') {
updateStats();
}
});
}
/**
* Update statistics from results
*/
function updateStats() {
const rows = document.querySelectorAll('#results-body tr');
let stats = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
rows.forEach(row => {
if (row.style.display === 'none') return;
const badge = row.querySelector('.severity-badge');
if (badge) {
if (badge.classList.contains('critical')) stats.critical++;
else if (badge.classList.contains('high')) stats.high++;
else if (badge.classList.contains('medium')) stats.medium++;
else if (badge.classList.contains('low')) stats.low++;
else if (badge.classList.contains('info')) stats.info++;
}
});
// Update stat cards
const updateStat = (id, value) => {
const el = document.getElementById(id);
if (el) el.textContent = value;
};
updateStat('stat-critical', stats.critical);
updateStat('stat-high', stats.high);
updateStat('stat-medium', stats.medium);
updateStat('stat-low', stats.low);
updateStat('stat-info', stats.info);
// Update total count
const total = stats.critical + stats.high + stats.medium + stats.low + stats.info;
const countEl = document.getElementById('results-count');
if (countEl) {
countEl.textContent = `${total} issues found`;
}
}
/**
* Export compliance report
*/
function exportReport() {
if (typeof htmx !== 'undefined') {
htmx.ajax('GET', '/api/compliance/export', {
swap: 'none'
});
}
}
/**
* Fix an issue
*/
function fixIssue(issueId) {
if (typeof htmx !== 'undefined') {
htmx.ajax('POST', `/api/compliance/fix/${issueId}`, {
swap: 'none'
}).then(() => {
// Refresh results
const scanBtn = document.getElementById('scan-btn');
if (scanBtn) scanBtn.click();
});
}
}
/**
* Close all modals
*/
function closeModals() {
document.querySelectorAll('.modal').forEach(modal => {
modal.classList.add('hidden');
});
}
/**
* Show toast notification
*/
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
requestAnimationFrame(() => {
toast.classList.add('show');
});
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Expose for external use
window.Tools = {
updateStats,
applyFilters,
fixIssue,
exportReport,
showToast
// Update stat cards
const updateStat = (id, value) => {
const el = document.getElementById(id);
if (el) el.textContent = value;
};
updateStat("stat-critical", stats.critical);
updateStat("stat-high", stats.high);
updateStat("stat-medium", stats.medium);
updateStat("stat-low", stats.low);
updateStat("stat-info", stats.info);
// Update total count
const total =
stats.critical + stats.high + stats.medium + stats.low + stats.info;
const countEl = document.getElementById("results-count");
if (countEl) {
countEl.textContent = `${total} issues found`;
}
}
/**
* Export compliance report
*/
function exportReport() {
if (typeof htmx !== "undefined") {
htmx.ajax("GET", "/api/compliance/export", {
swap: "none",
});
}
}
/**
* Fix an issue
*/
function fixIssue(issueId) {
if (typeof htmx !== "undefined") {
htmx
.ajax("POST", `/api/compliance/fix/${issueId}`, {
swap: "none",
})
.then(() => {
// Refresh results
const scanBtn = document.getElementById("scan-btn");
if (scanBtn) scanBtn.click();
});
}
}
/**
* Close all modals
*/
function closeModals() {
document.querySelectorAll(".modal").forEach((modal) => {
modal.classList.add("hidden");
});
}
/**
* Show toast notification
*/
function showToast(message, type = "success") {
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
requestAnimationFrame(() => {
toast.classList.add("show");
});
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
/**
* Configure a protection tool
*/
function configureProtectionTool(toolName) {
const modal =
document.getElementById("configure-modal") ||
document.getElementById("tool-config-modal");
if (modal) {
const titleEl = modal.querySelector(".modal-title, h2, h3");
if (titleEl) {
titleEl.textContent = `Configure ${toolName}`;
}
modal.dataset.tool = toolName;
if (modal.showModal) {
modal.showModal();
} else {
modal.classList.remove("hidden");
modal.style.display = "flex";
}
} else {
showToast(`Opening configuration for ${toolName}...`, "info");
fetch(`/api/tools/security/${toolName}/config`)
.then((r) => r.json())
.then((config) => {
console.log(`${toolName} config:`, config);
showToast(`${toolName} configuration loaded`, "success");
})
.catch((err) => {
console.error(`Error loading ${toolName} config:`, err);
showToast(`Failed to load ${toolName} configuration`, "error");
});
}
}
/**
* Run a protection tool scan
*/
function runProtectionTool(toolName) {
showToast(`Running ${toolName} scan...`, "info");
const statusEl = document.querySelector(
`[data-tool="${toolName}"] .tool-status, #${toolName}-status`,
);
if (statusEl) {
statusEl.textContent = "Running...";
statusEl.classList.add("running");
}
fetch(`/api/tools/security/${toolName}/run`, {
method: "POST",
headers: { "Content-Type": "application/json" },
})
.then((r) => r.json())
.then((result) => {
if (statusEl) {
statusEl.textContent = "Completed";
statusEl.classList.remove("running");
statusEl.classList.add("completed");
}
showToast(`${toolName} scan completed`, "success");
if (result.report_url) {
viewReport(toolName);
}
})
.catch((err) => {
console.error(`Error running ${toolName}:`, err);
if (statusEl) {
statusEl.textContent = "Error";
statusEl.classList.remove("running");
statusEl.classList.add("error");
}
showToast(`Failed to run ${toolName}`, "error");
});
}
/**
* Update a protection tool
*/
function updateProtectionTool(toolName) {
showToast(`Updating ${toolName}...`, "info");
fetch(`/api/tools/security/${toolName}/update`, {
method: "POST",
headers: { "Content-Type": "application/json" },
})
.then((r) => r.json())
.then((result) => {
showToast(
`${toolName} updated to version ${result.version || "latest"}`,
"success",
);
const versionEl = document.querySelector(
`[data-tool="${toolName}"] .tool-version, #${toolName}-version`,
);
if (versionEl && result.version) {
versionEl.textContent = result.version;
}
})
.catch((err) => {
console.error(`Error updating ${toolName}:`, err);
showToast(`Failed to update ${toolName}`, "error");
});
}
/**
* View report for a protection tool
*/
function viewReport(toolName) {
const reportModal =
document.getElementById("report-modal") ||
document.getElementById("view-report-modal");
if (reportModal) {
const titleEl = reportModal.querySelector(".modal-title, h2, h3");
if (titleEl) {
titleEl.textContent = `${toolName} Report`;
}
const contentEl = reportModal.querySelector(
".report-content, .modal-body",
);
if (contentEl) {
contentEl.innerHTML = '<div class="loading">Loading report...</div>';
}
if (reportModal.showModal) {
reportModal.showModal();
} else {
reportModal.classList.remove("hidden");
reportModal.style.display = "flex";
}
fetch(`/api/tools/security/${toolName}/report`)
.then((r) => r.json())
.then((report) => {
if (contentEl) {
contentEl.innerHTML = renderReport(toolName, report);
}
})
.catch((err) => {
console.error(`Error loading ${toolName} report:`, err);
if (contentEl) {
contentEl.innerHTML =
'<div class="error">Failed to load report</div>';
}
});
} else {
window.open(
`/api/tools/security/${toolName}/report?format=html`,
"_blank",
);
}
}
/**
* Render a security tool report
*/
function renderReport(toolName, report) {
const findings = report.findings || [];
const summary = report.summary || {};
return `
<div class="report-summary">
<h4>Summary</h4>
<div class="summary-stats">
<span class="stat critical">${summary.critical || 0} Critical</span>
<span class="stat high">${summary.high || 0} High</span>
<span class="stat medium">${summary.medium || 0} Medium</span>
<span class="stat low">${summary.low || 0} Low</span>
</div>
<p>Scan completed: ${report.completed_at || new Date().toISOString()}</p>
</div>
<div class="report-findings">
<h4>Findings (${findings.length})</h4>
${findings.length === 0 ? '<p class="no-findings">No issues found</p>' : ""}
${findings
.map(
(f) => `
<div class="finding ${f.severity || "info"}">
<span class="severity-badge ${f.severity || "info"}">${f.severity || "info"}</span>
<span class="finding-title">${f.title || f.message || "Finding"}</span>
<p class="finding-description">${f.description || ""}</p>
${f.remediation ? `<p class="finding-remediation"><strong>Fix:</strong> ${f.remediation}</p>` : ""}
</div>
`,
)
.join("")}
</div>
`;
}
/**
* Toggle auto action for a protection tool
*/
function toggleAutoAction(toolName, btn) {
const isEnabled =
btn.classList.contains("active") ||
btn.getAttribute("aria-pressed") === "true";
const newState = !isEnabled;
fetch(`/api/tools/security/${toolName}/auto`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: newState }),
})
.then((r) => r.json())
.then((result) => {
if (newState) {
btn.classList.add("active");
btn.setAttribute("aria-pressed", "true");
showToast(`Auto-scan enabled for ${toolName}`, "success");
} else {
btn.classList.remove("active");
btn.setAttribute("aria-pressed", "false");
showToast(`Auto-scan disabled for ${toolName}`, "info");
}
})
.catch((err) => {
console.error(`Error toggling auto action for ${toolName}:`, err);
showToast(`Failed to update ${toolName} settings`, "error");
});
}
/**
* Reindex a data source for search
*/
function reindexSource(sourceName) {
showToast(`Reindexing ${sourceName}...`, "info");
fetch(`/api/search/reindex/${sourceName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
})
.then((r) => r.json())
.then((result) => {
showToast(
`${sourceName} reindexing started. ${result.documents || 0} documents queued.`,
"success",
);
})
.catch((err) => {
console.error(`Error reindexing ${sourceName}:`, err);
showToast(`Failed to reindex ${sourceName}`, "error");
});
}
/**
* Show TSC (Trust Service Criteria) details
*/
function showTscDetails(category) {
const detailPanel =
document.getElementById("tsc-detail-panel") ||
document.querySelector(".tsc-details");
if (detailPanel) {
fetch(`/api/compliance/tsc/${category}`)
.then((r) => r.json())
.then((data) => {
detailPanel.innerHTML = renderTscDetails(category, data);
detailPanel.classList.add("open");
})
.catch((err) => {
console.error(`Error loading TSC details for ${category}:`, err);
showToast(`Failed to load ${category} details`, "error");
});
} else {
showToast(`Viewing ${category} criteria...`, "info");
}
}
/**
* Render TSC details
*/
function renderTscDetails(category, data) {
const controls = data.controls || [];
return `
<div class="tsc-detail-header">
<h3>${category.charAt(0).toUpperCase() + category.slice(1)} Criteria</h3>
<button class="close-btn" onclick="document.querySelector('.tsc-details').classList.remove('open')">×</button>
</div>
<div class="tsc-controls">
${controls
.map(
(c) => `
<div class="control-item ${c.status || "pending"}">
<span class="control-id">${c.id}</span>
<span class="control-name">${c.name}</span>
<span class="control-status">${c.status || "Pending"}</span>
</div>
`,
)
.join("")}
</div>
`;
}
/**
* Show control remediation steps
*/
function showControlRemediation(controlId) {
const modal =
document.getElementById("remediation-modal") ||
document.getElementById("control-modal");
if (modal) {
const titleEl = modal.querySelector(".modal-title, h2, h3");
if (titleEl) {
titleEl.textContent = `Remediate ${controlId}`;
}
const contentEl = modal.querySelector(
".modal-body, .remediation-content",
);
if (contentEl) {
contentEl.innerHTML =
'<div class="loading">Loading remediation steps...</div>';
}
if (modal.showModal) {
modal.showModal();
} else {
modal.classList.remove("hidden");
modal.style.display = "flex";
}
fetch(`/api/compliance/controls/${controlId}/remediation`)
.then((r) => r.json())
.then((data) => {
if (contentEl) {
contentEl.innerHTML = `
<div class="remediation-steps">
<h4>Steps to Remediate</h4>
<ol>
${(data.steps || []).map((s) => `<li>${s}</li>`).join("")}
</ol>
${data.documentation_url ? `<a href="${data.documentation_url}" target="_blank" class="btn btn-secondary">View Documentation</a>` : ""}
</div>
`;
}
})
.catch((err) => {
console.error(`Error loading remediation for ${controlId}:`, err);
if (contentEl) {
contentEl.innerHTML =
'<div class="error">Failed to load remediation steps</div>';
}
});
} else {
showToast(`Loading remediation for ${controlId}...`, "info");
}
}
// Expose for external use
window.Tools = {
updateStats,
applyFilters,
fixIssue,
exportReport,
showToast,
};
// Expose security tool functions globally
window.configureProtectionTool = configureProtectionTool;
window.runProtectionTool = runProtectionTool;
window.updateProtectionTool = updateProtectionTool;
window.viewReport = viewReport;
window.toggleAutoAction = toggleAutoAction;
window.reindexSource = reindexSource;
window.showTscDetails = showTscDetails;
window.showControlRemediation = showControlRemediation;
})();