diff --git a/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx b/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx index 15518f80..d3b6084b 100644 --- a/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx +++ b/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx @@ -92,7 +92,7 @@ export default function AdminConfigurationPage() { // Redirect if not in galactic admin mode if (!galacticMode) { - router.push("/"); + router.push("/dashboard"); } }, [router]); @@ -640,7 +640,7 @@ export default function AdminConfigurationPage() {
diff --git a/src/Web/autopilot-monitor-web/app/audit/page.tsx b/src/Web/autopilot-monitor-web/app/audit/page.tsx index 44a409e2..b3993670 100644 --- a/src/Web/autopilot-monitor-web/app/audit/page.tsx +++ b/src/Web/autopilot-monitor-web/app/audit/page.tsx @@ -151,7 +151,7 @@ export default function AuditPage() { Retry + )} +
+ +
+ + + + + Device + + {galacticAdminMode && ( + + )} + + Model + + + Status + + + Events + + + Duration + + + Started + + {adminMode && ( + + )} + + + + {paginatedSessions.length === 0 ? ( + + + + ) : ( + paginatedSessions.map((session) => ( + router.push(`/sessions/${session.sessionId}`)} + className="hover:bg-gray-50 cursor-pointer transition-colors" + > + + {galacticAdminMode && ( + + )} + + + + + + {adminMode && ( + + )} + + )) + )} + +
+ Tenant ID + + Actions +
+ No sessions found matching your search. +
+
+ {session.deviceName || session.serialNumber} +
+
+ {session.serialNumber} +
+
+
+ {session.tenantId} +
+
+
+ {session.manufacturer || "Unknown manufacturer"} +
+
+ {session.model || "Unknown model"} +
+
+ + + {session.eventCount} + + {Math.round(session.durationSeconds / 60)} min + + {new Date(session.startedAt).toLocaleString()} + + +
+
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+
+ Page {currentPage} of {totalPages} ({sortedSessions.length} total sessions) +
+
+ + +
+
+ )} +
+ )} +
+ + + {/* Delete Confirmation Modal */} + {showDeleteConfirm && sessionToDelete && ( +
+
e.stopPropagation()}> +
+
+
+ + + +
+

Delete Session

+
+ +
+

+ This action is irreversible! +

+

+ The session {sessionToDelete.sessionId} for device {sessionToDelete.deviceName || 'Unknown'} and all associated events will be permanently deleted. +

+

+ Do you want to continue? +

+
+ +
+ + +
+
+
+
+ )} + + + ); +} + +function StatsCard({ + title, + value, + description, + color, +}: { + title: string; + value: string; + description: string; + color: "blue" | "green" | "purple" | "red"; +}) { + const colorClasses = { + blue: "bg-blue-500", + green: "bg-green-500", + purple: "bg-purple-500", + red: "bg-red-500", + }; + + return ( +
+
+
+
+
+ + + +
+
+
+
+
{title}
+
+
{value}
+
+
+
+
+
+
{description}
+
+
+
+ ); +} + +function QuickLink({ + title, + description, + href, +}: { + title: string; + description: string; + href: string; +}) { + return ( + +

{title}

+

{description}

+
+ ); +} + +function SortableHeader({ + column, + currentSort, + direction, + onSort, + children, + className, +}: { + column: keyof Session; + currentSort: keyof Session | null; + direction: "asc" | "desc"; + onSort: (column: keyof Session) => void; + children: React.ReactNode; + className?: string; +}) { + const isActive = currentSort === column; + + return ( + onSort(column)} + className={`py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors select-none ${className ?? "px-6"}`} + > +
+ {children} + + {isActive ? (direction === "asc" ? "↑" : "↓") : "↕"} + +
+ + ); +} + +function StatusBadge({ status, failureReason }: { status: string; failureReason?: string }) { + const statusConfig = { + InProgress: { color: "bg-blue-100 text-blue-800", text: "In Progress" }, + Succeeded: { color: "bg-green-100 text-green-800", text: "Succeeded" }, + Failed: { color: "bg-red-100 text-red-800", text: "Failed" }, + Unknown: { color: "bg-gray-100 text-gray-800", text: "Unknown" }, + }; + + const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.Unknown; + + // Check if this is a timeout failure + const isTimeout = status === "Failed" && failureReason && failureReason.toLowerCase().includes("timed out"); + + return ( + + {config.text} + {isTimeout && ( + + ⏱️ + + )} + + ); +} + +function SettingsMenu({ + apiStatus, + onCheckHealth, + adminMode, + onAdminModeChange, + galacticAdminMode, + onGalacticAdminModeChange, + user, +}: { + apiStatus: "unchecked" | "checking" | "healthy" | "error"; + onCheckHealth: () => void; + adminMode: boolean; + onAdminModeChange: (enabled: boolean) => void; + galacticAdminMode: boolean; + onGalacticAdminModeChange: (enabled: boolean) => void; + user: { displayName: string; email: string; isGalacticAdmin?: boolean } | null; +}) { + const [isOpen, setIsOpen] = useState(false); + const [showAuditLog, setShowAuditLog] = useState(false); + const [auditLogs, setAuditLogs] = useState([]); + const [loadingLogs, setLoadingLogs] = useState(false); + const menuRef = useRef(null); + const { tenantId } = useTenant(); + const { getAccessToken } = useAuth(); // Add this line to get getAccessToken + + const fetchAuditLogs = async () => { + setLoadingLogs(true); + try { + // Get access token and include Authorization header + const token = await getAccessToken(); + + if (!token) { + console.error('Failed to get access token for audit logs'); + return; + } + + const response = await fetch(`${API_BASE_URL}/api/audit/logs?tenantId=${tenantId}`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const data = await response.json(); + setAuditLogs(data.logs || []); + } + } catch (error) { + console.error("Failed to fetch audit logs:", error); + } finally { + setLoadingLogs(false); + } + }; + + const statusColors = { + unchecked: "text-gray-500", + checking: "text-yellow-600", + healthy: "text-green-600", + error: "text-red-600", + }; + + const statusIcons = { + unchecked: "🔘", + checking: "🔄", + healthy: "✅", + error: "❌", + }; + + const statusText = { + unchecked: "Not checked", + checking: "Checking...", + healthy: "Connected", + error: "Not connected", + }; + + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + } + }, [isOpen]); + + return ( +
+ {/* Settings Button */} + + + {/* Dropdown Menu */} + {isOpen && ( +
+
+

Settings

+ + {/* Admin Mode Toggle */} +
+
+
+ Admin Mode + {adminMode && AKTIV} +
+ +
+ {adminMode && ( +

+ ⚠️ Allows deleting sessions +

+ )} +
+ + {/* Galactic Admin Toggle - Only visible to actual galactic admins */} + {user?.isGalacticAdmin && ( +
+
+
+ Galactic Admin + {galacticAdminMode && ACTIVE} +
+ +
+ {galacticAdminMode && ( +

+ Shows ALL sessions across ALL tenants +

+ )} +
+ )} + + {/* API Status Section - Clickable */} +
+ +
+ + {/* Configuration Section */} + + + {/* Usage Metrics Section - Always visible for tenant */} + + + {/* Platform Usage Metrics Section - Galactic Admin Only */} + {galacticAdminMode && ( + + )} + + {/* Audit Log Section */} +
+ +
+
+
+ )} + + {/* Audit Log Modal */} + {showAuditLog && ( +
setShowAuditLog(false)}> +
e.stopPropagation()}> +
+
+

Audit Log

+ +
+
+
+ {loadingLogs ? ( +
Lade Audit Logs...
+ ) : auditLogs.length === 0 ? ( +
Keine Audit Logs vorhanden
+ ) : ( +
+ {auditLogs.map((log) => ( +
+
+
+
+ + {log.action} + + {log.entityType} + {log.entityId} +
+
+ von {log.performedBy} +
+ {log.details && log.details !== '{}' && ( +
+ {log.details} +
+ )} +
+
+ {new Date(log.timestamp).toLocaleString('de-DE')} +
+
+
+ ))} +
+ )} +
+
+
+ )} +
+ ); +} + +function UserMenu({ + username, + email, + onLogout, +}: { + username: string; + email: string; + onLogout: () => void; +}) { + const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + + // Get initials from username + const getInitials = (name: string) => { + return name + .split(" ") + .map(n => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + } + }, [isOpen]); + + return ( +
+ {/* User Avatar Button */} + + + {/* Dropdown Menu */} + {isOpen && ( +
+
+ {/* User Info */} +
+
+ {getInitials(username)} +
+
+
+ {username} +
+
+ {email} +
+ +
+
+ + {/* Sign out */} +
+ +
+
+
+ )} +
+ ); +} diff --git a/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx b/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx index e2c33a97..16663a14 100644 --- a/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx +++ b/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx @@ -354,7 +354,7 @@ export default function FleetHealthPage() {