Update UI components and add drive-sentient.js
This commit is contained in:
parent
eb785b9a69
commit
e32e9b793a
6 changed files with 3635 additions and 1417 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
296
ui/suite/drive/drive-sentient.js
Normal file
296
ui/suite/drive/drive-sentient.js
Normal 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
|
|
@ -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;
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue