diff --git a/ui/suite/admin/admin-functions.js b/ui/suite/admin/admin-functions.js
index 4dfaef4..1eb98eb 100644
--- a/ui/suite/admin/admin-functions.js
+++ b/ui/suite/admin/admin-functions.js
@@ -3,724 +3,1561 @@
These functions are called by onclick handlers in admin HTML files
============================================================================= */
-(function() {
- 'use strict';
+(function () {
+ "use strict";
- // =============================================================================
- // MODAL HELPERS
- // =============================================================================
+ // =============================================================================
+ // MODAL HELPERS
+ // =============================================================================
- function showModal(modalId) {
- const modal = document.getElementById(modalId);
- if (modal) {
- if (modal.showModal) {
- modal.showModal();
- } else {
- modal.classList.add('open');
- modal.style.display = 'flex';
- }
- }
+ function showModal(modalId) {
+ const modal = document.getElementById(modalId);
+ if (modal) {
+ if (modal.showModal) {
+ modal.showModal();
+ } else {
+ modal.classList.add("open");
+ modal.style.display = "flex";
+ }
+ }
+ }
+
+ function hideModal(modalId) {
+ const modal = document.getElementById(modalId);
+ if (modal) {
+ if (modal.close) {
+ modal.close();
+ } else {
+ modal.classList.remove("open");
+ modal.style.display = "none";
+ }
+ }
+ }
+
+ function showNotification(message, type) {
+ if (typeof window.showNotification === "function") {
+ window.showNotification(message, type);
+ } else if (typeof window.GBAlerts !== "undefined") {
+ if (type === "success") window.GBAlerts.success("Admin", message);
+ else if (type === "error") window.GBAlerts.error("Admin", message);
+ else if (type === "warning") window.GBAlerts.warning("Admin", message);
+ else window.GBAlerts.info("Admin", message);
+ } else {
+ console.log(`[${type}] ${message}`);
+ }
+ }
+
+ // =============================================================================
+ // ACCOUNTS.HTML FUNCTIONS
+ // =============================================================================
+
+ function showSmtpModal() {
+ showModal("smtp-modal");
+ }
+
+ function closeSmtpModal() {
+ hideModal("smtp-modal");
+ }
+
+ function testSmtpConnection() {
+ const host = document.getElementById("smtp-host")?.value;
+ const port = document.getElementById("smtp-port")?.value;
+ const username = document.getElementById("smtp-username")?.value;
+
+ if (!host || !port) {
+ showNotification("Please fill in SMTP host and port", "error");
+ return;
}
- function hideModal(modalId) {
- const modal = document.getElementById(modalId);
- if (modal) {
- if (modal.close) {
- modal.close();
- } else {
- modal.classList.remove('open');
- modal.style.display = 'none';
- }
- }
- }
+ showNotification("Testing SMTP connection...", "info");
- function showNotification(message, type) {
- if (typeof window.showNotification === 'function') {
- window.showNotification(message, type);
- } else if (typeof window.GBAlerts !== 'undefined') {
- if (type === 'success') window.GBAlerts.success('Admin', message);
- else if (type === 'error') window.GBAlerts.error('Admin', message);
- else if (type === 'warning') window.GBAlerts.warning('Admin', message);
- else window.GBAlerts.info('Admin', message);
+ fetch("/api/settings/smtp/test", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ host, port: parseInt(port), username }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("SMTP connection successful!", "success");
} else {
- console.log(`[${type}] ${message}`);
+ showNotification(
+ "SMTP connection failed: " + (data.error || "Unknown error"),
+ "error",
+ );
}
- }
+ })
+ .catch((err) => {
+ showNotification("Connection test failed: " + err.message, "error");
+ });
+ }
- // =============================================================================
- // ACCOUNTS.HTML FUNCTIONS
- // =============================================================================
+ function connectAccount(provider) {
+ showNotification(`Connecting to ${provider}...`, "info");
+ // OAuth flow would redirect to provider
+ window.location.href = `/api/auth/oauth/${provider}?redirect=/admin/accounts`;
+ }
- function showSmtpModal() {
- showModal('smtp-modal');
- }
+ function disconnectAccount(provider) {
+ if (!confirm(`Disconnect ${provider} account?`)) return;
- function closeSmtpModal() {
- hideModal('smtp-modal');
- }
-
- function testSmtpConnection() {
- const host = document.getElementById('smtp-host')?.value;
- const port = document.getElementById('smtp-port')?.value;
- const username = document.getElementById('smtp-username')?.value;
-
- if (!host || !port) {
- showNotification('Please fill in SMTP host and port', 'error');
- return;
- }
-
- showNotification('Testing SMTP connection...', 'info');
-
- fetch('/api/settings/smtp/test', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ host, port: parseInt(port), username })
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('SMTP connection successful!', 'success');
- } else {
- showNotification('SMTP connection failed: ' + (data.error || 'Unknown error'), 'error');
- }
- })
- .catch(err => {
- showNotification('Connection test failed: ' + err.message, 'error');
- });
- }
-
- function connectAccount(provider) {
- showNotification(`Connecting to ${provider}...`, 'info');
- // OAuth flow would redirect to provider
- window.location.href = `/api/auth/oauth/${provider}?redirect=/admin/accounts`;
- }
-
- function disconnectAccount(provider) {
- if (!confirm(`Disconnect ${provider} account?`)) return;
-
- fetch(`/api/settings/accounts/${provider}/disconnect`, { method: 'POST' })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification(`${provider} disconnected`, 'success');
- location.reload();
- } else {
- showNotification('Failed to disconnect: ' + data.error, 'error');
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
- }
-
- // =============================================================================
- // ADMIN-DASHBOARD.HTML FUNCTIONS
- // =============================================================================
-
- function showInviteMemberModal() {
- showModal('invite-member-modal');
- }
-
- function closeInviteMemberModal() {
- hideModal('invite-member-modal');
- }
-
- function showBulkInviteModal() {
- showModal('bulk-invite-modal');
- }
-
- function closeBulkInviteModal() {
- hideModal('bulk-invite-modal');
- }
-
- function sendInvitation() {
- const email = document.getElementById('invite-email')?.value;
- const role = document.getElementById('invite-role')?.value || 'member';
-
- if (!email) {
- showNotification('Please enter an email address', 'error');
- return;
- }
-
- fetch('/api/admin/invitations', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email, role })
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('Invitation sent to ' + email, 'success');
- closeInviteMemberModal();
- } else {
- showNotification('Failed to send invitation: ' + data.error, 'error');
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
- }
-
- function sendBulkInvitations() {
- const emailsText = document.getElementById('bulk-emails')?.value || '';
- const role = document.getElementById('bulk-role')?.value || 'member';
- const emails = emailsText.split(/[\n,;]+/).map(e => e.trim()).filter(e => e);
-
- if (emails.length === 0) {
- showNotification('Please enter at least one email address', 'error');
- return;
- }
-
- fetch('/api/admin/invitations/bulk', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ emails, role })
- })
- .then(response => response.json())
- .then(data => {
- showNotification(`${data.sent || emails.length} invitations sent`, 'success');
- closeBulkInviteModal();
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
- }
-
- function resendInvitation(invitationId) {
- fetch(`/api/admin/invitations/${invitationId}/resend`, { method: 'POST' })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('Invitation resent', 'success');
- } else {
- showNotification('Failed to resend: ' + data.error, 'error');
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
- }
-
- function cancelInvitation(invitationId) {
- if (!confirm('Cancel this invitation?')) return;
-
- fetch(`/api/admin/invitations/${invitationId}`, { method: 'DELETE' })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('Invitation cancelled', 'success');
- location.reload();
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
- }
-
- // =============================================================================
- // BILLING-DASHBOARD.HTML FUNCTIONS
- // =============================================================================
-
- function updateBillingPeriod(period) {
- const params = new URLSearchParams({ period });
-
- // Update dashboard stats via HTMX or fetch
- if (typeof htmx !== 'undefined') {
- htmx.ajax('GET', `/api/admin/billing/stats?${params}`, '#billing-stats');
+ fetch(`/api/settings/accounts/${provider}/disconnect`, { method: "POST" })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification(`${provider} disconnected`, "success");
+ location.reload();
} else {
- fetch(`/api/admin/billing/stats?${params}`)
- .then(r => r.json())
- .then(data => updateBillingStats(data))
- .catch(err => console.error('Failed to update billing period:', err));
+ showNotification("Failed to disconnect: " + data.error, "error");
}
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
+
+ // =============================================================================
+ // ADMIN-DASHBOARD.HTML FUNCTIONS
+ // =============================================================================
+
+ function showInviteMemberModal() {
+ showModal("invite-member-modal");
+ }
+
+ function closeInviteMemberModal() {
+ hideModal("invite-member-modal");
+ }
+
+ function showBulkInviteModal() {
+ showModal("bulk-invite-modal");
+ }
+
+ function closeBulkInviteModal() {
+ hideModal("bulk-invite-modal");
+ }
+
+ function sendInvitation() {
+ const email = document.getElementById("invite-email")?.value;
+ const role = document.getElementById("invite-role")?.value || "member";
+
+ if (!email) {
+ showNotification("Please enter an email address", "error");
+ return;
}
- function updateBillingStats(data) {
- if (data.totalRevenue) {
- const el = document.getElementById('total-revenue');
- if (el) el.textContent = formatCurrency(data.totalRevenue);
- }
- if (data.activeSubscriptions) {
- const el = document.getElementById('active-subscriptions');
- if (el) el.textContent = data.activeSubscriptions;
- }
- }
-
- function exportBillingReport() {
- const period = document.getElementById('billingPeriod')?.value || 'current';
- showNotification('Generating billing report...', 'info');
-
- fetch(`/api/admin/billing/export?period=${period}`)
- .then(response => {
- if (response.ok) return response.blob();
- throw new Error('Export failed');
- })
- .then(blob => {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `billing-report-${period}.csv`;
- a.click();
- URL.revokeObjectURL(url);
- showNotification('Report downloaded', 'success');
- })
- .catch(err => showNotification('Export failed: ' + err.message, 'error'));
- }
-
- function toggleBreakdownView() {
- const chart = document.getElementById('breakdown-chart');
- const table = document.getElementById('breakdown-table');
-
- if (chart && table) {
- const showingChart = !chart.classList.contains('hidden');
- chart.classList.toggle('hidden', showingChart);
- table.classList.toggle('hidden', !showingChart);
- }
- }
-
- function showQuotaSettings() {
- showModal('quota-settings-modal');
- }
-
- function closeQuotaSettings() {
- hideModal('quota-settings-modal');
- }
-
- function saveQuotaSettings() {
- const form = document.getElementById('quota-form');
- if (!form) return;
-
- const formData = new FormData(form);
- const quotas = Object.fromEntries(formData);
-
- fetch('/api/admin/billing/quotas', {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(quotas)
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('Quota settings saved', 'success');
- closeQuotaSettings();
- } else {
- showNotification('Failed to save: ' + data.error, 'error');
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
- }
-
- function configureAlerts() {
- showModal('alerts-config-modal');
- }
-
- function closeAlertsConfig() {
- hideModal('alerts-config-modal');
- }
-
- function saveAlertSettings() {
- const form = document.getElementById('alerts-form');
- if (!form) return;
-
- const formData = new FormData(form);
- const settings = Object.fromEntries(formData);
-
- fetch('/api/admin/billing/alerts', {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(settings)
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('Alert settings saved', 'success');
- closeAlertsConfig();
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
- }
-
- // =============================================================================
- // BILLING.HTML FUNCTIONS
- // =============================================================================
-
- function showUpgradeModal() {
- showModal('upgrade-modal');
- }
-
- function closeUpgradeModal() {
- hideModal('upgrade-modal');
- }
-
- function showCancelModal() {
- showModal('cancel-modal');
- }
-
- function closeCancelModal() {
- hideModal('cancel-modal');
- }
-
- function showAddPaymentModal() {
- showModal('add-payment-modal');
- }
-
- function closeAddPaymentModal() {
- hideModal('add-payment-modal');
- }
-
- function showEditAddressModal() {
- showModal('edit-address-modal');
- }
-
- function closeEditAddressModal() {
- hideModal('edit-address-modal');
- }
-
- function exportInvoices() {
- showNotification('Exporting invoices...', 'info');
-
- fetch('/api/billing/invoices/export')
- .then(response => {
- if (response.ok) return response.blob();
- throw new Error('Export failed');
- })
- .then(blob => {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'invoices.csv';
- a.click();
- URL.revokeObjectURL(url);
- showNotification('Invoices exported', 'success');
- })
- .catch(err => showNotification('Export failed: ' + err.message, 'error'));
- }
-
- function contactSales() {
- window.open('mailto:sales@example.com?subject=Enterprise Plan Inquiry', '_blank');
- }
-
- function showDowngradeOptions() {
- closeCancelModal();
- showUpgradeModal();
- // Focus on lower-tier plans
- const planSelector = document.querySelector('.plan-options');
- if (planSelector) {
- planSelector.scrollIntoView({ behavior: 'smooth' });
- }
- }
-
- function selectPlan(planId) {
- document.querySelectorAll('.plan-option').forEach(el => {
- el.classList.toggle('selected', el.dataset.plan === planId);
- });
- }
-
- function confirmUpgrade() {
- const selectedPlan = document.querySelector('.plan-option.selected');
- if (!selectedPlan) {
- showNotification('Please select a plan', 'error');
- return;
- }
-
- const planId = selectedPlan.dataset.plan;
-
- fetch('/api/billing/subscription/upgrade', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ plan_id: planId })
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('Plan upgraded successfully!', 'success');
- closeUpgradeModal();
- location.reload();
- } else {
- showNotification('Upgrade failed: ' + data.error, 'error');
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
- }
-
- function confirmCancellation() {
- const reason = document.getElementById('cancel-reason')?.value;
-
- fetch('/api/billing/subscription/cancel', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ reason })
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('Subscription cancelled', 'success');
- closeCancelModal();
- location.reload();
- } else {
- showNotification('Cancellation failed: ' + data.error, 'error');
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
- }
-
- // =============================================================================
- // COMPLIANCE-DASHBOARD.HTML FUNCTIONS
- // =============================================================================
-
- function updateFramework(framework) {
- // Update dashboard for selected compliance framework
- if (typeof htmx !== 'undefined') {
- htmx.ajax('GET', `/api/compliance/dashboard?framework=${framework}`, '#compliance-content');
+ fetch("/api/admin/invitations", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, role }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Invitation sent to " + email, "success");
+ closeInviteMemberModal();
} else {
- fetch(`/api/compliance/dashboard?framework=${framework}`)
- .then(r => r.json())
- .then(data => updateComplianceDashboard(data))
- .catch(err => console.error('Failed to update framework:', err));
+ showNotification("Failed to send invitation: " + data.error, "error");
}
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
+
+ function sendBulkInvitations() {
+ const emailsText = document.getElementById("bulk-emails")?.value || "";
+ const role = document.getElementById("bulk-role")?.value || "member";
+ const emails = emailsText
+ .split(/[\n,;]+/)
+ .map((e) => e.trim())
+ .filter((e) => e);
+
+ if (emails.length === 0) {
+ showNotification("Please enter at least one email address", "error");
+ return;
}
- function updateComplianceDashboard(data) {
- // Update various dashboard elements
- if (data.score) {
- const el = document.getElementById('compliance-score');
- if (el) el.textContent = data.score + '%';
+ fetch("/api/admin/invitations/bulk", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ emails, role }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ showNotification(
+ `${data.sent || emails.length} invitations sent`,
+ "success",
+ );
+ closeBulkInviteModal();
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
+
+ function resendInvitation(invitationId) {
+ fetch(`/api/admin/invitations/${invitationId}/resend`, { method: "POST" })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Invitation resent", "success");
+ } else {
+ showNotification("Failed to resend: " + data.error, "error");
}
- }
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
- function generateComplianceReport() {
- const framework = document.getElementById('complianceFramework')?.value || 'soc2';
- showNotification('Generating compliance report...', 'info');
+ function cancelInvitation(invitationId) {
+ if (!confirm("Cancel this invitation?")) return;
- fetch(`/api/compliance/report?framework=${framework}`)
- .then(response => {
- if (response.ok) return response.blob();
- throw new Error('Report generation failed');
- })
- .then(blob => {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `compliance-report-${framework}.pdf`;
- a.click();
- URL.revokeObjectURL(url);
- showNotification('Report generated', 'success');
- })
- .catch(err => showNotification('Report failed: ' + err.message, 'error'));
- }
-
- function startAuditPrep() {
- showModal('audit-prep-modal');
- }
-
- function closeAuditPrep() {
- hideModal('audit-prep-modal');
- }
-
- function showEvidenceUpload() {
- showModal('evidence-upload-modal');
- }
-
- function closeEvidenceUpload() {
- hideModal('evidence-upload-modal');
- }
-
- function uploadEvidence() {
- const fileInput = document.getElementById('evidence-file');
- const category = document.getElementById('evidence-category')?.value;
-
- if (!fileInput?.files?.length) {
- showNotification('Please select a file', 'error');
- return;
+ fetch(`/api/admin/invitations/${invitationId}`, { method: "DELETE" })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Invitation cancelled", "success");
+ location.reload();
}
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
- const formData = new FormData();
- formData.append('file', fileInput.files[0]);
- formData.append('category', category);
+ // =============================================================================
+ // BILLING-DASHBOARD.HTML FUNCTIONS
+ // =============================================================================
- fetch('/api/compliance/evidence', {
- method: 'POST',
- body: formData
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('Evidence uploaded', 'success');
- closeEvidenceUpload();
- } else {
- showNotification('Upload failed: ' + data.error, 'error');
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
+ function updateBillingPeriod(period) {
+ const params = new URLSearchParams({ period });
+
+ // Update dashboard stats via HTMX or fetch
+ if (typeof htmx !== "undefined") {
+ htmx.ajax("GET", `/api/admin/billing/stats?${params}`, "#billing-stats");
+ } else {
+ fetch(`/api/admin/billing/stats?${params}`)
+ .then((r) => r.json())
+ .then((data) => updateBillingStats(data))
+ .catch((err) => console.error("Failed to update billing period:", err));
}
+ }
- function filterLogs() {
- const category = document.getElementById('logCategory')?.value || 'all';
+ function updateBillingStats(data) {
+ if (data.totalRevenue) {
+ const el = document.getElementById("total-revenue");
+ if (el) el.textContent = formatCurrency(data.totalRevenue);
+ }
+ if (data.activeSubscriptions) {
+ const el = document.getElementById("active-subscriptions");
+ if (el) el.textContent = data.activeSubscriptions;
+ }
+ }
- if (typeof htmx !== 'undefined') {
- htmx.ajax('GET', `/api/compliance/audit-log?category=${category}`, '#audit-log-list');
+ function exportBillingReport() {
+ const period = document.getElementById("billingPeriod")?.value || "current";
+ showNotification("Generating billing report...", "info");
+
+ fetch(`/api/admin/billing/export?period=${period}`)
+ .then((response) => {
+ if (response.ok) return response.blob();
+ throw new Error("Export failed");
+ })
+ .then((blob) => {
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `billing-report-${period}.csv`;
+ a.click();
+ URL.revokeObjectURL(url);
+ showNotification("Report downloaded", "success");
+ })
+ .catch((err) =>
+ showNotification("Export failed: " + err.message, "error"),
+ );
+ }
+
+ function toggleBreakdownView() {
+ const chart = document.getElementById("breakdown-chart");
+ const table = document.getElementById("breakdown-table");
+
+ if (chart && table) {
+ const showingChart = !chart.classList.contains("hidden");
+ chart.classList.toggle("hidden", showingChart);
+ table.classList.toggle("hidden", !showingChart);
+ }
+ }
+
+ function showQuotaSettings() {
+ showModal("quota-settings-modal");
+ }
+
+ function closeQuotaSettings() {
+ hideModal("quota-settings-modal");
+ }
+
+ function saveQuotaSettings() {
+ const form = document.getElementById("quota-form");
+ if (!form) return;
+
+ const formData = new FormData(form);
+ const quotas = Object.fromEntries(formData);
+
+ fetch("/api/admin/billing/quotas", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(quotas),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Quota settings saved", "success");
+ closeQuotaSettings();
+ } else {
+ showNotification("Failed to save: " + data.error, "error");
}
- }
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
- function exportAuditLog() {
- const category = document.getElementById('logCategory')?.value || 'all';
- showNotification('Exporting audit log...', 'info');
+ function configureAlerts() {
+ showModal("alerts-config-modal");
+ }
- fetch(`/api/compliance/audit-log/export?category=${category}`)
- .then(response => {
- if (response.ok) return response.blob();
- throw new Error('Export failed');
- })
- .then(blob => {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'audit-log.csv';
- a.click();
- URL.revokeObjectURL(url);
- showNotification('Audit log exported', 'success');
- })
- .catch(err => showNotification('Export failed: ' + err.message, 'error'));
- }
+ function closeAlertsConfig() {
+ hideModal("alerts-config-modal");
+ }
- // =============================================================================
- // GROUPS.HTML FUNCTIONS
- // =============================================================================
+ function saveAlertSettings() {
+ const form = document.getElementById("alerts-form");
+ if (!form) return;
- function closeDetailPanel() {
- const panel = document.getElementById('detail-panel');
- if (panel) {
- panel.classList.remove('open');
+ const formData = new FormData(form);
+ const settings = Object.fromEntries(formData);
+
+ fetch("/api/admin/billing/alerts", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(settings),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Alert settings saved", "success");
+ closeAlertsConfig();
}
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
+
+ // =============================================================================
+ // BILLING.HTML FUNCTIONS
+ // =============================================================================
+
+ function showUpgradeModal() {
+ showModal("upgrade-modal");
+ }
+
+ function closeUpgradeModal() {
+ hideModal("upgrade-modal");
+ }
+
+ function showCancelModal() {
+ showModal("cancel-modal");
+ }
+
+ function closeCancelModal() {
+ hideModal("cancel-modal");
+ }
+
+ function showAddPaymentModal() {
+ showModal("add-payment-modal");
+ }
+
+ function closeAddPaymentModal() {
+ hideModal("add-payment-modal");
+ }
+
+ function showEditAddressModal() {
+ showModal("edit-address-modal");
+ }
+
+ function closeEditAddressModal() {
+ hideModal("edit-address-modal");
+ }
+
+ function exportInvoices() {
+ showNotification("Exporting invoices...", "info");
+
+ fetch("/api/billing/invoices/export")
+ .then((response) => {
+ if (response.ok) return response.blob();
+ throw new Error("Export failed");
+ })
+ .then((blob) => {
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "invoices.csv";
+ a.click();
+ URL.revokeObjectURL(url);
+ showNotification("Invoices exported", "success");
+ })
+ .catch((err) =>
+ showNotification("Export failed: " + err.message, "error"),
+ );
+ }
+
+ function contactSales() {
+ window.open(
+ "mailto:sales@example.com?subject=Enterprise Plan Inquiry",
+ "_blank",
+ );
+ }
+
+ function showDowngradeOptions() {
+ closeCancelModal();
+ showUpgradeModal();
+ // Focus on lower-tier plans
+ const planSelector = document.querySelector(".plan-options");
+ if (planSelector) {
+ planSelector.scrollIntoView({ behavior: "smooth" });
+ }
+ }
+
+ function selectPlan(planId) {
+ document.querySelectorAll(".plan-option").forEach((el) => {
+ el.classList.toggle("selected", el.dataset.plan === planId);
+ });
+ }
+
+ function confirmUpgrade() {
+ const selectedPlan = document.querySelector(".plan-option.selected");
+ if (!selectedPlan) {
+ showNotification("Please select a plan", "error");
+ return;
}
- function openDetailPanel(groupId) {
- const panel = document.getElementById('detail-panel');
- if (panel) {
- panel.classList.add('open');
- // Load group details
- if (typeof htmx !== 'undefined') {
- htmx.ajax('GET', `/api/admin/groups/${groupId}`, '#panel-content');
- }
+ const planId = selectedPlan.dataset.plan;
+
+ fetch("/api/billing/subscription/upgrade", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ plan_id: planId }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Plan upgraded successfully!", "success");
+ closeUpgradeModal();
+ location.reload();
+ } else {
+ showNotification("Upgrade failed: " + data.error, "error");
}
- }
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
- function createGroup() {
- showModal('create-group-modal');
- }
+ function confirmCancellation() {
+ const reason = document.getElementById("cancel-reason")?.value;
- function closeCreateGroup() {
- hideModal('create-group-modal');
- }
-
- function saveGroup() {
- const name = document.getElementById('group-name')?.value;
- const description = document.getElementById('group-description')?.value;
-
- if (!name) {
- showNotification('Please enter a group name', 'error');
- return;
+ fetch("/api/billing/subscription/cancel", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ reason }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Subscription cancelled", "success");
+ closeCancelModal();
+ location.reload();
+ } else {
+ showNotification("Cancellation failed: " + data.error, "error");
}
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
- fetch('/api/admin/groups', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name, description })
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('Group created', 'success');
- closeCreateGroup();
- location.reload();
- } else {
- showNotification('Failed to create group: ' + data.error, 'error');
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
+ // =============================================================================
+ // COMPLIANCE-DASHBOARD.HTML FUNCTIONS
+ // =============================================================================
+
+ function updateFramework(framework) {
+ // Update dashboard for selected compliance framework
+ if (typeof htmx !== "undefined") {
+ htmx.ajax(
+ "GET",
+ `/api/compliance/dashboard?framework=${framework}`,
+ "#compliance-content",
+ );
+ } else {
+ fetch(`/api/compliance/dashboard?framework=${framework}`)
+ .then((r) => r.json())
+ .then((data) => updateComplianceDashboard(data))
+ .catch((err) => console.error("Failed to update framework:", err));
+ }
+ }
+
+ function updateComplianceDashboard(data) {
+ // Update various dashboard elements
+ if (data.score) {
+ const el = document.getElementById("compliance-score");
+ if (el) el.textContent = data.score + "%";
+ }
+ }
+
+ function generateComplianceReport() {
+ const framework =
+ document.getElementById("complianceFramework")?.value || "soc2";
+ showNotification("Generating compliance report...", "info");
+
+ fetch(`/api/compliance/report?framework=${framework}`)
+ .then((response) => {
+ if (response.ok) return response.blob();
+ throw new Error("Report generation failed");
+ })
+ .then((blob) => {
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `compliance-report-${framework}.pdf`;
+ a.click();
+ URL.revokeObjectURL(url);
+ showNotification("Report generated", "success");
+ })
+ .catch((err) =>
+ showNotification("Report failed: " + err.message, "error"),
+ );
+ }
+
+ function startAuditPrep() {
+ showModal("audit-prep-modal");
+ }
+
+ function closeAuditPrep() {
+ hideModal("audit-prep-modal");
+ }
+
+ function showEvidenceUpload() {
+ showModal("evidence-upload-modal");
+ }
+
+ function closeEvidenceUpload() {
+ hideModal("evidence-upload-modal");
+ }
+
+ function uploadEvidence() {
+ const fileInput = document.getElementById("evidence-file");
+ const category = document.getElementById("evidence-category")?.value;
+
+ if (!fileInput?.files?.length) {
+ showNotification("Please select a file", "error");
+ return;
}
- function deleteGroup(groupId) {
- if (!confirm('Delete this group? This action cannot be undone.')) return;
+ const formData = new FormData();
+ formData.append("file", fileInput.files[0]);
+ formData.append("category", category);
- fetch(`/api/admin/groups/${groupId}`, { method: 'DELETE' })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- showNotification('Group deleted', 'success');
- closeDetailPanel();
- location.reload();
- }
- })
- .catch(err => showNotification('Error: ' + err.message, 'error'));
+ fetch("/api/compliance/evidence", {
+ method: "POST",
+ body: formData,
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Evidence uploaded", "success");
+ closeEvidenceUpload();
+ } else {
+ showNotification("Upload failed: " + data.error, "error");
+ }
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
+
+ function filterLogs() {
+ const category = document.getElementById("logCategory")?.value || "all";
+
+ if (typeof htmx !== "undefined") {
+ htmx.ajax(
+ "GET",
+ `/api/compliance/audit-log?category=${category}`,
+ "#audit-log-list",
+ );
+ }
+ }
+
+ function exportAuditLog() {
+ const category = document.getElementById("logCategory")?.value || "all";
+ showNotification("Exporting audit log...", "info");
+
+ fetch(`/api/compliance/audit-log/export?category=${category}`)
+ .then((response) => {
+ if (response.ok) return response.blob();
+ throw new Error("Export failed");
+ })
+ .then((blob) => {
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "audit-log.csv";
+ a.click();
+ URL.revokeObjectURL(url);
+ showNotification("Audit log exported", "success");
+ })
+ .catch((err) =>
+ showNotification("Export failed: " + err.message, "error"),
+ );
+ }
+
+ // =============================================================================
+ // GROUPS.HTML FUNCTIONS
+ // =============================================================================
+
+ function closeDetailPanel() {
+ const panel = document.getElementById("detail-panel");
+ if (panel) {
+ panel.classList.remove("open");
+ }
+ }
+
+ function openDetailPanel(groupId) {
+ const panel = document.getElementById("detail-panel");
+ if (panel) {
+ panel.classList.add("open");
+ // Load group details
+ if (typeof htmx !== "undefined") {
+ htmx.ajax("GET", `/api/admin/groups/${groupId}`, "#panel-content");
+ }
+ }
+ }
+
+ function createGroup() {
+ showModal("create-group-modal");
+ }
+
+ function closeCreateGroup() {
+ hideModal("create-group-modal");
+ }
+
+ function saveGroup() {
+ const name = document.getElementById("group-name")?.value;
+ const description = document.getElementById("group-description")?.value;
+
+ if (!name) {
+ showNotification("Please enter a group name", "error");
+ return;
}
- // =============================================================================
- // UTILITY FUNCTIONS
- // =============================================================================
+ fetch("/api/admin/groups", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ name, description }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Group created", "success");
+ closeCreateGroup();
+ location.reload();
+ } else {
+ showNotification("Failed to create group: " + data.error, "error");
+ }
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
- function formatCurrency(amount, currency = 'USD') {
- return new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: currency
- }).format(amount);
+ function deleteGroup(groupId) {
+ if (!confirm("Delete this group? This action cannot be undone.")) return;
+
+ fetch(`/api/admin/groups/${groupId}`, { method: "DELETE" })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Group deleted", "success");
+ closeDetailPanel();
+ location.reload();
+ }
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
+
+ // =============================================================================
+ // ROLE MANAGEMENT FUNCTIONS (roles.html)
+ // =============================================================================
+
+ let currentRole = null;
+ let availablePermissions = [];
+ let assignedPermissions = [];
+
+ function selectRole(roleId, element) {
+ // Update UI selection
+ document
+ .querySelectorAll(".role-item")
+ .forEach((item) => item.classList.remove("selected"));
+ if (element) element.classList.add("selected");
+
+ // Show role detail
+ document
+ .getElementById("role-placeholder")
+ ?.style.setProperty("display", "none");
+ document
+ .getElementById("role-detail")
+ ?.style.setProperty("display", "block");
+
+ // Fetch role details
+ fetch(`/api/rbac/roles/${roleId}`)
+ .then((response) => response.json())
+ .then((data) => {
+ currentRole = data;
+ renderRoleDetail(data);
+ })
+ .catch((err) =>
+ showNotification("Failed to load role: " + err.message, "error"),
+ );
+ }
+
+ function renderRoleDetail(role) {
+ document.getElementById("role-display-name").textContent =
+ role.displayName || role.name;
+ document.getElementById("role-name").textContent = role.name;
+
+ const typeBadge = document.getElementById("role-type-badge");
+ if (typeBadge) {
+ typeBadge.textContent = role.isSystem ? "System" : "Custom";
+ typeBadge.className =
+ "role-type-badge " + (role.isSystem ? "system" : "custom");
}
- // =============================================================================
- // EXPORT TO WINDOW
- // =============================================================================
+ // Enable/disable delete button based on system role
+ const deleteBtn = document.getElementById("btn-delete-role");
+ if (deleteBtn) deleteBtn.disabled = role.isSystem;
- // Accounts
- window.showSmtpModal = showSmtpModal;
- window.closeSmtpModal = closeSmtpModal;
- window.testSmtpConnection = testSmtpConnection;
- window.connectAccount = connectAccount;
- window.disconnectAccount = disconnectAccount;
+ // Load permissions
+ loadRolePermissions(role.id);
+ }
- // Admin Dashboard
- window.showInviteMemberModal = showInviteMemberModal;
- window.closeInviteMemberModal = closeInviteMemberModal;
- window.showBulkInviteModal = showBulkInviteModal;
- window.closeBulkInviteModal = closeBulkInviteModal;
- window.sendInvitation = sendInvitation;
- window.sendBulkInvitations = sendBulkInvitations;
- window.resendInvitation = resendInvitation;
- window.cancelInvitation = cancelInvitation;
+ function loadRolePermissions(roleId) {
+ Promise.all([
+ fetch("/api/rbac/permissions").then((r) => r.json()),
+ fetch(`/api/rbac/roles/${roleId}/permissions`).then((r) => r.json()),
+ ])
+ .then(([allPerms, rolePerms]) => {
+ assignedPermissions = rolePerms || [];
+ availablePermissions = (allPerms || []).filter(
+ (p) => !assignedPermissions.find((rp) => rp.id === p.id),
+ );
+ renderPermissionLists();
+ })
+ .catch((err) => console.error("Failed to load permissions:", err));
+ }
- // Billing Dashboard
- window.updateBillingPeriod = updateBillingPeriod;
- window.exportBillingReport = exportBillingReport;
- window.toggleBreakdownView = toggleBreakdownView;
- window.showQuotaSettings = showQuotaSettings;
- window.closeQuotaSettings = closeQuotaSettings;
- window.saveQuotaSettings = saveQuotaSettings;
- window.configureAlerts = configureAlerts;
- window.closeAlertsConfig = closeAlertsConfig;
- window.saveAlertSettings = saveAlertSettings;
+ function renderPermissionLists() {
+ const availableList = document.getElementById("available-permissions");
+ const assignedList = document.getElementById("assigned-permissions");
- // Billing
- window.showUpgradeModal = showUpgradeModal;
- window.closeUpgradeModal = closeUpgradeModal;
- window.showCancelModal = showCancelModal;
- window.closeCancelModal = closeCancelModal;
- window.showAddPaymentModal = showAddPaymentModal;
- window.closeAddPaymentModal = closeAddPaymentModal;
- window.showEditAddressModal = showEditAddressModal;
- window.closeEditAddressModal = closeEditAddressModal;
- window.exportInvoices = exportInvoices;
- window.contactSales = contactSales;
- window.showDowngradeOptions = showDowngradeOptions;
- window.selectPlan = selectPlan;
- window.confirmUpgrade = confirmUpgrade;
- window.confirmCancellation = confirmCancellation;
+ if (availableList) {
+ availableList.innerHTML =
+ availablePermissions
+ .map(
+ (p) => `
+
+ ${p.name}
+ ${p.scope || "global"}
+
+ `,
+ )
+ .join("") ||
+ 'No available permissions
';
+ }
- // Compliance Dashboard
- window.updateFramework = updateFramework;
- window.generateComplianceReport = generateComplianceReport;
- window.startAuditPrep = startAuditPrep;
- window.closeAuditPrep = closeAuditPrep;
- window.showEvidenceUpload = showEvidenceUpload;
- window.closeEvidenceUpload = closeEvidenceUpload;
- window.uploadEvidence = uploadEvidence;
- window.filterLogs = filterLogs;
- window.exportAuditLog = exportAuditLog;
+ if (assignedList) {
+ assignedList.innerHTML =
+ assignedPermissions
+ .map(
+ (p) => `
+
+ ${p.name}
+ ${p.scope || "global"}
+
+ `,
+ )
+ .join("") || 'No assigned permissions
';
+ }
+ }
- // Groups
- window.closeDetailPanel = closeDetailPanel;
- window.openDetailPanel = openDetailPanel;
- window.createGroup = createGroup;
- window.closeCreateGroup = closeCreateGroup;
- window.saveGroup = saveGroup;
- window.deleteGroup = deleteGroup;
+ function togglePermissionSelect(element) {
+ element.classList.toggle("selected");
+ }
+ function assignSelected() {
+ const selected = document.querySelectorAll(
+ "#available-permissions .permission-item.selected",
+ );
+ selected.forEach((item) => {
+ const id = item.dataset.id;
+ const perm = availablePermissions.find((p) => p.id === id);
+ if (perm) {
+ availablePermissions = availablePermissions.filter((p) => p.id !== id);
+ assignedPermissions.push(perm);
+ }
+ });
+ renderPermissionLists();
+ }
+
+ function assignAll() {
+ assignedPermissions = [...assignedPermissions, ...availablePermissions];
+ availablePermissions = [];
+ renderPermissionLists();
+ }
+
+ function removeSelected() {
+ const selected = document.querySelectorAll(
+ "#assigned-permissions .permission-item.selected",
+ );
+ selected.forEach((item) => {
+ const id = item.dataset.id;
+ const perm = assignedPermissions.find((p) => p.id === id);
+ if (perm) {
+ assignedPermissions = assignedPermissions.filter((p) => p.id !== id);
+ availablePermissions.push(perm);
+ }
+ });
+ renderPermissionLists();
+ }
+
+ function removeAll() {
+ availablePermissions = [...availablePermissions, ...assignedPermissions];
+ assignedPermissions = [];
+ renderPermissionLists();
+ }
+
+ function savePermissions() {
+ if (!currentRole) {
+ showNotification("No role selected", "error");
+ return;
+ }
+
+ const permissionIds = assignedPermissions.map((p) => p.id);
+
+ fetch(`/api/rbac/roles/${currentRole.id}/permissions`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ permissions: permissionIds }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Permissions saved successfully", "success");
+ } else {
+ showNotification(
+ "Failed to save permissions: " + (data.error || "Unknown error"),
+ "error",
+ );
+ }
+ })
+ .catch((err) =>
+ showNotification("Error saving permissions: " + err.message, "error"),
+ );
+ }
+
+ function resetPermissions() {
+ if (!currentRole) return;
+ if (
+ !confirm(
+ "Reset permissions to default? This will undo any unsaved changes.",
+ )
+ )
+ return;
+ loadRolePermissions(currentRole.id);
+ showNotification("Permissions reset to saved state", "info");
+ }
+
+ function duplicateRole() {
+ if (!currentRole) {
+ showNotification("No role selected", "error");
+ return;
+ }
+
+ const newName = prompt(
+ "Enter name for the new role:",
+ currentRole.name + "_copy",
+ );
+ if (!newName) return;
+
+ fetch("/api/rbac/roles", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ name: newName,
+ displayName: currentRole.displayName + " (Copy)",
+ description: currentRole.description,
+ permissions: assignedPermissions.map((p) => p.id),
+ }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success || data.id) {
+ showNotification("Role duplicated successfully", "success");
+ location.reload();
+ } else {
+ showNotification(
+ "Failed to duplicate role: " + (data.error || "Unknown error"),
+ "error",
+ );
+ }
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
+
+ function confirmDeleteRole() {
+ if (!currentRole) return;
+ if (currentRole.isSystem) {
+ showNotification("System roles cannot be deleted", "error");
+ return;
+ }
+ showModal("delete-role-modal");
+ }
+
+ function deleteRole() {
+ if (!currentRole) return;
+
+ fetch(`/api/rbac/roles/${currentRole.id}`, { method: "DELETE" })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Role deleted", "success");
+ hideModal("delete-role-modal");
+ location.reload();
+ } else {
+ showNotification(
+ "Failed to delete role: " + (data.error || "Unknown error"),
+ "error",
+ );
+ }
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
+
+ function assignUsersToRole() {
+ const selectedUsers = Array.from(
+ document.querySelectorAll("#user-assign-list input:checked"),
+ ).map((input) => input.value);
+
+ if (selectedUsers.length === 0) {
+ showNotification("Please select at least one user", "error");
+ return;
+ }
+
+ if (!currentRole) {
+ showNotification("No role selected", "error");
+ return;
+ }
+
+ fetch(`/api/rbac/roles/${currentRole.id}/users`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ userIds: selectedUsers }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification(
+ `${selectedUsers.length} user(s) assigned to role`,
+ "success",
+ );
+ hideModal("assign-users-modal");
+ } else {
+ showNotification(
+ "Failed to assign users: " + (data.error || "Unknown error"),
+ "error",
+ );
+ }
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
+
+ function assignGroupsToRole() {
+ const selectedGroups = Array.from(
+ document.querySelectorAll("#group-assign-list input:checked"),
+ ).map((input) => input.value);
+
+ if (selectedGroups.length === 0) {
+ showNotification("Please select at least one group", "error");
+ return;
+ }
+
+ if (!currentRole) {
+ showNotification("No role selected", "error");
+ return;
+ }
+
+ fetch(`/api/rbac/roles/${currentRole.id}/groups`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ groupIds: selectedGroups }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification(
+ `${selectedGroups.length} group(s) assigned to role`,
+ "success",
+ );
+ hideModal("assign-groups-modal");
+ } else {
+ showNotification(
+ "Failed to assign groups: " + (data.error || "Unknown error"),
+ "error",
+ );
+ }
+ })
+ .catch((err) => showNotification("Error: " + err.message, "error"));
+ }
+
+ function filterRoles(type) {
+ const items = document.querySelectorAll(".role-item");
+ items.forEach((item) => {
+ const isSystem = item.dataset.system === "true";
+ if (type === "all") {
+ item.style.display = "";
+ } else if (type === "system") {
+ item.style.display = isSystem ? "" : "none";
+ } else if (type === "custom") {
+ item.style.display = isSystem ? "none" : "";
+ }
+ });
+ }
+
+ // =============================================================================
+ // BILLING ADMIN FUNCTIONS
+ // =============================================================================
+
+ function downloadInvoice(invoiceId) {
+ showNotification(`Downloading invoice ${invoiceId}...`, "info");
+
+ fetch(`/api/billing/invoices/${invoiceId}/download`)
+ .then((response) => {
+ if (response.ok) {
+ return response.blob();
+ }
+ throw new Error("Download failed");
+ })
+ .then((blob) => {
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = `${invoiceId}.pdf`;
+ link.click();
+ URL.revokeObjectURL(url);
+ showNotification("Invoice downloaded", "success");
+ })
+ .catch((err) =>
+ showNotification("Failed to download: " + err.message, "error"),
+ );
+ }
+
+ function dismissAlert(button) {
+ const alertItem = button.closest(".alert-item");
+ if (alertItem) {
+ alertItem.style.opacity = "0";
+ alertItem.style.transform = "translateX(100%)";
+ setTimeout(() => alertItem.remove(), 300);
+ }
+ }
+
+ function viewEvidence(evidenceId) {
+ showNotification(`Loading evidence: ${evidenceId}...`, "info");
+
+ fetch(`/api/compliance/evidence/${evidenceId}`)
+ .then((response) => response.json())
+ .then((data) => {
+ // Show evidence in modal or new window
+ if (data.url) {
+ window.open(data.url, "_blank");
+ } else {
+ showModal("evidence-view-modal");
+ const content = document.getElementById("evidence-content");
+ if (content) {
+ content.innerHTML = `
+ ${data.name || evidenceId}
+ ${data.description || "No description available"}
+
+ Type: ${data.type || "Document"}
+ Uploaded: ${data.uploadedAt || "Unknown"}
+
+ `;
+ }
+ }
+ })
+ .catch((err) =>
+ showNotification("Failed to load evidence: " + err.message, "error"),
+ );
+ }
+
+ // =============================================================================
+ // OPERATIONS DASHBOARD FUNCTIONS
+ // =============================================================================
+
+ let autoRefreshEnabled = true;
+ let autoRefreshInterval = null;
+
+ function toggleAutoRefresh() {
+ autoRefreshEnabled = !autoRefreshEnabled;
+ const label = document.getElementById("autoRefreshLabel");
+ if (label) {
+ label.textContent = `Auto-refresh: ${autoRefreshEnabled ? "ON" : "OFF"}`;
+ }
+
+ if (autoRefreshEnabled) {
+ startAutoRefresh();
+ } else {
+ stopAutoRefresh();
+ }
+ }
+
+ function startAutoRefresh() {
+ if (autoRefreshInterval) clearInterval(autoRefreshInterval);
+ autoRefreshInterval = setInterval(() => {
+ refreshHealth();
+ }, 30000);
+ }
+
+ function stopAutoRefresh() {
+ if (autoRefreshInterval) {
+ clearInterval(autoRefreshInterval);
+ autoRefreshInterval = null;
+ }
+ }
+
+ function showAlertConfig() {
+ showModal("alert-config-modal");
+ }
+
+ function closeAlertConfig() {
+ hideModal("alert-config-modal");
+ }
+
+ function showTraceDetail(traceId) {
+ showNotification(`Loading trace ${traceId}...`, "info");
+
+ fetch(`/api/ops/traces/${traceId}`)
+ .then((response) => response.json())
+ .then((data) => {
+ showModal("trace-detail-modal");
+ const content = document.getElementById("trace-detail-content");
+ if (content) {
+ content.innerHTML = `
+
+
+
+ Trace ID
+ ${data.traceId || traceId}
+
+
+ Duration
+ ${data.duration || "0"}ms
+
+
+ Spans
+ ${data.spanCount || 0}
+
+
+ Service
+ ${data.service || "unknown"}
+
+
+
+ ${(data.spans || [])
+ .map(
+ (span) => `
+
+ ${span.name}
+ ${span.duration}ms
+
+ `,
+ )
+ .join("")}
+
+ `;
+ }
+ })
+ .catch((err) =>
+ showNotification("Failed to load trace: " + err.message, "error"),
+ );
+ }
+
+ function refreshHealth() {
+ const healthGrid = document.querySelector(".health-grid");
+ if (healthGrid) {
+ healthGrid.innerHTML =
+ '';
+ }
+
+ fetch("/api/ops/health")
+ .then((response) => response.json())
+ .then((data) => {
+ if (healthGrid) {
+ healthGrid.innerHTML =
+ (data.services || [])
+ .map(
+ (service) => `
+
+
+
+ ${service.name}
+
+
+ Latency: ${service.latency || 0}ms
+ Uptime: ${service.uptime || "100%"}
+
+
+ `,
+ )
+ .join("") || 'No services found
';
+ }
+ })
+ .catch((err) => {
+ if (healthGrid) {
+ healthGrid.innerHTML =
+ 'Failed to load health data
';
+ }
+ });
+ }
+
+ // =============================================================================
+ // ONBOARDING FUNCTIONS
+ // =============================================================================
+
+ let currentStep = 1;
+
+ function nextStep(step) {
+ const currentPanel = document.querySelector(
+ `.onboarding-panel[data-step="${step}"]`,
+ );
+ const nextPanel = document.querySelector(
+ `.onboarding-panel[data-step="${step + 1}"]`,
+ );
+
+ if (currentPanel) currentPanel.classList.remove("active");
+ if (nextPanel) nextPanel.classList.add("active");
+
+ currentStep = step + 1;
+ updateStepIndicators();
+ }
+
+ function prevStep(step) {
+ const currentPanel = document.querySelector(
+ `.onboarding-panel[data-step="${step}"]`,
+ );
+ const prevPanel = document.querySelector(
+ `.onboarding-panel[data-step="${step - 1}"]`,
+ );
+
+ if (currentPanel) currentPanel.classList.remove("active");
+ if (prevPanel) prevPanel.classList.add("active");
+
+ currentStep = step - 1;
+ updateStepIndicators();
+ }
+
+ function updateStepIndicators() {
+ document.querySelectorAll(".step-indicator").forEach((indicator, index) => {
+ const stepNum = index + 1;
+ indicator.classList.remove("active", "completed");
+ if (stepNum < currentStep) {
+ indicator.classList.add("completed");
+ } else if (stepNum === currentStep) {
+ indicator.classList.add("active");
+ }
+ });
+ }
+
+ function skipPayment() {
+ showNotification("Skipping payment setup - Free plan selected", "info");
+ nextStep(4);
+ }
+
+ function removeLogo() {
+ const logoPreview = document.getElementById("logo-preview");
+ const logoInput = document.getElementById("logo-input");
+
+ if (logoPreview) {
+ logoPreview.innerHTML =
+ 'No logo uploaded ';
+ }
+ if (logoInput) {
+ logoInput.value = "";
+ }
+ showNotification("Logo removed", "success");
+ }
+
+ function previewLogo(input) {
+ if (input.files && input.files[0]) {
+ const reader = new FileReader();
+ reader.onload = function (e) {
+ const preview = document.getElementById("logo-preview");
+ if (preview) {
+ preview.innerHTML = ` `;
+ }
+ };
+ reader.readAsDataURL(input.files[0]);
+ }
+ }
+
+ // =============================================================================
+ // ACCOUNTS LIST FUNCTIONS (sources/accounts-list.html)
+ // =============================================================================
+
+ function editAccount(accountId) {
+ showNotification(`Loading account ${accountId}...`, "info");
+
+ fetch(`/api/sources/accounts/${accountId}`)
+ .then((response) => response.json())
+ .then((data) => {
+ showModal("edit-account-modal");
+ // Populate form fields
+ const form = document.getElementById("edit-account-form");
+ if (form) {
+ form
+ .querySelector('[name="account-name"]')
+ ?.setAttribute("value", data.name || "");
+ form
+ .querySelector('[name="account-email"]')
+ ?.setAttribute("value", data.email || "");
+ }
+ })
+ .catch((err) =>
+ showNotification("Failed to load account: " + err.message, "error"),
+ );
+ }
+
+ function syncAccount(accountId) {
+ showNotification(`Syncing account ${accountId}...`, "info");
+
+ fetch(`/api/sources/accounts/${accountId}/sync`, { method: "POST" })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Account sync started", "success");
+ } else {
+ showNotification(
+ "Sync failed: " + (data.error || "Unknown error"),
+ "error",
+ );
+ }
+ })
+ .catch((err) => showNotification("Sync error: " + err.message, "error"));
+ }
+
+ // =============================================================================
+ // SEARCH SETTINGS FUNCTIONS (admin/search-settings.html)
+ // =============================================================================
+
+ function openReindexModal() {
+ const modal = document.getElementById("reindex-modal");
+ if (modal) {
+ modal.showModal();
+ }
+ }
+
+ function closeReindexModal() {
+ const modal = document.getElementById("reindex-modal");
+ if (modal) {
+ modal.close();
+ }
+ }
+
+ function startReindex() {
+ showNotification("Starting reindex operation...", "info");
+
+ fetch("/api/ui/sources/kb/reindex", { method: "POST" })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Reindex started successfully", "success");
+ closeReindexModal();
+ refreshStats();
+ } else {
+ showNotification(
+ "Reindex failed: " + (data.error || "Unknown error"),
+ "error",
+ );
+ }
+ })
+ .catch((err) =>
+ showNotification("Reindex error: " + err.message, "error"),
+ );
+ }
+
+ function refreshStats() {
+ showNotification("Refreshing search statistics...", "info");
+
+ fetch("/api/ui/sources/kb/stats")
+ .then((response) => response.json())
+ .then((data) => {
+ // Update stats display
+ const totalDocs = document.getElementById("total-documents");
+ const indexSize = document.getElementById("index-size");
+ const lastIndexed = document.getElementById("last-indexed");
+
+ if (totalDocs) totalDocs.textContent = data.totalDocuments || "0";
+ if (indexSize) indexSize.textContent = data.indexSize || "0 MB";
+ if (lastIndexed) lastIndexed.textContent = data.lastIndexed || "Never";
+
+ showNotification("Statistics updated", "success");
+ })
+ .catch((err) =>
+ showNotification("Failed to refresh stats: " + err.message, "error"),
+ );
+ }
+
+ function saveSearchSettings() {
+ const form = document.getElementById("search-settings-form");
+ if (!form) {
+ showNotification("Settings form not found", "error");
+ return;
+ }
+
+ const formData = new FormData(form);
+ const settings = Object.fromEntries(formData.entries());
+
+ showNotification("Saving search settings...", "info");
+
+ fetch("/api/settings/search", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(settings),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.success) {
+ showNotification("Search settings saved successfully", "success");
+ } else {
+ showNotification(
+ "Failed to save settings: " + (data.error || "Unknown error"),
+ "error",
+ );
+ }
+ })
+ .catch((err) => showNotification("Save error: " + err.message, "error"));
+ }
+
+ // =============================================================================
+ // UTILITY FUNCTIONS
+ // =============================================================================
+
+ function formatCurrency(amount, currency = "USD") {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: currency,
+ }).format(amount);
+ }
+
+ // =============================================================================
+ // EXPORT TO WINDOW
+ // =============================================================================
+
+ // Accounts
+ window.showSmtpModal = showSmtpModal;
+ window.closeSmtpModal = closeSmtpModal;
+ window.testSmtpConnection = testSmtpConnection;
+ window.connectAccount = connectAccount;
+ window.disconnectAccount = disconnectAccount;
+
+ // Admin Dashboard
+ window.showInviteMemberModal = showInviteMemberModal;
+ window.closeInviteMemberModal = closeInviteMemberModal;
+ window.showBulkInviteModal = showBulkInviteModal;
+ window.closeBulkInviteModal = closeBulkInviteModal;
+ window.sendInvitation = sendInvitation;
+ window.sendBulkInvitations = sendBulkInvitations;
+ window.resendInvitation = resendInvitation;
+ window.cancelInvitation = cancelInvitation;
+
+ // Billing Dashboard
+ window.updateBillingPeriod = updateBillingPeriod;
+ window.exportBillingReport = exportBillingReport;
+ window.toggleBreakdownView = toggleBreakdownView;
+ window.showQuotaSettings = showQuotaSettings;
+ window.closeQuotaSettings = closeQuotaSettings;
+ window.saveQuotaSettings = saveQuotaSettings;
+ window.configureAlerts = configureAlerts;
+ window.closeAlertsConfig = closeAlertsConfig;
+ window.saveAlertSettings = saveAlertSettings;
+
+ // Billing
+ window.showUpgradeModal = showUpgradeModal;
+ window.closeUpgradeModal = closeUpgradeModal;
+ window.showCancelModal = showCancelModal;
+ window.closeCancelModal = closeCancelModal;
+ window.showAddPaymentModal = showAddPaymentModal;
+ window.closeAddPaymentModal = closeAddPaymentModal;
+ window.showEditAddressModal = showEditAddressModal;
+ window.closeEditAddressModal = closeEditAddressModal;
+ window.exportInvoices = exportInvoices;
+ window.contactSales = contactSales;
+ window.showDowngradeOptions = showDowngradeOptions;
+ window.selectPlan = selectPlan;
+ window.confirmUpgrade = confirmUpgrade;
+ window.confirmCancellation = confirmCancellation;
+
+ // Compliance Dashboard
+ window.updateFramework = updateFramework;
+ window.generateComplianceReport = generateComplianceReport;
+ window.startAuditPrep = startAuditPrep;
+ window.closeAuditPrep = closeAuditPrep;
+ window.showEvidenceUpload = showEvidenceUpload;
+ window.closeEvidenceUpload = closeEvidenceUpload;
+ window.uploadEvidence = uploadEvidence;
+ window.filterLogs = filterLogs;
+ window.exportAuditLog = exportAuditLog;
+
+ // Groups
+ window.closeDetailPanel = closeDetailPanel;
+ window.openDetailPanel = openDetailPanel;
+ window.createGroup = createGroup;
+ window.closeCreateGroup = closeCreateGroup;
+ window.saveGroup = saveGroup;
+ window.deleteGroup = deleteGroup;
+
+ // Role Management
+ window.selectRole = selectRole;
+ window.assignSelected = assignSelected;
+ window.assignAll = assignAll;
+ window.removeSelected = removeSelected;
+ window.removeAll = removeAll;
+ window.savePermissions = savePermissions;
+ window.resetPermissions = resetPermissions;
+ window.duplicateRole = duplicateRole;
+ window.confirmDeleteRole = confirmDeleteRole;
+ window.deleteRole = deleteRole;
+ window.assignUsersToRole = assignUsersToRole;
+ window.assignGroupsToRole = assignGroupsToRole;
+ window.filterRoles = filterRoles;
+
+ // Billing Admin
+ window.downloadInvoice = downloadInvoice;
+ window.dismissAlert = dismissAlert;
+ window.viewEvidence = viewEvidence;
+
+ // Operations Dashboard
+ window.toggleAutoRefresh = toggleAutoRefresh;
+ window.showAlertConfig = showAlertConfig;
+ window.closeAlertConfig = closeAlertConfig;
+ window.showTraceDetail = showTraceDetail;
+ window.refreshHealth = refreshHealth;
+
+ // Onboarding
+ window.nextStep = nextStep;
+ window.prevStep = prevStep;
+ window.skipPayment = skipPayment;
+ window.removeLogo = removeLogo;
+ window.previewLogo = previewLogo;
+
+ // Account Management
+ window.editAccount = editAccount;
+ window.syncAccount = syncAccount;
+
+ // Search Settings
+ window.openReindexModal = openReindexModal;
+ window.closeReindexModal = closeReindexModal;
+ window.startReindex = startReindex;
+ window.refreshStats = refreshStats;
+ window.saveSearchSettings = saveSearchSettings;
})();
diff --git a/ui/suite/canvas/canvas.js b/ui/suite/canvas/canvas.js
index 23b5a3a..b029387 100644
--- a/ui/suite/canvas/canvas.js
+++ b/ui/suite/canvas/canvas.js
@@ -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) => `
+
+ ${layer.visible ? "👁️" : "👁️🗨️"}
+ ${layer.name}
+ ${layer.locked ? "🔒" : "🔓"}
+
+ `,
+ )
+ .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
// =============================================================================
diff --git a/ui/suite/chat/chat.css b/ui/suite/chat/chat.css
index 2f949fc..8b086b4 100644
--- a/ui/suite/chat/chat.css
+++ b/ui/suite/chat/chat.css
@@ -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 {
diff --git a/ui/suite/drive/drive-sentient.js b/ui/suite/drive/drive-sentient.js
new file mode 100644
index 0000000..26009d9
--- /dev/null
+++ b/ui/suite/drive/drive-sentient.js
@@ -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 = '' + escapeHtml(content) + "
";
+ 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 = `
+ ${icon}
+ ${escapeHtml(name)}
+ ${size}
+ `;
+ 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();
+ }
+})();
diff --git a/ui/suite/learn/learn.js b/ui/suite/learn/learn.js
index a7c9c5e..c729b5a 100644
--- a/ui/suite/learn/learn.js
+++ b/ui/suite/learn/learn.js
@@ -5,121 +5,123 @@
// State management
const LearnState = {
- courses: [],
- myCourses: [],
- mandatoryAssignments: [],
- certificates: [],
- categories: [],
- currentCourse: null,
- currentLesson: null,
- currentQuiz: null,
- quizAnswers: {},
- quizTimer: null,
- quizTimeRemaining: 0,
- currentQuestionIndex: 0,
- filters: {
- category: 'all',
- difficulty: ['beginner', 'intermediate', 'advanced'],
- search: '',
- sort: 'recent'
- },
- pagination: {
- offset: 0,
- limit: 12,
- hasMore: true
- },
- userStats: {
- coursesCompleted: 0,
- coursesInProgress: 0,
- certificates: 0,
- timeSpent: 0
- }
+ courses: [],
+ myCourses: [],
+ mandatoryAssignments: [],
+ certificates: [],
+ categories: [],
+ currentCourse: null,
+ currentLesson: null,
+ currentQuiz: null,
+ quizAnswers: {},
+ quizTimer: null,
+ quizTimeRemaining: 0,
+ currentQuestionIndex: 0,
+ filters: {
+ category: "all",
+ difficulty: ["beginner", "intermediate", "advanced"],
+ search: "",
+ sort: "recent",
+ },
+ pagination: {
+ offset: 0,
+ limit: 12,
+ hasMore: true,
+ },
+ userStats: {
+ coursesCompleted: 0,
+ coursesInProgress: 0,
+ certificates: 0,
+ timeSpent: 0,
+ },
};
// API Base URL
-const LEARN_API = '/api/learn';
+const LEARN_API = "/api/learn";
// ============================================================================
// INITIALIZATION
// ============================================================================
-document.addEventListener('DOMContentLoaded', () => {
- initLearn();
+document.addEventListener("DOMContentLoaded", () => {
+ initLearn();
});
function initLearn() {
- loadUserStats();
- loadCategories();
- loadCourses();
- loadMyCourses();
- loadMandatoryAssignments();
- loadCertificates();
- loadRecommendations();
- bindEvents();
+ loadUserStats();
+ loadCategories();
+ loadCourses();
+ loadMyCourses();
+ loadMandatoryAssignments();
+ loadCertificates();
+ loadRecommendations();
+ bindEvents();
}
function bindEvents() {
- // Search input
- const searchInput = document.getElementById('searchCourses');
- if (searchInput) {
- let searchTimeout;
- searchInput.addEventListener('input', (e) => {
- clearTimeout(searchTimeout);
- searchTimeout = setTimeout(() => {
- LearnState.filters.search = e.target.value;
- LearnState.pagination.offset = 0;
- loadCourses();
- }, 300);
- });
+ // Search input
+ const searchInput = document.getElementById("searchCourses");
+ if (searchInput) {
+ let searchTimeout;
+ searchInput.addEventListener("input", (e) => {
+ clearTimeout(searchTimeout);
+ searchTimeout = setTimeout(() => {
+ LearnState.filters.search = e.target.value;
+ LearnState.pagination.offset = 0;
+ loadCourses();
+ }, 300);
+ });
+ }
+
+ // Sort select
+ const sortSelect = document.getElementById("sortCourses");
+ if (sortSelect) {
+ sortSelect.addEventListener("change", (e) => {
+ LearnState.filters.sort = e.target.value;
+ LearnState.pagination.offset = 0;
+ loadCourses();
+ });
+ }
+
+ // Category filters
+ document.querySelectorAll(".category-item").forEach((item) => {
+ item.addEventListener("click", () => {
+ document
+ .querySelectorAll(".category-item")
+ .forEach((i) => i.classList.remove("active"));
+ item.classList.add("active");
+ LearnState.filters.category = item.dataset.category;
+ LearnState.pagination.offset = 0;
+ loadCourses();
+ });
+ });
+
+ // Difficulty filters
+ document.querySelectorAll("[data-difficulty]").forEach((checkbox) => {
+ checkbox.addEventListener("change", () => {
+ LearnState.filters.difficulty = Array.from(
+ document.querySelectorAll("[data-difficulty]:checked"),
+ ).map((cb) => cb.dataset.difficulty);
+ LearnState.pagination.offset = 0;
+ loadCourses();
+ });
+ });
+
+ // Close modals on background click
+ document.querySelectorAll(".modal").forEach((modal) => {
+ modal.addEventListener("click", (e) => {
+ if (e.target === modal) {
+ modal.classList.add("hidden");
+ }
+ });
+ });
+
+ // Keyboard shortcuts
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ closeAllModals();
}
-
- // Sort select
- const sortSelect = document.getElementById('sortCourses');
- if (sortSelect) {
- sortSelect.addEventListener('change', (e) => {
- LearnState.filters.sort = e.target.value;
- LearnState.pagination.offset = 0;
- loadCourses();
- });
- }
-
- // Category filters
- document.querySelectorAll('.category-item').forEach(item => {
- item.addEventListener('click', () => {
- document.querySelectorAll('.category-item').forEach(i => i.classList.remove('active'));
- item.classList.add('active');
- LearnState.filters.category = item.dataset.category;
- LearnState.pagination.offset = 0;
- loadCourses();
- });
- });
-
- // Difficulty filters
- document.querySelectorAll('[data-difficulty]').forEach(checkbox => {
- checkbox.addEventListener('change', () => {
- LearnState.filters.difficulty = Array.from(
- document.querySelectorAll('[data-difficulty]:checked')
- ).map(cb => cb.dataset.difficulty);
- LearnState.pagination.offset = 0;
- loadCourses();
- });
- });
-
- // Close modals on background click
- document.querySelectorAll('.modal').forEach(modal => {
- modal.addEventListener('click', (e) => {
- if (e.target === modal) {
- modal.classList.add('hidden');
- }
- });
- });
-
- // Keyboard shortcuts
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') {
- closeAllModals();
- }
- });
+ });
}
// ============================================================================
@@ -127,192 +129,193 @@ function bindEvents() {
// ============================================================================
async function apiCall(endpoint, options = {}) {
- try {
- const response = await fetch(`${LEARN_API}${endpoint}`, {
- headers: {
- 'Content-Type': 'application/json',
- ...options.headers
- },
- ...options
- });
+ try {
+ const response = await fetch(`${LEARN_API}${endpoint}`, {
+ headers: {
+ "Content-Type": "application/json",
+ ...options.headers,
+ },
+ ...options,
+ });
- const data = await response.json();
+ const data = await response.json();
- if (!response.ok) {
- throw new Error(data.error || 'API request failed');
- }
-
- return data;
- } catch (error) {
- console.error('API Error:', error);
- showNotification('error', 'Erro ao carregar dados. Tente novamente.');
- throw error;
+ if (!response.ok) {
+ throw new Error(data.error || "API request failed");
}
+
+ return data;
+ } catch (error) {
+ console.error("API Error:", error);
+ showNotification("error", "Erro ao carregar dados. Tente novamente.");
+ throw error;
+ }
}
async function loadUserStats() {
- try {
- const response = await apiCall('/stats/user');
- if (response.success) {
- LearnState.userStats = response.data;
- updateUserStatsUI();
- }
- } catch (error) {
- // Use mock data if API fails
- updateUserStatsUI();
+ try {
+ const response = await apiCall("/stats/user");
+ if (response.success) {
+ LearnState.userStats = response.data;
+ updateUserStatsUI();
}
+ } catch (error) {
+ // Use mock data if API fails
+ updateUserStatsUI();
+ }
}
async function loadCategories() {
- try {
- const response = await apiCall('/categories');
- if (response.success) {
- LearnState.categories = response.data;
- updateCategoryCounts();
- }
- } catch (error) {
- // Categories loaded from HTML
+ try {
+ const response = await apiCall("/categories");
+ if (response.success) {
+ LearnState.categories = response.data;
+ updateCategoryCounts();
}
+ } catch (error) {
+ // Categories loaded from HTML
+ }
}
async function loadCourses() {
- try {
- const params = new URLSearchParams({
- limit: LearnState.pagination.limit,
- offset: LearnState.pagination.offset
- });
+ try {
+ const params = new URLSearchParams({
+ limit: LearnState.pagination.limit,
+ offset: LearnState.pagination.offset,
+ });
- if (LearnState.filters.category !== 'all') {
- params.append('category', LearnState.filters.category);
- }
-
- if (LearnState.filters.search) {
- params.append('search', LearnState.filters.search);
- }
-
- if (LearnState.filters.difficulty.length < 3) {
- params.append('difficulty', LearnState.filters.difficulty.join(','));
- }
-
- const response = await apiCall(`/courses?${params}`);
-
- if (response.success) {
- if (LearnState.pagination.offset === 0) {
- LearnState.courses = response.data;
- } else {
- LearnState.courses = [...LearnState.courses, ...response.data];
- }
-
- LearnState.pagination.hasMore = response.data.length >= LearnState.pagination.limit;
- renderCourses();
- }
- } catch (error) {
- // Render mock courses for demo
- renderMockCourses();
+ if (LearnState.filters.category !== "all") {
+ params.append("category", LearnState.filters.category);
}
+
+ if (LearnState.filters.search) {
+ params.append("search", LearnState.filters.search);
+ }
+
+ if (LearnState.filters.difficulty.length < 3) {
+ params.append("difficulty", LearnState.filters.difficulty.join(","));
+ }
+
+ const response = await apiCall(`/courses?${params}`);
+
+ if (response.success) {
+ if (LearnState.pagination.offset === 0) {
+ LearnState.courses = response.data;
+ } else {
+ LearnState.courses = [...LearnState.courses, ...response.data];
+ }
+
+ LearnState.pagination.hasMore =
+ response.data.length >= LearnState.pagination.limit;
+ renderCourses();
+ }
+ } catch (error) {
+ // Render mock courses for demo
+ renderMockCourses();
+ }
}
async function loadMyCourses() {
- try {
- const response = await apiCall('/progress');
- if (response.success) {
- LearnState.myCourses = response.data;
- renderMyCourses();
- }
- } catch (error) {
- renderMockMyCourses();
+ try {
+ const response = await apiCall("/progress");
+ if (response.success) {
+ LearnState.myCourses = response.data;
+ renderMyCourses();
}
+ } catch (error) {
+ renderMockMyCourses();
+ }
}
async function loadMandatoryAssignments() {
- try {
- const response = await apiCall('/assignments/pending');
- if (response.success) {
- LearnState.mandatoryAssignments = response.data;
- renderMandatoryAssignments();
- updateMandatoryAlert();
- }
- } catch (error) {
- renderMockMandatory();
+ try {
+ const response = await apiCall("/assignments/pending");
+ if (response.success) {
+ LearnState.mandatoryAssignments = response.data;
+ renderMandatoryAssignments();
+ updateMandatoryAlert();
}
+ } catch (error) {
+ renderMockMandatory();
+ }
}
async function loadCertificates() {
- try {
- const response = await apiCall('/certificates');
- if (response.success) {
- LearnState.certificates = response.data;
- renderCertificates();
- }
- } catch (error) {
- renderMockCertificates();
+ try {
+ const response = await apiCall("/certificates");
+ if (response.success) {
+ LearnState.certificates = response.data;
+ renderCertificates();
}
+ } catch (error) {
+ renderMockCertificates();
+ }
}
async function loadRecommendations() {
- try {
- const response = await apiCall('/recommendations');
- if (response.success) {
- renderRecommendations(response.data);
- }
- } catch (error) {
- renderMockRecommendations();
+ try {
+ const response = await apiCall("/recommendations");
+ if (response.success) {
+ renderRecommendations(response.data);
}
+ } catch (error) {
+ renderMockRecommendations();
+ }
}
async function loadCourseDetail(courseId) {
- try {
- const response = await apiCall(`/courses/${courseId}`);
- if (response.success) {
- LearnState.currentCourse = response.data;
- renderCourseModal(response.data);
- }
- } catch (error) {
- showMockCourseDetail(courseId);
+ try {
+ const response = await apiCall(`/courses/${courseId}`);
+ if (response.success) {
+ LearnState.currentCourse = response.data;
+ renderCourseModal(response.data);
}
+ } catch (error) {
+ showMockCourseDetail(courseId);
+ }
}
async function startCourseAPI(courseId) {
- try {
- const response = await apiCall(`/progress/${courseId}/start`, {
- method: 'POST'
- });
- if (response.success) {
- showNotification('success', 'Curso iniciado com sucesso!');
- loadMyCourses();
- return response.data;
- }
- } catch (error) {
- showNotification('error', 'Erro ao iniciar o curso.');
+ try {
+ const response = await apiCall(`/progress/${courseId}/start`, {
+ method: "POST",
+ });
+ if (response.success) {
+ showNotification("success", "Curso iniciado com sucesso!");
+ loadMyCourses();
+ return response.data;
}
+ } catch (error) {
+ showNotification("error", "Erro ao iniciar o curso.");
+ }
}
async function completeLessonAPI(lessonId) {
- try {
- const response = await apiCall(`/progress/${lessonId}/complete`, {
- method: 'POST'
- });
- if (response.success) {
- showNotification('success', 'Aula concluída!');
- return response.data;
- }
- } catch (error) {
- showNotification('error', 'Erro ao marcar aula como concluída.');
+ try {
+ const response = await apiCall(`/progress/${lessonId}/complete`, {
+ method: "POST",
+ });
+ if (response.success) {
+ showNotification("success", "Aula concluída!");
+ return response.data;
}
+ } catch (error) {
+ showNotification("error", "Erro ao marcar aula como concluída.");
+ }
}
async function submitQuizAPI(courseId, answers) {
- try {
- const response = await apiCall(`/courses/${courseId}/quiz`, {
- method: 'POST',
- body: JSON.stringify({ answers })
- });
- if (response.success) {
- return response.data;
- }
- } catch (error) {
- showNotification('error', 'Erro ao enviar respostas.');
+ try {
+ const response = await apiCall(`/courses/${courseId}/quiz`, {
+ method: "POST",
+ body: JSON.stringify({ answers }),
+ });
+ if (response.success) {
+ return response.data;
}
+ } catch (error) {
+ showNotification("error", "Erro ao enviar respostas.");
+ }
}
// ============================================================================
@@ -320,74 +323,80 @@ async function submitQuizAPI(courseId, answers) {
// ============================================================================
function updateUserStatsUI() {
- document.getElementById('statCoursesCompleted').textContent = LearnState.userStats.courses_completed || 0;
- document.getElementById('statCoursesInProgress').textContent = LearnState.userStats.courses_in_progress || 0;
- document.getElementById('statCertificates').textContent = LearnState.userStats.certificates_earned || 0;
- document.getElementById('statTimeSpent').textContent = `${LearnState.userStats.total_time_spent_hours || 0}h`;
+ document.getElementById("statCoursesCompleted").textContent =
+ LearnState.userStats.courses_completed || 0;
+ document.getElementById("statCoursesInProgress").textContent =
+ LearnState.userStats.courses_in_progress || 0;
+ document.getElementById("statCertificates").textContent =
+ LearnState.userStats.certificates_earned || 0;
+ document.getElementById("statTimeSpent").textContent =
+ `${LearnState.userStats.total_time_spent_hours || 0}h`;
}
function updateCategoryCounts() {
- // Update category counts based on courses
- const counts = {};
- LearnState.courses.forEach(course => {
- counts[course.category] = (counts[course.category] || 0) + 1;
- });
+ // Update category counts based on courses
+ const counts = {};
+ LearnState.courses.forEach((course) => {
+ counts[course.category] = (counts[course.category] || 0) + 1;
+ });
- document.getElementById('countAll').textContent = LearnState.courses.length;
- document.getElementById('countMandatory').textContent = LearnState.mandatoryAssignments.length;
+ document.getElementById("countAll").textContent = LearnState.courses.length;
+ document.getElementById("countMandatory").textContent =
+ LearnState.mandatoryAssignments.length;
}
function renderCourses() {
- const grid = document.getElementById('coursesGrid');
- const countLabel = document.getElementById('coursesCountLabel');
+ const grid = document.getElementById("coursesGrid");
+ const countLabel = document.getElementById("coursesCountLabel");
- if (!grid) return;
+ if (!grid) return;
- if (LearnState.pagination.offset === 0) {
- grid.innerHTML = '';
- }
+ if (LearnState.pagination.offset === 0) {
+ grid.innerHTML = "";
+ }
- if (LearnState.courses.length === 0) {
- grid.innerHTML = `
+ if (LearnState.courses.length === 0) {
+ grid.innerHTML = `
📚
Nenhum curso encontrado
Tente ajustar os filtros de busca.
`;
- countLabel.textContent = '0 cursos';
- return;
- }
+ countLabel.textContent = "0 cursos";
+ return;
+ }
- LearnState.courses.forEach(course => {
- grid.appendChild(createCourseCard(course));
- });
+ LearnState.courses.forEach((course) => {
+ grid.appendChild(createCourseCard(course));
+ });
- countLabel.textContent = `${LearnState.courses.length} cursos`;
+ countLabel.textContent = `${LearnState.courses.length} cursos`;
- // Show/hide load more button
- const loadMore = document.getElementById('loadMore');
- if (loadMore) {
- loadMore.style.display = LearnState.pagination.hasMore ? 'block' : 'none';
- }
+ // Show/hide load more button
+ const loadMore = document.getElementById("loadMore");
+ if (loadMore) {
+ loadMore.style.display = LearnState.pagination.hasMore ? "block" : "none";
+ }
}
function createCourseCard(course) {
- const card = document.createElement('div');
- card.className = 'course-card';
- card.onclick = () => openCourseModal(course.id);
+ const card = document.createElement("div");
+ card.className = "course-card";
+ card.onclick = () => openCourseModal(course.id);
- const difficultyClass = (course.difficulty || 'beginner').toLowerCase();
- const progress = course.user_progress || 0;
+ const difficultyClass = (course.difficulty || "beginner").toLowerCase();
+ const progress = course.user_progress || 0;
- card.innerHTML = `
+ card.innerHTML = `
- ${course.thumbnail_url
+ ${
+ course.thumbnail_url
? `
`
: `
📖 `
}
- ${course.is_mandatory ? '
Obrigatório ' : ''}
- ${progress > 0 ? `
${progress}% ` : ''}
+ ${course.is_mandatory ? '
Obrigatório ' : ""}
+ ${progress > 0 ? `
${progress}% ` : ""}
${escapeHtml(course.title)}
@@ -401,65 +410,77 @@ function createCourseCard(course) {
${formatDuration(course.duration_minutes)}
- ${progress > 0 ? `
+ ${
+ progress > 0
+ ? `
- ` : ''}
+ `
+ : ""
+ }
`;
- return card;
+ return card;
}
function renderMyCourses() {
- const continueLearning = document.getElementById('continueLearning');
- const completedCourses = document.getElementById('completedCourses');
+ const continueLearning = document.getElementById("continueLearning");
+ const completedCourses = document.getElementById("completedCourses");
- if (!continueLearning || !completedCourses) return;
+ if (!continueLearning || !completedCourses) return;
- const inProgress = LearnState.myCourses.filter(c => c.status === 'in_progress');
- const completed = LearnState.myCourses.filter(c => c.status === 'completed');
+ const inProgress = LearnState.myCourses.filter(
+ (c) => c.status === "in_progress",
+ );
+ const completed = LearnState.myCourses.filter(
+ (c) => c.status === "completed",
+ );
- // Update badge
- document.getElementById('myCoursesCount').textContent = inProgress.length;
+ // Update badge
+ document.getElementById("myCoursesCount").textContent = inProgress.length;
- // Render in-progress courses
- if (inProgress.length === 0) {
- continueLearning.innerHTML = `
+ // Render in-progress courses
+ if (inProgress.length === 0) {
+ continueLearning.innerHTML = `
📚
Nenhum curso em andamento
`;
- } else {
- continueLearning.innerHTML = inProgress.map(course => createCourseListItem(course)).join('');
- }
+ } else {
+ continueLearning.innerHTML = inProgress
+ .map((course) => createCourseListItem(course))
+ .join("");
+ }
- // Render completed courses
- if (completed.length === 0) {
- completedCourses.innerHTML = `
+ // Render completed courses
+ if (completed.length === 0) {
+ completedCourses.innerHTML = `
✅
Nenhum curso concluído ainda
`;
- } else {
- completedCourses.innerHTML = completed.map(course => createCourseListItem(course, true)).join('');
- }
+ } else {
+ completedCourses.innerHTML = completed
+ .map((course) => createCourseListItem(course, true))
+ .join("");
+ }
}
function createCourseListItem(course, isCompleted = false) {
- return `
+ return `
📖
-
${escapeHtml(course.course_title || course.title || 'Curso')}
+
${escapeHtml(course.course_title || course.title || "Curso")}
${formatDuration(course.duration_minutes || 30)}
@@ -472,7 +493,7 @@ function createCourseListItem(course, isCompleted = false) {
- ${isCompleted ? 'Revisar' : 'Continuar'}
+ ${isCompleted ? "Revisar" : "Continuar"}
@@ -480,93 +501,102 @@ function createCourseListItem(course, isCompleted = false) {
}
function renderMandatoryAssignments() {
- const list = document.getElementById('mandatoryList');
- const badge = document.getElementById('mandatoryCount');
+ const list = document.getElementById("mandatoryList");
+ const badge = document.getElementById("mandatoryCount");
- if (!list) return;
+ if (!list) return;
- badge.textContent = LearnState.mandatoryAssignments.length;
+ badge.textContent = LearnState.mandatoryAssignments.length;
- if (LearnState.mandatoryAssignments.length === 0) {
- list.innerHTML = `
+ if (LearnState.mandatoryAssignments.length === 0) {
+ list.innerHTML = `
🎉
Tudo em dia!
Você não possui treinamentos obrigatórios pendentes.
`;
- return;
- }
+ return;
+ }
- list.innerHTML = LearnState.mandatoryAssignments.map(assignment => {
- const isOverdue = assignment.due_date && new Date(assignment.due_date) < new Date();
- const daysUntilDue = assignment.due_date
- ? Math.ceil((new Date(assignment.due_date) - new Date()) / (1000 * 60 * 60 * 24))
- : null;
- const isUrgent = daysUntilDue !== null && daysUntilDue <= 7 && daysUntilDue > 0;
+ list.innerHTML = LearnState.mandatoryAssignments
+ .map((assignment) => {
+ const isOverdue =
+ assignment.due_date && new Date(assignment.due_date) < new Date();
+ const daysUntilDue = assignment.due_date
+ ? Math.ceil(
+ (new Date(assignment.due_date) - new Date()) /
+ (1000 * 60 * 60 * 24),
+ )
+ : null;
+ const isUrgent =
+ daysUntilDue !== null && daysUntilDue <= 7 && daysUntilDue > 0;
- return `
-
- ${isOverdue ? '⚠️' : (isUrgent ? '⏰' : '📋')}
+ ${isOverdue ? "⚠️" : isUrgent ? "⏰" : "📋"}
-
${escapeHtml(assignment.course_title || 'Treinamento Obrigatório')}
-
- ${isOverdue
- ? '
⚠️ Prazo vencido! '
- : (daysUntilDue !== null
- ? `
Prazo: ${daysUntilDue} dias `
- : '
Sem prazo definido '
- )
+
${escapeHtml(assignment.course_title || "Treinamento Obrigatório")}
+
+ ${
+ isOverdue
+ ? "⚠️ Prazo vencido! "
+ : daysUntilDue !== null
+ ? `Prazo: ${daysUntilDue} dias `
+ : "Sem prazo definido "
}
- ${isOverdue ? 'Iniciar Agora' : 'Começar'}
+ ${isOverdue ? "Iniciar Agora" : "Começar"}
`;
- }).join('');
+ })
+ .join("");
}
function updateMandatoryAlert() {
- const alert = document.getElementById('mandatoryAlert');
- const alertText = document.getElementById('mandatoryAlertText');
+ const alert = document.getElementById("mandatoryAlert");
+ const alertText = document.getElementById("mandatoryAlertText");
- if (!alert) return;
+ if (!alert) return;
- const overdueCount = LearnState.mandatoryAssignments.filter(a =>
- a.due_date && new Date(a.due_date) < new Date()
- ).length;
+ const overdueCount = LearnState.mandatoryAssignments.filter(
+ (a) => a.due_date && new Date(a.due_date) < new Date(),
+ ).length;
- const urgentCount = LearnState.mandatoryAssignments.filter(a => {
- if (!a.due_date) return false;
- const days = Math.ceil((new Date(a.due_date) - new Date()) / (1000 * 60 * 60 * 24));
- return days > 0 && days <= 7;
- }).length;
+ const urgentCount = LearnState.mandatoryAssignments.filter((a) => {
+ if (!a.due_date) return false;
+ const days = Math.ceil(
+ (new Date(a.due_date) - new Date()) / (1000 * 60 * 60 * 24),
+ );
+ return days > 0 && days <= 7;
+ }).length;
- if (overdueCount > 0 || urgentCount > 0) {
- alert.style.display = 'flex';
- if (overdueCount > 0) {
- alertText.textContent = `Você possui ${overdueCount} treinamento(s) com prazo vencido!`;
- } else {
- alertText.textContent = `Você possui ${urgentCount} treinamento(s) com prazo próximo.`;
- }
+ if (overdueCount > 0 || urgentCount > 0) {
+ alert.style.display = "flex";
+ if (overdueCount > 0) {
+ alertText.textContent = `Você possui ${overdueCount} treinamento(s) com prazo vencido!`;
} else {
- alert.style.display = 'none';
+ alertText.textContent = `Você possui ${urgentCount} treinamento(s) com prazo próximo.`;
}
+ } else {
+ alert.style.display = "none";
+ }
}
function renderCertificates() {
- const grid = document.getElementById('certificatesGrid');
- const preview = document.getElementById('certificatesPreview');
+ const grid = document.getElementById("certificatesGrid");
+ const preview = document.getElementById("certificatesPreview");
- if (!grid) return;
+ if (!grid) return;
- if (LearnState.certificates.length === 0) {
- grid.innerHTML = `
+ if (LearnState.certificates.length === 0) {
+ grid.innerHTML = `
🏆
Nenhum certificado ainda
@@ -574,22 +604,24 @@ function renderCertificates() {
`;
- if (preview) {
- preview.innerHTML = `
+ if (preview) {
+ preview.innerHTML = `
🏆
Nenhum certificado ainda
`;
- }
- return;
}
+ return;
+ }
- grid.innerHTML = LearnState.certificates.map(cert => `
+ grid.innerHTML = LearnState.certificates
+ .map(
+ (cert) => `
${cert.score}%
@@ -609,36 +641,47 @@ function renderCertificates() {
- `).join('');
+ `,
+ )
+ .join("");
- // Update sidebar preview
- if (preview) {
- preview.innerHTML = LearnState.certificates.slice(0, 3).map(cert => `
+ // Update sidebar preview
+ if (preview) {
+ preview.innerHTML = LearnState.certificates
+ .slice(0, 3)
+ .map(
+ (cert) => `
🎓
-
${escapeHtml(cert.course_title || 'Curso')}
+
${escapeHtml(cert.course_title || "Curso")}
${formatDate(cert.issued_at)}
- `).join('');
- }
+ `,
+ )
+ .join("");
+ }
}
function renderRecommendations(courses) {
- const carousel = document.getElementById('recommendedCourses');
- if (!carousel) return;
+ const carousel = document.getElementById("recommendedCourses");
+ if (!carousel) return;
- if (!courses || courses.length === 0) {
- carousel.innerHTML = '
Explore o catálogo para encontrar cursos.
';
- return;
- }
+ if (!courses || courses.length === 0) {
+ carousel.innerHTML =
+ '
Explore o catálogo para encontrar cursos.
';
+ return;
+ }
- carousel.innerHTML = courses.slice(0, 6).map(course => {
- const card = createCourseCard(course);
- card.style.minWidth = '280px';
- return card.outerHTML;
- }).join('');
+ carousel.innerHTML = courses
+ .slice(0, 6)
+ .map((course) => {
+ const card = createCourseCard(course);
+ card.style.minWidth = "280px";
+ return card.outerHTML;
+ })
+ .join("");
}
// ============================================================================
@@ -646,32 +689,41 @@ function renderRecommendations(courses) {
// ============================================================================
function openCourseModal(courseId) {
- loadCourseDetail(courseId);
- document.getElementById('courseModal').classList.remove('hidden');
+ loadCourseDetail(courseId);
+ document.getElementById("courseModal").classList.remove("hidden");
}
function closeCourseModal() {
- document.getElementById('courseModal').classList.add('hidden');
- LearnState.currentCourse = null;
+ document.getElementById("courseModal").classList.add("hidden");
+ LearnState.currentCourse = null;
}
function renderCourseModal(data) {
- const { course, lessons, quiz } = data;
+ const { course, lessons, quiz } = data;
- document.getElementById('modalCourseTitle').textContent = course.title;
- document.getElementById('modalDescription').textContent = course.description || 'Sem descrição disponível.';
- document.getElementById('modalDifficulty').textContent = formatDifficulty(course.difficulty);
- document.getElementById('modalDifficulty').className = `difficulty-badge ${(course.difficulty || 'beginner').toLowerCase()}`;
- document.getElementById('modalDuration').querySelector('span').textContent = formatDuration(course.duration_minutes);
- document.getElementById('modalLessonsCount').querySelector('span').textContent = `${lessons?.length || 0} aulas`;
+ document.getElementById("modalCourseTitle").textContent = course.title;
+ document.getElementById("modalDescription").textContent =
+ course.description || "Sem descrição disponível.";
+ document.getElementById("modalDifficulty").textContent = formatDifficulty(
+ course.difficulty,
+ );
+ document.getElementById("modalDifficulty").className =
+ `difficulty-badge ${(course.difficulty || "beginner").toLowerCase()}`;
+ document.getElementById("modalDuration").querySelector("span").textContent =
+ formatDuration(course.duration_minutes);
+ document
+ .getElementById("modalLessonsCount")
+ .querySelector("span").textContent = `${lessons?.length || 0} aulas`;
- // Render lessons
- const lessonsList = document.getElementById('modalLessonsList');
- if (lessons && lessons.length > 0) {
- lessonsList.innerHTML = lessons.map((lesson, index) => `
-
0) {
+ lessonsList.innerHTML = lessons
+ .map(
+ (lesson, index) => `
+
-
${lesson.is_completed ? '✓' : index + 1}
+
${lesson.is_completed ? "✓" : index + 1}
${escapeHtml(lesson.title)}
${formatDuration(lesson.duration_minutes)}
@@ -682,143 +734,424 @@ function renderCourseModal(data) {
- `).join('');
- } else {
- lessonsList.innerHTML = '
Nenhuma aula disponível.
';
- }
+ `,
+ )
+ .join("");
+ } else {
+ lessonsList.innerHTML =
+ '
Nenhuma aula disponível.
';
+ }
- // Render quiz section
- const quizSection = document.getElementById('modalQuizSection');
- if (quiz) {
- quizSection.style.display = 'block';
- const questions = typeof quiz.questions === 'string'
- ? JSON.parse(quiz.questions)
- : (quiz.questions || []);
- document.getElementById('modalQuizQuestions').textContent = `${questions.length} questões`;
- document.getElementById('modalQuizTime').textContent = quiz.time_limit_minutes
- ? `${quiz.time_limit_minutes} min`
- : 'Sem limite';
- document.getElementById('modalQuizPassing').textContent = `${quiz.passing_score}% para aprovação`;
- LearnState.currentQuiz = quiz;
- } else {
- quizSection.style.display = 'none';
- }
+ // Render quiz section
+ const quizSection = document.getElementById("modalQuizSection");
+ if (quiz) {
+ quizSection.style.display = "block";
+ const questions =
+ typeof quiz.questions === "string"
+ ? JSON.parse(quiz.questions)
+ : quiz.questions || [];
+ document.getElementById("modalQuizQuestions").textContent =
+ `${questions.length} questões`;
+ document.getElementById("modalQuizTime").textContent =
+ quiz.time_limit_minutes ? `${quiz.time_limit_minutes} min` : "Sem limite";
+ document.getElementById("modalQuizPassing").textContent =
+ `${quiz.passing_score}% para aprovação`;
+ LearnState.currentQuiz = quiz;
+ } else {
+ quizSection.style.display = "none";
+ }
- // Update button text based on progress
- const startBtn = document.getElementById('startCourseBtn');
- if (data.user_progress) {
- const progress = data.user_progress;
- if (progress.status === 'completed') {
- startBtn.innerHTML = `
+ // Update button text based on progress
+ const startBtn = document.getElementById("startCourseBtn");
+ if (data.user_progress) {
+ const progress = data.user_progress;
+ if (progress.status === "completed") {
+ startBtn.innerHTML = `
Revisar Curso
`;
- } else if (progress.status === 'in_progress') {
- startBtn.innerHTML = `
+ } else if (progress.status === "in_progress") {
+ startBtn.innerHTML = `
Continuar
`;
- }
+ }
- // Show progress
- document.getElementById('modalProgress').style.display = 'block';
- document.getElementById('modalProgressFill').style.width = `${progress.completion_percentage || 0}%`;
- document.getElementById('modalProgressText').textContent = `${progress.completion_percentage || 0}% completo`;
- } else {
- document.getElementById('modalProgress').style.display = 'none';
- startBtn.innerHTML = `
+ // Show progress
+ document.getElementById("modalProgress").style.display = "block";
+ document.getElementById("modalProgressFill").style.width =
+ `${progress.completion_percentage || 0}%`;
+ document.getElementById("modalProgressText").textContent =
+ `${progress.completion_percentage || 0}% completo`;
+ } else {
+ document.getElementById("modalProgress").style.display = "none";
+ startBtn.innerHTML = `
Iniciar Curso
`;
- }
+ }
}
function startCourse() {
- if (!LearnState.currentCourse) return;
+ if (!LearnState.currentCourse) return;
- const course = LearnState.currentCourse.course || LearnState.currentCourse;
- const lessons = LearnState.currentCourse.lessons || [];
+ const course = LearnState.currentCourse.course || LearnState.currentCourse;
+ const lessons = LearnState.currentCourse.lessons || [];
- // Start course via API
- startCourseAPI(course.id);
+ // Start course via API
+ startCourseAPI(course.id);
- // Open first lesson
- if (lessons.length > 0) {
- openLesson(lessons[0].id, 0);
- } else {
- showNotification('info', 'Este curso ainda não possui aulas.');
- }
+ // Open first lesson
+ if (lessons.length > 0) {
+ openLesson(lessons[0].id, 0);
+ } else {
+ showNotification("info", "Este curso ainda não possui aulas.");
+ }
}
function openLesson(lessonId, index) {
- const lessons = LearnState.currentCourse?.lessons || [];
- const lesson = lessons.find(l => l.id === lessonId) || lessons[index];
+ const lessons = LearnState.currentCourse?.lessons || [];
+ const lesson = lessons.find((l) => l.id === lessonId) || lessons[index];
- if (!lesson) {
- showNotification('error', 'Aula não encontrada.');
- return;
- }
+ if (!lesson) {
+ showNotification("error", "Aula não encontrada.");
+ return;
+ }
- LearnState.currentLesson = lesson;
- LearnState.currentLessonIndex = index;
+ LearnState.currentLesson = lesson;
+ LearnState.currentLessonIndex = index;
- // Close course modal, open lesson modal
- closeCourseModal();
- document.getElementById('lessonModal').classList.remove('hidden');
+ // Close course modal, open lesson modal
+ closeCourseModal();
+ document.getElementById("lessonModal").classList.remove("hidden");
- // Update lesson UI
- document.getElementById('lessonTitle').textContent = lesson.title;
- document.getElementById('lessonNavTitle').textContent = `Aula ${index + 1} de ${lessons.length}`;
+ // Update lesson UI
+ document.getElementById("lessonTitle").textContent = lesson.title;
+ document.getElementById("lessonNavTitle").textContent =
+ `Aula ${index + 1} de ${lessons.length}`;
- // Render content based on type
- const contentDiv = document.getElementById('lessonContent');
- if (lesson.video_url) {
- contentDiv.innerHTML = `
+ // Render content based on type
+ const contentDiv = document.getElementById("lessonContent");
+ if (lesson.video_url) {
+ contentDiv.innerHTML = `
- ${lesson.content || ''}
+ ${lesson.content || ""}
`;
- } else {
- contentDiv.innerHTML = `
+ } else {
+ contentDiv.innerHTML = `
- ${lesson.content || '
Conteúdo da aula será exibido aqui.
'}
+ ${lesson.content || "
Conteúdo da aula será exibido aqui.
"}
`;
- }
+ }
- // Update sidebar list
- const sidebar = document.getElementById('lessonListSidebar');
- sidebar.innerHTML = lessons.map((l, i) => `
-
`
+
-
${l.is_completed ? '✓' : i + 1}
+
${l.is_completed ? "✓" : i + 1}
${escapeHtml(l.title)}
- `).join('');
+ `,
+ )
+ .join("");
- // Update navigation buttons
- document.getElementById('prevLessonBtn').disabled = index === 0;
- document.getElementById('nextLessonBtn').disabled = index >= lessons.length - 1;
+ // Update navigation buttons
+ document.getElementById("prevLessonBtn").disabled = index === 0;
+ document.getElementById("nextLessonBtn").disabled =
+ index >= lessons.length - 1;
- // Update progress
- const progress = ((index + 1) / lessons.length) * 100;
- document.getElementById('lessonProgressFill').style.width = `${progress}%`;
+ // Update progress
+ const progress = ((index + 1) / lessons.length) * 100;
+ document.getElementById("lessonProgressFill").style.width = `${progress}%`;
}
function closeLessonModal() {
- document.getElementById('lessonModal').classList.add('hidden');
- LearnState.currentLesson = null;
+ document.getElementById("lessonModal").classList.add("hidden");
+ LearnState.currentLesson = null;
- // Reopen course modal
+ // Reopen course modal
+ if (LearnState.currentCourse) {
+ openCourseModal(LearnState.currentCourse.id);
+ }
+}
+
+function toggleLearnSidebar() {
+ const sidebar = document.querySelector(".learn-sidebar");
+ if (sidebar) {
+ sidebar.classList.toggle("collapsed");
+ }
+}
+
+function switchTab(tabId) {
+ document.querySelectorAll(".tab-btn, .learn-tab-btn").forEach((btn) => {
+ btn.classList.remove("active");
+ if (
+ btn.dataset.tab === tabId ||
+ btn.getAttribute("onclick")?.includes(tabId)
+ ) {
+ btn.classList.add("active");
+ }
+ });
+
+ document
+ .querySelectorAll(".tab-content, .learn-tab-content")
+ .forEach((content) => {
+ content.classList.add("hidden");
+ if (content.id === tabId || content.id === `${tabId}-tab`) {
+ content.classList.remove("hidden");
+ }
+ });
+}
+
+function showAllCertificates() {
+ switchTab("certificates");
+}
+
+function loadMoreCourses() {
+ const currentCount = document.querySelectorAll(".course-card").length;
+ fetch(`/api/learn/courses?offset=${currentCount}&limit=12`)
+ .then((r) => r.json())
+ .then((data) => {
+ const grid = document.querySelector(".courses-grid");
+ if (grid && data.courses) {
+ data.courses.forEach((course) => {
+ grid.insertAdjacentHTML("beforeend", createCourseCard(course));
+ });
+ }
+ if (!data.hasMore) {
+ const btn = document.querySelector('[onclick="loadMoreCourses()"]');
+ if (btn) btn.style.display = "none";
+ }
+ })
+ .catch((err) => console.error("Error loading more courses:", err));
+}
+
+function startQuiz() {
+ if (!LearnState.currentCourse) return;
+
+ LearnState.quizState = {
+ questions: LearnState.currentCourse.quiz || [],
+ currentIndex: 0,
+ answers: {},
+ startTime: Date.now(),
+ };
+
+ document.getElementById("courseModal").classList.add("hidden");
+ document.getElementById("quizModal").classList.remove("hidden");
+ renderQuizQuestion();
+}
+
+function renderQuizQuestion() {
+ const { questions, currentIndex } = LearnState.quizState;
+ if (!questions || questions.length === 0) return;
+
+ const question = questions[currentIndex];
+ const container = document.getElementById("quizQuestionContainer");
+
+ container.innerHTML = `
+
+
Question ${currentIndex + 1} of ${questions.length}
+
${question.text}
+
+ ${question.options
+ .map(
+ (opt, i) => `
+
+
+ ${opt}
+
+ `,
+ )
+ .join("")}
+
+
+ `;
+
+ document.getElementById("quizProgress").textContent =
+ `${currentIndex + 1}/${questions.length}`;
+ document.getElementById("quizProgressFill").style.width =
+ `${((currentIndex + 1) / questions.length) * 100}%`;
+}
+
+function selectAnswer(index) {
+ LearnState.quizState.answers[LearnState.quizState.currentIndex] = index;
+}
+
+function prevQuestion() {
+ if (LearnState.quizState.currentIndex > 0) {
+ LearnState.quizState.currentIndex--;
+ renderQuizQuestion();
+ }
+}
+
+function nextQuestion() {
+ if (
+ LearnState.quizState.currentIndex <
+ LearnState.quizState.questions.length - 1
+ ) {
+ LearnState.quizState.currentIndex++;
+ renderQuizQuestion();
+ }
+}
+
+function submitQuiz() {
+ const { questions, answers, startTime } = LearnState.quizState;
+ let correct = 0;
+
+ questions.forEach((q, i) => {
+ if (answers[i] === q.correctIndex) correct++;
+ });
+
+ const score = Math.round((correct / questions.length) * 100);
+ const passed = score >= 70;
+ const duration = Math.round((Date.now() - startTime) / 1000);
+
+ LearnState.quizResult = {
+ score,
+ correct,
+ total: questions.length,
+ passed,
+ duration,
+ };
+
+ document.getElementById("quizModal").classList.add("hidden");
+ document.getElementById("quizResultModal").classList.remove("hidden");
+
+ document.getElementById("quizScore").textContent = `${score}%`;
+ document.getElementById("quizCorrect").textContent =
+ `${correct}/${questions.length}`;
+ document.getElementById("quizStatus").textContent = passed
+ ? "Passed!"
+ : "Not Passed";
+ document.getElementById("quizStatus").className = passed
+ ? "status-passed"
+ : "status-failed";
+
+ if (passed && LearnState.currentCourse) {
+ fetch(`/api/learn/courses/${LearnState.currentCourse.id}/complete`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ score, duration }),
+ }).catch((err) => console.error("Error completing course:", err));
+ }
+}
+
+function closeQuizResult() {
+ document.getElementById("quizResultModal").classList.add("hidden");
+ LearnState.quizState = null;
+ LearnState.quizResult = null;
+ renderMyCourses();
+ renderCertificates();
+}
+
+function reviewAnswers() {
+ document.getElementById("quizResultModal").classList.add("hidden");
+ document.getElementById("quizModal").classList.remove("hidden");
+ LearnState.quizState.currentIndex = 0;
+ renderQuizQuestion();
+}
+
+function confirmExitQuiz() {
+ if (confirm("Are you sure you want to exit? Your progress will be lost.")) {
+ document.getElementById("quizModal").classList.add("hidden");
+ LearnState.quizState = null;
+ if (LearnState.currentCourse) {
+ openCourseModal(LearnState.currentCourse.id);
+ }
+ }
+}
+
+function downloadCertificate() {
+ if (!LearnState.currentCourse) return;
+ window.open(
+ `/api/learn/certificates/${LearnState.currentCourse.id}/download`,
+ "_blank",
+ );
+}
+
+function downloadCertificateById(certId) {
+ window.open(`/api/learn/certificates/${certId}/download`, "_blank");
+}
+
+function closeCertificateModal() {
+ document.getElementById("certificateModal").classList.add("hidden");
+}
+
+function prevLesson() {
+ if (!LearnState.currentCourse || !LearnState.currentLesson) return;
+ const lessons = LearnState.currentCourse.lessons || [];
+ const currentIndex = lessons.findIndex(
+ (l) => l.id === LearnState.currentLesson.id,
+ );
+ if (currentIndex > 0) {
+ openLesson(lessons[currentIndex - 1].id, currentIndex - 1);
+ }
+}
+
+function nextLesson() {
+ if (!LearnState.currentCourse || !LearnState.currentLesson) return;
+ const lessons = LearnState.currentCourse.lessons || [];
+ const currentIndex = lessons.findIndex(
+ (l) => l.id === LearnState.currentLesson.id,
+ );
+ if (currentIndex < lessons.length - 1) {
+ openLesson(lessons[currentIndex + 1].id, currentIndex + 1);
+ }
+}
+
+function completeLesson() {
+ if (!LearnState.currentLesson || !LearnState.currentCourse) return;
+
+ fetch(`/api/learn/lessons/${LearnState.currentLesson.id}/complete`, {
+ method: "POST",
+ })
+ .then(() => {
+ LearnState.currentLesson.completed = true;
+ closeLessonModal();
+ })
+ .catch((err) => console.error("Error completing lesson:", err));
+}
+
+// Export functions to window
+window.toggleLearnSidebar = toggleLearnSidebar;
+window.showAllCertificates = showAllCertificates;
+window.loadMoreCourses = loadMoreCourses;
+window.startQuiz = startQuiz;
+window.prevQuestion = prevQuestion;
+window.nextQuestion = nextQuestion;
+window.submitQuiz = submitQuiz;
+window.closeQuizResult = closeQuizResult;
+window.reviewAnswers = reviewAnswers;
+window.confirmExitQuiz = confirmExitQuiz;
+window.downloadCertificate = downloadCertificate;
+window.downloadCertificateById = downloadCertificateById;
+window.closeCertificateModal = closeCertificateModal;
+window.prevLesson = prevLesson;
+window.nextLesson = nextLesson;
+window.completeLesson = completeLesson;
+window.selectAnswer = selectAnswer;
+window.switchTab = switchTab;
+window.openCourseModal = openCourseModal;
+window.closeCourseModal = closeCourseModal;
+window.startCourse = startCourse;
+window.openLesson = openLesson;
+window.closeLessonModal = closeLessonModal;
diff --git a/ui/suite/tools/tools.js b/ui/suite/tools/tools.js
index d4ea839..634fde6 100644
--- a/ui/suite/tools/tools.js
+++ b/ui/suite/tools/tools.js
@@ -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 = '
Loading report...
';
+ }
+
+ 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 =
+ '
Failed to load report
';
+ }
+ });
+ } 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 `
+
+
Summary
+
+ ${summary.critical || 0} Critical
+ ${summary.high || 0} High
+ ${summary.medium || 0} Medium
+ ${summary.low || 0} Low
+
+
Scan completed: ${report.completed_at || new Date().toISOString()}
+
+
+
Findings (${findings.length})
+ ${findings.length === 0 ? '
No issues found
' : ""}
+ ${findings
+ .map(
+ (f) => `
+
+
${f.severity || "info"}
+
${f.title || f.message || "Finding"}
+
${f.description || ""}
+ ${f.remediation ? `
Fix: ${f.remediation}
` : ""}
+
+ `,
+ )
+ .join("")}
+
+ `;
+ }
+
+ /**
+ * 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 `
+
+
+ ${controls
+ .map(
+ (c) => `
+
+ ${c.id}
+ ${c.name}
+ ${c.status || "Pending"}
+
+ `,
+ )
+ .join("")}
+
+ `;
+ }
+
+ /**
+ * 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 =
+ '
Loading remediation steps...
';
+ }
+
+ 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 = `
+
+ `;
+ }
+ })
+ .catch((err) => {
+ console.error(`Error loading remediation for ${controlId}:`, err);
+ if (contentEl) {
+ contentEl.innerHTML =
+ '
Failed to load remediation steps
';
+ }
+ });
+ } 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;
})();