admin.html•38.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SAP MCP Server - Admin Dashboard</title>
<!-- SAP UI5 Theme and Fonts -->
<link rel="stylesheet" href="https://ui5.sap.com/resources/sap/ui/core/themes/sap_horizon/library.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=72:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: '72', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f5f6fa;
min-height: 100vh;
color: #32363a;
}
/* Header */
.header {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 3px solid #0070f2;
}
.header h1 {
color: #0070f2;
font-weight: 600;
font-size: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.sap-logo {
width: 32px;
height: 32px;
background: #0070f2;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 14px;
}
.user-info {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.9rem;
color: #6a6d70;
}
.status-badge {
background: #0070f2;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 16px;
font-size: 0.8rem;
font-weight: 500;
}
/* Main Container */
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* Statistics Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e4e7ea;
transition: all 0.2s ease;
}
.stat-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
border-color: #0070f2;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #0070f2;
margin-bottom: 0.5rem;
}
.stat-label {
color: #6a6d70;
font-size: 0.9rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Users Table */
.table-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e4e7ea;
overflow: hidden;
}
.table-header {
padding: 1.5rem;
background: #f8f9fb;
border-bottom: 1px solid #e4e7ea;
display: flex;
align-items: center;
justify-content: space-between;
}
.table-title {
font-size: 1.25rem;
font-weight: 600;
color: #32363a;
}
.refresh-btn {
background: #0070f2;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.refresh-btn:hover {
background: #005bb5;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 1rem 1.5rem;
text-align: left;
border-bottom: 1px solid #e4e7ea;
}
.table th {
background: #f8f9fb;
font-weight: 600;
color: #32363a;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table td {
font-size: 0.9rem;
}
.table tbody tr:hover {
background: #f8f9fb;
}
/* Role Badges */
.role-badge {
padding: 0.25rem 0.75rem;
border-radius: 16px;
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.role-admin {
background: #ffeaa7;
color: #d63031;
}
.role-editor {
background: #a7f0ba;
color: #00b894;
}
.role-viewer {
background: #74b9ff;
color: white;
}
/* Status Indicators */
.status-indicator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-active .status-dot {
background: #00b894;
}
.status-expired .status-dot {
background: #d63031;
}
/* Action Buttons */
.action-btn {
padding: 0.25rem 0.75rem;
border: 1px solid #d1d8e0;
background: white;
border-radius: 4px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s ease;
margin-right: 0.5rem;
}
.action-btn:hover {
border-color: #0070f2;
color: #0070f2;
}
/* Loading State */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 3rem;
color: #6a6d70;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid #e4e7ea;
border-top: 3px solid #0070f2;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive */
@media (max-width: 768px) {
.header {
padding: 1rem;
flex-direction: column;
gap: 1rem;
}
.container {
padding: 1rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.table-container {
overflow-x: auto;
}
.table {
min-width: 800px;
}
}
/* Error State */
.error {
background: #fff5f5;
border: 1px solid #fed7d7;
color: #c53030;
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
}
</style>
</head>
<body>
<div class="header">
<div>
<h1>
<div class="sap-logo">S</div>
MCP Server Admin Dashboard
</h1>
</div>
<div class="user-info">
<span id="currentUser">Loading...</span>
<div class="status-badge" id="authStatus">Admin</div>
</div>
</div>
<div class="container">
<!-- Statistics Cards -->
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<div class="stat-value" id="totalUsers">-</div>
<div class="stat-label">Total Users</div>
</div>
<div class="stat-card">
<div class="stat-value" id="activeUsers">-</div>
<div class="stat-label">Active Sessions</div>
</div>
<div class="stat-card">
<div class="stat-value" id="adminUsers">-</div>
<div class="stat-label">Admin Users</div>
</div>
</div>
<!-- OData Configuration Management -->
<div class="table-container" style="margin-bottom: 2rem;">
<div class="table-header">
<div class="table-title">🔧 OData Service Configuration</div>
<button class="refresh-btn" onclick="reloadODataConfig()" id="reloadBtn">
<span>🔄</span>
Reload Services
</button>
</div>
<div style="padding: 1.5rem; background: white; border-radius: 0 0 8px 8px; border: 1px solid #e3e6ea; border-top: none;">
<!-- Current Configuration Status -->
<div id="configStatus" style="margin-bottom: 1.5rem;">
<div class="loading">
<div class="spinner"></div>
Loading configuration status...
</div>
</div>
</div>
</div>
<!-- Destination Status -->
<div class="table-container" style="margin-bottom: 2rem;">
<div class="table-header">
<div class="table-title">🔗 Destination Status</div>
<button class="refresh-btn" onclick="loadDestinationStatus()" id="destStatusBtn">
<span>🔄</span>
Check Status
</button>
</div>
<div id="destinationStatusContent">
<div id="destinationLoadingState" style="display: flex; align-items: center; justify-content: center; padding: 2rem; color: #666;">
<div>⏳ Loading destination status...</div>
</div>
<div id="destinationStatusDisplay" style="display: none;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
<!-- Design-Time Destination -->
<div class="stat-card">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<div id="designTimeStatusIcon">🔄</div>
<strong>Design-Time Destination</strong>
</div>
<div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;">
<strong>Name:</strong> <span id="designTimeName">-</span>
</div>
<div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;">
<strong>Status:</strong> <span id="designTimeStatus">-</span>
</div>
<div id="designTimeError" style="font-size: 0.8rem; color: #c53030; margin-top: 0.5rem; display: none;"></div>
<div style="font-size: 0.8rem; color: #666; margin-top: 0.5rem;">
Used for: Service discovery, metadata
</div>
</div>
<!-- Runtime Destination -->
<div class="stat-card">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<div id="runtimeStatusIcon">🔄</div>
<strong>Runtime Destination</strong>
</div>
<div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;">
<strong>Name:</strong> <span id="runtimeName">-</span>
</div>
<div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;">
<strong>Status:</strong> <span id="runtimeStatus">-</span>
</div>
<div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;">
<strong>Auth:</strong> <span id="runtimeAuthType">-</span>
</div>
<div id="runtimeHybrid" style="font-size: 0.8rem; color: #0070f2; margin-bottom: 0.25rem; display: none;">
🔒 Hybrid: Principal Propagation + BasicAuth fallback
</div>
<div id="runtimeError" style="font-size: 0.8rem; color: #c53030; margin-top: 0.5rem; display: none;"></div>
<div style="font-size: 0.8rem; color: #666; margin-top: 0.5rem;">
Used for: CRUD operations, data access
</div>
</div>
<!-- Configuration Info -->
<div class="stat-card">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<div>⚙️</div>
<strong>Configuration</strong>
</div>
<div style="font-size: 0.9rem; color: #666; margin-bottom: 0.25rem;">
<strong>Mode:</strong> <span id="configMode">-</span>
</div>
<div style="font-size: 0.8rem; color: #666; margin-top: 0.5rem;">
<div id="configModeDescription">-</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Users Table -->
<div class="table-container">
<div class="table-header">
<div class="table-title">User Sessions & Authorizations</div>
<button class="refresh-btn" onclick="loadUsers()">
<span>🔄</span>
Refresh
</button>
</div>
<div id="loadingState" class="loading">
<div class="spinner"></div>
Loading user data...
</div>
<div id="errorState" class="error" style="display: none;">
<strong>Error:</strong> <span id="errorMessage"></span>
</div>
<table class="table" id="usersTable" style="display: none;">
<thead>
<tr>
<th>User</th>
<th>Role</th>
<th>Scopes</th>
<th>Status</th>
<th>Last Activity</th>
<th>Session Type</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="usersTableBody">
</tbody>
</table>
</div>
</div>
<script>
let usersData = [];
// Format date for display
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString();
}
// Format relative time
function formatRelativeTime(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffMinutes = Math.floor(diffMs / (1000 * 60));
if (diffHours > 24) {
return `${Math.floor(diffHours / 24)}d ago`;
} else if (diffHours > 0) {
return `${diffHours}h ago`;
} else if (diffMinutes > 0) {
return `${diffMinutes}m ago`;
} else {
return 'Just now';
}
}
// Get session ID from URL parameters or localStorage
function getSessionId() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('session') || localStorage.getItem('mcp_session_id') || 'global_user_auth';
}
// Load users data
async function loadUsers() {
try {
document.getElementById('loadingState').style.display = 'flex';
document.getElementById('errorState').style.display = 'none';
document.getElementById('usersTable').style.display = 'none';
const sessionId = getSessionId();
const response = await fetch(`/auth/admin/users?session=${encodeURIComponent(sessionId)}`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to load users');
}
usersData = data.users;
updateStatistics(data.summary);
updateUsersTable(data.users);
// Display user info with Session ID
const currentSessionId = getSessionId();
document.getElementById('currentUser').innerHTML = `${data.requestedBy}<br><small style="color: #666;">Session ID: <code style="background: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-size: 0.8em;">${currentSessionId}</code></small>`;
document.getElementById('loadingState').style.display = 'none';
document.getElementById('usersTable').style.display = 'table';
} catch (error) {
console.error('Error loading users:', error);
document.getElementById('loadingState').style.display = 'none';
document.getElementById('errorState').style.display = 'block';
document.getElementById('errorMessage').textContent = error.message;
}
}
// Update statistics cards
function updateStatistics(summary) {
document.getElementById('totalUsers').textContent = summary.totalUsers;
document.getElementById('activeUsers').textContent = summary.activeUsers;
document.getElementById('adminUsers').textContent = summary.adminUsers;
}
// Update users table
function updateUsersTable(users) {
const tbody = document.getElementById('usersTableBody');
tbody.innerHTML = '';
users.forEach(user => {
const row = document.createElement('tr');
const statusClass = user.isActive ? 'status-active' : 'status-expired';
const statusText = user.isActive ? 'Active' : 'Expired';
row.innerHTML = `
<td>
<strong>${user.user}</strong>
<br>
<small style="color: #6a6d70;">${user.clientInfo.ipAddress}</small>
</td>
<td>
<span class="role-badge role-${user.role}">${user.role}</span>
</td>
<td>
<span style="font-size: 0.8rem; color: #6a6d70;">
${user.scopes.join(', ') || 'None'}
</span>
</td>
<td>
<div class="status-indicator ${statusClass}">
<div class="status-dot"></div>
${statusText}
</div>
</td>
<td>
<span title="${formatDate(user.lastActivity)}">
${formatRelativeTime(user.lastActivity)}
</span>
</td>
<td>
<span style="font-size: 0.8rem; color: #6a6d70;">
${user.sessionType}
</span>
</td>
<td>
<button class="action-btn" onclick="viewSession('${user.sessionId}')">View</button>
<button class="action-btn" onclick="deleteSession('${user.sessionId}')">Delete</button>
</td>
`;
tbody.appendChild(row);
});
}
// View session details
function viewSession(sessionId) {
const user = usersData.find(u => u.sessionId === sessionId);
if (!user) return;
const details = `
Session Details for ${user.user}:
Session ID: ${user.sessionId}
Role: ${user.role}
Scopes: ${user.scopes.join(', ')}
Status: ${user.isActive ? 'Active' : 'Expired'}
Authenticated: ${formatDate(user.authenticatedAt)}
Last Activity: ${formatDate(user.lastActivity)}
Expires: ${formatDate(user.expiresAt)}
Client: ${user.clientInfo.userAgent}
IP: ${user.clientInfo.ipAddress}
Type: ${user.sessionType}
`;
alert(details);
}
// Delete session
async function deleteSession(sessionId) {
const user = usersData.find(u => u.sessionId === sessionId);
if (!user) return;
if (confirm(`Are you sure you want to delete the session for ${user.user}?`)) {
try {
const authSessionId = getSessionId();
const response = await fetch(`/auth/admin/users/delete?session=${encodeURIComponent(authSessionId)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ sessionId: sessionId })
});
const data = await response.json();
if (response.ok) {
alert(`Session for ${user.user} deleted successfully`);
loadUsers(); // Refresh the table
} else {
alert(`Error: ${data.message}`);
}
} catch (error) {
alert(`Error deleting session: ${error.message}`);
}
}
}
// Reload OData configuration and trigger service rediscovery
async function reloadODataConfig() {
const reloadBtn = document.getElementById('reloadBtn');
const originalText = reloadBtn.innerHTML;
try {
// Update button to show loading state
reloadBtn.innerHTML = '<span>⏳</span> Reloading...';
reloadBtn.disabled = true;
const authSessionId = getSessionId();
const response = await fetch(`/auth/admin/odata/reload?session=${encodeURIComponent(authSessionId)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (response.ok && result.success) {
alert(`✅ OData Configuration Reload Successful!\n\n${result.message}\n\nServices Count: ${result.servicesCount || 'Unknown'}\n\nTriggered by: ${result.triggeredBy}\nTime: ${new Date(result.triggeredAt).toLocaleString()}`);
} else {
alert(`❌ OData Reload Failed!\n\n${result.message || result.error}`);
}
} catch (error) {
alert(`❌ Error reloading OData configuration: ${error.message}`);
} finally {
// Restore button
reloadBtn.innerHTML = originalText;
reloadBtn.disabled = false;
}
}
// Load OData configuration status
async function loadConfigStatus() {
const statusDiv = document.getElementById('configStatus');
try {
const authSessionId = getSessionId();
const response = await fetch(`/auth/admin/odata/status?session=${encodeURIComponent(authSessionId)}`);
const result = await response.json();
if (response.ok && result.success) {
const config = result.configuration.config;
const servicesCount = result.configuration.servicesCount;
const discoveredServices = result.configuration.discoveredServices;
statusDiv.innerHTML = `
<div class="info-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
<div class="info-card" style="background: #f8f9fa; padding: 1rem; border-radius: 8px; border-left: 4px solid #007bff;">
<div style="font-weight: 600; color: #495057; margin-bottom: 0.5rem;">📍 Configuration Source</div>
<div style="color: #6c757d; font-size: 0.9rem;">${config.configurationSource || 'Unknown'}</div>
</div>
<div class="info-card" style="background: #f8f9fa; padding: 1rem; border-radius: 8px; border-left: 4px solid #28a745;">
<div style="font-weight: 600; color: #495057; margin-bottom: 0.5rem;">📊 Services Discovered</div>
<div style="color: #6c757d; font-size: 0.9rem; font-weight: 600;">${servicesCount} services</div>
</div>
<div class="info-card" style="background: #f8f9fa; padding: 1rem; border-radius: 8px; border-left: 4px solid #ffc107;">
<div style="font-weight: 600; color: #495057; margin-bottom: 0.5rem;">🔄 Max Services</div>
<div style="color: #6c757d; font-size: 0.9rem;">${config.maxServices || 'Not set'}</div>
</div>
</div>
<div style="background: #e7f3ff; border: 1px solid #b8daff; border-radius: 6px; padding: 1rem; margin-bottom: 1rem;">
<div style="font-weight: 600; color: #004085; margin-bottom: 0.5rem;">🔧 Current Configuration</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; font-size: 0.9rem;">
<div>
<strong>Service Patterns:</strong><br>
<code style="background: white; padding: 0.2rem 0.4rem; border-radius: 3px; display: inline-block; margin-top: 0.25rem;">
${Array.isArray(config.servicePatterns) ? config.servicePatterns.join(', ') : (config.servicePatterns || 'Not set')}
</code>
</div>
<div>
<strong>Exclusion Patterns:</strong><br>
<code style="background: white; padding: 0.2rem 0.4rem; border-radius: 3px; display: inline-block; margin-top: 0.25rem;">
${Array.isArray(config.exclusionPatterns) ? config.exclusionPatterns.join(', ') : (config.exclusionPatterns || 'None')}
</code>
</div>
</div>
<div style="margin-top: 0.75rem; font-size: 0.9rem;">
<strong>Allow All Services:</strong>
<span style="color: ${config.allowAllServices ? '#28a745' : '#dc3545'}; font-weight: 600;">
${config.allowAllServices ? 'Yes' : 'No'}
</span>
</div>
</div>
${discoveredServices.length > 0 ? `
<details style="border: 1px solid #dee2e6; border-radius: 6px; margin-bottom: 1rem;">
<summary style="padding: 0.75rem; background: #f8f9fa; cursor: pointer; font-weight: 600;">
📋 Discovered Services (${discoveredServices.length})
</summary>
<div style="padding: 1rem; max-height: 200px; overflow-y: auto;">
${discoveredServices.map(service => `
<div style="padding: 0.5rem; border-bottom: 1px solid #e9ecef; font-size: 0.9rem;">
<div style="font-weight: 600;">${service.name}</div>
<div style="color: #6c757d;">ID: ${service.id}</div>
</div>
`).join('')}
</div>
</details>
` : ''}
`;
} else {
statusDiv.innerHTML = `
<div style="background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 6px; padding: 1rem; color: #721c24;">
❌ Failed to load configuration status: ${result.message || 'Unknown error'}
</div>
`;
}
} catch (error) {
statusDiv.innerHTML = `
<div style="background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 6px; padding: 1rem; color: #721c24;">
❌ Error loading configuration: ${error.message}
</div>
`;
}
}
// Load destination status
async function loadDestinationStatus() {
const statusBtn = document.getElementById('destStatusBtn');
const loadingState = document.getElementById('destinationLoadingState');
const statusDisplay = document.getElementById('destinationStatusDisplay');
try {
statusBtn.disabled = true;
loadingState.style.display = 'flex';
statusDisplay.style.display = 'none';
const authSessionId = getSessionId();
const response = await fetch(`/auth/admin/destinations/status?session=${encodeURIComponent(authSessionId)}`);
const result = await response.json();
if (response.ok && result.success) {
updateDestinationStatusDisplay(result.destinations);
loadingState.style.display = 'none';
statusDisplay.style.display = 'block';
} else {
throw new Error(result.message || 'Failed to load destination status');
}
} catch (error) {
console.error('Failed to load destination status:', error);
loadingState.innerHTML = `<div style="color: #c53030;">❌ Failed to load destination status: ${error.message}</div>`;
} finally {
statusBtn.disabled = false;
}
}
// Update destination status display
function updateDestinationStatusDisplay(destinations) {
// Design-time destination
const designTimeIcon = document.getElementById('designTimeStatusIcon');
const designTimeName = document.getElementById('designTimeName');
const designTimeStatus = document.getElementById('designTimeStatus');
const designTimeError = document.getElementById('designTimeError');
designTimeName.textContent = destinations.designTime.name;
if (destinations.designTime.available) {
designTimeIcon.textContent = '✅';
designTimeStatus.textContent = 'Available';
designTimeStatus.style.color = '#38a169';
designTimeError.style.display = 'none';
} else {
designTimeIcon.textContent = '❌';
designTimeStatus.textContent = 'Not Available';
designTimeStatus.style.color = '#c53030';
if (destinations.designTime.error) {
designTimeError.textContent = destinations.designTime.error;
designTimeError.style.display = 'block';
} else {
designTimeError.style.display = 'none';
}
}
// Runtime destination
const runtimeIcon = document.getElementById('runtimeStatusIcon');
const runtimeName = document.getElementById('runtimeName');
const runtimeStatus = document.getElementById('runtimeStatus');
const runtimeAuthType = document.getElementById('runtimeAuthType');
const runtimeHybrid = document.getElementById('runtimeHybrid');
const runtimeError = document.getElementById('runtimeError');
runtimeName.textContent = destinations.runtime.name;
// Display authentication type
if (destinations.runtime.authType) {
runtimeAuthType.textContent = destinations.runtime.authType;
if (destinations.runtime.authType === 'PrincipalPropagation') {
runtimeAuthType.style.color = '#0070f2';
} else {
runtimeAuthType.style.color = '#666';
}
}
// Show hybrid authentication indicator
if (destinations.runtime.hybrid) {
runtimeHybrid.style.display = 'block';
} else {
runtimeHybrid.style.display = 'none';
}
if (destinations.runtime.available) {
runtimeIcon.textContent = '✅';
runtimeStatus.textContent = 'Available';
runtimeStatus.style.color = '#38a169';
runtimeError.style.display = 'none';
} else {
// Check if it's a Principal Propagation JWT requirement
const isPrincipalPropError = destinations.runtime.error &&
(destinations.runtime.error.includes('user token') ||
destinations.runtime.error.includes('PrincipalPropagation'));
if (isPrincipalPropError) {
runtimeIcon.textContent = '🔐';
runtimeStatus.textContent = 'Requires User JWT';
runtimeStatus.style.color = '#0070f2';
runtimeError.innerHTML = '💡 Principal Propagation requires authenticated user context. Status check from admin dashboard is expected to fail.';
runtimeError.style.color = '#0070f2';
runtimeError.style.display = 'block';
} else {
runtimeIcon.textContent = '❌';
runtimeStatus.textContent = 'Not Available';
runtimeStatus.style.color = '#c53030';
if (destinations.runtime.error) {
runtimeError.textContent = destinations.runtime.error;
runtimeError.style.display = 'block';
} else {
runtimeError.style.display = 'none';
}
}
}
// Configuration
const configMode = document.getElementById('configMode');
const configModeDescription = document.getElementById('configModeDescription');
if (destinations.config.useSingleDestination) {
configMode.textContent = 'Single Destination';
configModeDescription.textContent = 'Both operations use the same destination. Simpler setup, but may have different authentication requirements.';
} else {
configMode.textContent = 'Dual Destination';
configModeDescription.textContent = 'Separate destinations for discovery/metadata and runtime operations. Recommended for production environments.';
}
}
// Enhanced reload function to also refresh status
const originalReloadODataConfig = reloadODataConfig;
reloadODataConfig = async function() {
await originalReloadODataConfig();
// Refresh status after reload
setTimeout(loadConfigStatus, 1000);
setTimeout(loadDestinationStatus, 1500);
};
// Auto-refresh every 30 seconds
setInterval(loadUsers, 30000);
setInterval(loadConfigStatus, 60000); // Refresh config status every minute
setInterval(loadDestinationStatus, 120000); // Refresh destination status every 2 minutes
// Load initial data
document.addEventListener('DOMContentLoaded', function() {
loadUsers();
loadConfigStatus();
loadDestinationStatus();
});
</script>
</body>
</html>