Compare commits

...

2 Commits

Author SHA1 Message Date
STEP35
012a1fe83e fix(harness): replace dead /api/world/ws with configurable WS bridge
Some checks failed
CI / test (pull_request) Failing after 1m15s
Review Approval Gate / verify-review (pull_request) Failing after 14s
CI / validate (pull_request) Failing after 1m20s
- Replace dead HTTP path with direct WebSocket connection to server.py
- Add ws_host and ws_port query-param config (defaults: localhost:8765)
- Remove duplicate initializeMemPalace() call in connectHermes()
- Follows L402_PORT pattern for environment configuration

Closes #694
2026-04-30 20:22:42 -04:00
60e8b62a79 feat(portals): wire portal status wall to live portals.json (#1720)
Some checks failed
Deploy Nexus / deploy (push) Failing after 8s
Staging Verification Gate / verify-staging (push) Failing after 20s
2026-04-29 12:09:42 +00:00
2 changed files with 155 additions and 55 deletions

12
app.js
View File

@@ -2277,9 +2277,6 @@ function sendChatMessage(overrideText = null) {
// ═══ HERMES WEBSOCKET ═══
function connectHermes() {
// Initialize MemPalace before Hermes connection
initializeMemPalace();
// Existing Hermes connection code...
// Initialize MemPalace before Hermes connection
initializeMemPalace();
if (hermesWs) return;
@@ -2287,8 +2284,6 @@ function connectHermes() {
// Initialize MemPalace storage
try {
console.log('Initializing MemPalace memory system...');
// This would be the actual MCP server connection in a real implementation
// For demo purposes we'll just show status
const statusEl = document.getElementById('mem-palace-status');
if (statusEl) {
statusEl.textContent = 'MEMPALACE INITIALIZING';
@@ -2303,8 +2298,11 @@ function connectHermes() {
}
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/world/ws`;
// WebSocket gateway configuration — connects to server.py (Nexus WS bridge)
const WS_HOST = new URLSearchParams(window.location.search).get('ws_host') || 'localhost';
const WS_PORT = parseInt(new URLSearchParams(window.location.search).get('ws_port') || '8765');
const isSecure = window.location.protocol === 'https:';
const wsUrl = `${isSecure ? 'wss:' : 'ws:'}//${WS_HOST}:${WS_PORT}`;
console.log(`Connecting to Hermes at ${wsUrl}...`);
hermesWs = new WebSocket(wsUrl);

View File

@@ -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 => ({
'&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;'
})[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>