|
|
|
|
@@ -238,6 +238,21 @@
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.env-badge {
|
|
|
|
|
font-size: 9px;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
}
|
|
|
|
|
.env-badge.env_production { background: rgba(255, 68, 102, 0.15); color: var(--color-danger); }
|
|
|
|
|
.env-badge.env_staging { background: rgba(255, 170, 34, 0.15); color: var(--color-warning); }
|
|
|
|
|
.env-badge.env_local { background: rgba(74, 240, 192, 0.15); color: var(--color-primary); }
|
|
|
|
|
.env-badge.env_unknown { background: rgba(138, 154, 184, 0.15); color: var(--color-text-muted); }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
@@ -251,16 +266,11 @@
|
|
|
|
|
<span style="font-size:11px;color:var(--color-text-muted)">LIVE</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="portal-grid">
|
|
|
|
|
<div class="portal-grid" id="portal-grid">
|
|
|
|
|
<!-- Populated dynamically from portals.json -->
|
|
|
|
|
|
|
|
|
|
<!-- Portal: Hermes -->
|
|
|
|
|
<div class="portal-card status-online">
|
|
|
|
|
<div class="portal-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="portal-name">Hermes</div>
|
|
|
|
|
<div class="portal-id">portal://hermes.nexus</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="status-badge online">online</span>
|
|
|
|
|
<span class="status-badge online">online</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="portal-meta">
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
@@ -285,13 +295,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Portal: Archive -->
|
|
|
|
|
<div class="portal-card status-online">
|
|
|
|
|
<div class="portal-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="portal-name">Archive</div>
|
|
|
|
|
<div class="portal-id">portal://archive.nexus</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="status-badge online">online</span>
|
|
|
|
|
<span class="status-badge online">online</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="portal-meta">
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
@@ -316,13 +320,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Portal: Satflow -->
|
|
|
|
|
<div class="portal-card status-warning">
|
|
|
|
|
<div class="portal-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="portal-name">Satflow</div>
|
|
|
|
|
<div class="portal-id">portal://satflow.nexus</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="status-badge warning">degraded</span>
|
|
|
|
|
<span class="status-badge warning">degraded</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="portal-meta">
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
@@ -347,13 +345,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Portal: Evennia -->
|
|
|
|
|
<div class="portal-card status-online">
|
|
|
|
|
<div class="portal-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="portal-name">Evennia</div>
|
|
|
|
|
<div class="portal-id">portal://evennia.nexus</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="status-badge online">online</span>
|
|
|
|
|
<span class="status-badge online">online</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="portal-meta">
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
@@ -378,13 +370,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Portal: Bannerlord -->
|
|
|
|
|
<div class="portal-card status-offline">
|
|
|
|
|
<div class="portal-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="portal-name">Bannerlord</div>
|
|
|
|
|
<div class="portal-id">portal://bannerlord.nexus</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="status-badge offline">offline</span>
|
|
|
|
|
<span class="status-badge offline">offline</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="portal-meta">
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
@@ -409,13 +395,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Portal: OpenClaw -->
|
|
|
|
|
<div class="portal-card status-locked">
|
|
|
|
|
<div class="portal-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="portal-name">OpenClaw</div>
|
|
|
|
|
<div class="portal-id">portal://openclaw.nexus</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="status-badge locked">locked</span>
|
|
|
|
|
<span class="status-badge locked">locked</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="portal-meta">
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
@@ -445,34 +425,156 @@
|
|
|
|
|
<div class="summary-bar">
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="summary-count" style="color:var(--color-primary)">4</div>
|
|
|
|
|
<div class="summary-count online" data-default="0">0</div>
|
|
|
|
|
<div class="summary-label">Online</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="summary-count" style="color:var(--color-warning)">1</div>
|
|
|
|
|
<div class="summary-count degraded" data-default="0">0</div>
|
|
|
|
|
<div class="summary-label">Degraded</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="summary-count" style="color:var(--color-danger)">1</div>
|
|
|
|
|
<div class="summary-count offline" data-default="0">0</div>
|
|
|
|
|
<div class="summary-label">Offline</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="summary-count" style="color:var(--color-secondary)">1</div>
|
|
|
|
|
<div class="summary-count locked" data-default="0">0</div>
|
|
|
|
|
<div class="summary-label">Locked</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="margin-left:auto;align-self:center;font-size:10px;color:var(--color-text-muted)">
|
|
|
|
|
LAST SYNC: <span style="color:var(--color-text)">04:20:07 UTC</span>
|
|
|
|
|
LAST SYNC: <span class="last-sync-time" style="color:var(--color-text)">--:--:-- UTC</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// Portal Status Wall — dynamic renderer backed by portals.json
|
|
|
|
|
// Fetches real portal data and renders environment-aware status cards.
|
|
|
|
|
// Ref: #714
|
|
|
|
|
|
|
|
|
|
(async function() {
|
|
|
|
|
const grid = document.querySelector('.portal-grid');
|
|
|
|
|
if (!grid) {
|
|
|
|
|
console.error('Portal grid container not found');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch('../portals.json');
|
|
|
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
|
|
|
const portals = await resp.json();
|
|
|
|
|
|
|
|
|
|
// Clear any placeholder cards
|
|
|
|
|
grid.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
// Environment label mapping
|
|
|
|
|
const envLabels = {production: 'PROD', staging: 'STAGING', local: 'LOCAL'};
|
|
|
|
|
|
|
|
|
|
// Status to CSS class and display label
|
|
|
|
|
const statusConfig = {
|
|
|
|
|
online: {cls: 'online', label: 'ONLINE'},
|
|
|
|
|
offline: {cls: 'offline', label: 'OFFLINE'},
|
|
|
|
|
warning: {cls: 'warning', label: 'DEGRADED'},
|
|
|
|
|
locked: {cls: 'locked', label: 'LOCKED'},
|
|
|
|
|
standby: {cls: 'warning', label: 'STANDBY'}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Render portal cards from portals.json
|
|
|
|
|
for (const p of portals) {
|
|
|
|
|
const env = (p.environment || 'unknown').toLowerCase();
|
|
|
|
|
const statusKey = (p.status || 'offline').toLowerCase();
|
|
|
|
|
const cfg = statusConfig[statusKey] || {cls: 'offline', label: statusKey.toUpperCase()};
|
|
|
|
|
|
|
|
|
|
const card = document.createElement('div');
|
|
|
|
|
card.className = `portal-card status-${cfg.cls}`;
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
<div class="portal-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="portal-name">${escHtml(p.name || p.id)}</div>
|
|
|
|
|
<div class="portal-id">portal://${escHtml(p.id)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="status-badge ${cfg.cls}">${cfg.label}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="portal-meta">
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
<span class="meta-label">Type</span>
|
|
|
|
|
<span class="meta-value">${escHtml(p.portal_type || 'world')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
<span class="meta-label">Category</span>
|
|
|
|
|
<span class="meta-value">${escHtml(p.world_category || '-')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
<span class="meta-label">Env</span>
|
|
|
|
|
<span class="meta-value env-badge env_${env}">${envLabels[env] || env.toUpperCase()}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="meta-row">
|
|
|
|
|
<span class="meta-label">Readiness</span>
|
|
|
|
|
<span class="meta-value">${escHtml(p.readiness_state || 'unknown')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
${p.blocked_reason ? `
|
|
|
|
|
<div class="meta-row" style="color:var(--color-warning)">
|
|
|
|
|
<span class="meta-label">Blocked</span>
|
|
|
|
|
<span class="meta-value">${escHtml(p.blocked_reason)}</span>
|
|
|
|
|
</div>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
grid.appendChild(card);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update summary bar counts
|
|
|
|
|
updateSummary(portals);
|
|
|
|
|
|
|
|
|
|
// Update last sync time
|
|
|
|
|
const timeEl = document.querySelector('.last-sync-time');
|
|
|
|
|
if (timeEl) {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
timeEl.textContent = now.toISOString().slice(11,19) + ' UTC';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to load portal status:', e);
|
|
|
|
|
grid.innerHTML = '<div style="color:var(--color-danger);padding:12px;">Portal registry offline — unable to load portals.json</div>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper: escape HTML
|
|
|
|
|
function escHtml(s) {
|
|
|
|
|
return String(s || '').replace(/[&<>"']/g, c => ({
|
|
|
|
|
'&':'&', '<':'<', '>':'>', '"':'"', "'":'''
|
|
|
|
|
})[c]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper: update summary counts
|
|
|
|
|
function updateSummary(portals) {
|
|
|
|
|
const counts = {online:0, degraded:0, offline:0, locked:0};
|
|
|
|
|
for (const p of portals) {
|
|
|
|
|
const st = (p.status || '').toLowerCase();
|
|
|
|
|
if (st === 'online') counts.online++;
|
|
|
|
|
else if (st === 'offline') counts.offline++;
|
|
|
|
|
else if (st === 'warning' || st === 'degraded') counts.degraded++;
|
|
|
|
|
else if (st === 'locked') counts.locked++;
|
|
|
|
|
else counts.offline++;
|
|
|
|
|
}
|
|
|
|
|
const map = {
|
|
|
|
|
'online': '.summary-count.online',
|
|
|
|
|
'degraded': '.summary-count.degraded',
|
|
|
|
|
'offline': '.summary-count.offline',
|
|
|
|
|
'locked': '.summary-count.locked'
|
|
|
|
|
};
|
|
|
|
|
for (const [key, selector] of Object.entries(map)) {
|
|
|
|
|
const el = document.querySelector(selector);
|
|
|
|
|
if (el) el.textContent = counts[key];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
|