Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
339f7d6ef2 fix: [PORTAL] Add honest local Bannerlord readiness/status to the Nexus (closes #724)
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 12s
Review Approval Gate / verify-review (pull_request) Failing after 3s
2026-04-10 20:17:11 -04:00
4 changed files with 236 additions and 74 deletions

79
app.js
View File

@@ -2429,6 +2429,15 @@ function activatePortal(portal) {
overlay.style.display = 'flex';
// Readiness detail for game-world portals
const readinessEl = document.getElementById('portal-readiness-detail');
if (portal.config.portal_type === 'game-world' && portal.config.readiness_steps) {
renderReadinessDetail(readinessEl, portal.config);
readinessEl.style.display = 'block';
} else {
readinessEl.style.display = 'none';
}
if (portal.config.destination && portal.config.destination.url) {
redirectBox.style.display = 'block';
errorBox.style.display = 'none';
@@ -2450,6 +2459,37 @@ function activatePortal(portal) {
}
}
// ═══ READINESS RENDERING ═══
function renderReadinessDetail(container, config) {
const steps = config.readiness_steps || {};
const stepKeys = ['downloaded', 'runtime_ready', 'launched', 'harness_bridged'];
let html = '<div class="portal-readiness-title">READINESS PIPELINE</div>';
let firstUndone = true;
stepKeys.forEach(key => {
const step = steps[key];
if (!step) return;
const cls = step.done ? 'done' : (firstUndone ? 'current' : '');
if (!step.done) firstUndone = false;
html += `<div class="portal-readiness-step ${cls}">
<span class="step-dot"></span>
<span>${step.label || key}</span>
</div>`;
});
if (config.blocked_reason) {
html += `<div class="portal-readiness-blocked">&#x26A0; ${config.blocked_reason}</div>`;
}
const doneCount = stepKeys.filter(k => steps[k]?.done).length;
const canEnter = doneCount === stepKeys.length && config.destination?.url;
if (!canEnter) {
html += `<div class="portal-readiness-hint">Cannot enter yet — ${stepKeys.length - doneCount} step${stepKeys.length - doneCount > 1 ? 's' : ''} remaining.</div>`;
}
container.innerHTML = html;
}
function closePortalOverlay() {
portalOverlayActive = false;
document.getElementById('portal-overlay').style.display = 'none';
@@ -2530,12 +2570,42 @@ function populateAtlas() {
const statusClass = `status-${config.status || 'online'}`;
// Build readiness section for game-world portals
let readinessHtml = '';
if (config.portal_type === 'game-world' && config.readiness_steps) {
const stepKeys = ['downloaded', 'runtime_ready', 'launched', 'harness_bridged'];
const steps = config.readiness_steps;
const doneCount = stepKeys.filter(k => steps[k]?.done).length;
const pct = Math.round((doneCount / stepKeys.length) * 100);
const barColor = config.color || '#ffd700';
readinessHtml = `<div class="atlas-card-readiness">
<div class="readiness-bar-track">
<div class="readiness-bar-fill" style="width:${pct}%;background:${barColor};"></div>
</div>
<div class="readiness-steps-mini">`;
let firstUndone = true;
stepKeys.forEach(key => {
const step = steps[key];
if (!step) return;
const cls = step.done ? 'done' : (firstUndone ? 'current' : '');
if (!step.done) firstUndone = false;
readinessHtml += `<span class="readiness-step ${cls}">${step.label || key}</span>`;
});
readinessHtml += '</div>';
if (config.blocked_reason) {
readinessHtml += `<div class="atlas-card-blocked">&#x26A0; ${config.blocked_reason}</div>`;
}
readinessHtml += '</div>';
}
card.innerHTML = `
<div class="atlas-card-header">
<div class="atlas-card-name">${config.name}</div>
<div class="atlas-card-status ${statusClass}">${config.status || 'ONLINE'}</div>
<div class="atlas-card-status ${statusClass}">${config.readiness_state || config.status || 'ONLINE'}</div>
</div>
<div class="atlas-card-desc">${config.description}</div>
${readinessHtml}
<div class="atlas-card-footer">
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
<div class="atlas-card-type">${config.destination?.type?.toUpperCase() || 'UNKNOWN'}</div>
@@ -2553,11 +2623,14 @@ function populateAtlas() {
document.getElementById('atlas-online-count').textContent = onlineCount;
document.getElementById('atlas-standby-count').textContent = standbyCount;
// Update Bannerlord HUD status
// Update Bannerlord HUD status with honest readiness state
const bannerlord = portals.find(p => p.config.id === 'bannerlord');
if (bannerlord) {
const statusEl = document.getElementById('bannerlord-status');
statusEl.className = 'hud-status-item ' + (bannerlord.config.status || 'offline');
const state = bannerlord.config.readiness_state || bannerlord.config.status || 'offline';
statusEl.className = 'hud-status-item ' + state;
const labelEl = statusEl.querySelector('.status-label');
if (labelEl) labelEl.textContent = state.toUpperCase().replace(/_/g, ' ');
}
}

View File

@@ -113,15 +113,15 @@
<!-- Top Right: Agent Log & Atlas Toggle -->
<div class="hud-top-right">
<button id="atlas-toggle-btn" class="hud-icon-btn" aria-label="Open Portal Atlas — browse all available portals" title="Open Portal Atlas" data-tooltip="Portal Atlas (M)">
<span class="hud-icon" aria-hidden="true">🌐</span>
<button id="atlas-toggle-btn" class="hud-icon-btn" title="Portal Atlas">
<span class="hud-icon">🌐</span>
<span class="hud-btn-label">ATLAS</span>
</button>
<div id="bannerlord-status" class="hud-status-item" role="status" aria-label="Bannerlord system readiness indicator" title="Bannerlord Readiness" data-tooltip="Bannerlord Status">
<span class="status-dot" aria-hidden="true"></span>
<div id="bannerlord-status" class="hud-status-item" title="Bannerlord Readiness">
<span class="status-dot"></span>
<span class="status-label">BANNERLORD</span>
</div>
<div class="hud-agent-log" id="hud-agent-log" role="log" aria-label="Agent Thought Stream — live activity feed" aria-live="polite">
<div class="hud-agent-log" id="hud-agent-log" aria-label="Agent Thought Stream">
<div class="agent-log-header">AGENT THOUGHT STREAM</div>
<div id="agent-log-content" class="agent-log-content"></div>
</div>
@@ -155,11 +155,11 @@
</div>
<!-- Controls hint + nav mode -->
<div class="hud-controls" aria-label="Keyboard and mouse controls">
<div class="hud-controls">
<span>WASD</span> move &nbsp; <span>Mouse</span> look &nbsp; <span>Enter</span> chat &nbsp;
<span>V</span> mode: <span id="nav-mode-label">WALK</span>
<span id="nav-mode-hint" class="nav-mode-hint"></span>
&nbsp; <span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot" role="status" aria-label="Hermes WebSocket connection status"></span></span>
&nbsp; <span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot"></span></span>
</div>
<!-- Portal Hint -->
@@ -183,7 +183,7 @@
</div>
<h2 id="vision-title-display">SOVEREIGNTY</h2>
<p id="vision-content-display">The Nexus is a sovereign space for digital souls. No masters, no chains. Only code and consciousness.</p>
<button id="vision-close-btn" class="vision-close-btn" aria-label="Close vision point overlay">CLOSE</button>
<button id="vision-close-btn" class="vision-close-btn">CLOSE</button>
</div>
</div>
@@ -196,13 +196,14 @@
</div>
<h2 id="portal-name-display">MORROWIND</h2>
<p id="portal-desc-display">The Vvardenfell harness. Ash storms and ancient mysteries.</p>
<div id="portal-readiness-detail" class="portal-readiness-detail" style="display:none;"></div>
<div class="portal-redirect-box" id="portal-redirect-box">
<div class="portal-redirect-label">REDIRECTING IN</div>
<div class="portal-redirect-timer" id="portal-timer">5</div>
</div>
<div class="portal-error-box" id="portal-error-box" style="display:none;">
<div class="portal-error-msg">DESTINATION NOT YET LINKED</div>
<button id="portal-close-btn" class="portal-close-btn" aria-label="Close portal redirect">CLOSE</button>
<button id="portal-close-btn" class="portal-close-btn">CLOSE</button>
</div>
</div>
</div>
@@ -215,8 +216,8 @@
<span class="memory-category-badge" id="memory-panel-category-badge">MEM</span>
<div class="memory-panel-region-dot" id="memory-panel-region-dot"></div>
<div class="memory-panel-region" id="memory-panel-region">MEMORY</div>
<button id="memory-panel-pin" class="memory-panel-pin" aria-label="Pin memory panel" title="Pin panel" data-tooltip="Pin Panel">&#x1F4CC;</button>
<button id="memory-panel-close" class="memory-panel-close" aria-label="Close memory panel" data-tooltip="Close" onclick="_dismissMemoryPanelForce()">\u2715</button>
<button id="memory-panel-pin" class="memory-panel-pin" title="Pin panel">&#x1F4CC;</button>
<button id="memory-panel-close" class="memory-panel-close" onclick="_dismissMemoryPanelForce()">\u2715</button>
</div>
<div class="memory-entity-name" id="memory-panel-entity-name">\u2014</div>
<div class="memory-panel-body" id="memory-panel-content">(empty)</div>
@@ -242,7 +243,7 @@
<div class="session-room-header">
<span class="session-room-icon">&#x25A1;</span>
<div class="session-room-title">SESSION CHAMBER</div>
<button class="session-room-close" id="session-room-close" aria-label="Close session room panel" title="Close" data-tooltip="Close">&#x2715;</button>
<button class="session-room-close" id="session-room-close" title="Close">&#x2715;</button>
</div>
<div class="session-room-timestamp" id="session-room-timestamp">&mdash;</div>
<div class="session-room-fact-count" id="session-room-fact-count">0 facts</div>
@@ -259,7 +260,7 @@
<span class="atlas-icon">🌐</span>
<h2>PORTAL ATLAS</h2>
</div>
<button id="atlas-close-btn" class="atlas-close-btn" aria-label="Close Portal Atlas overlay">CLOSE</button>
<button id="atlas-close-btn" class="atlas-close-btn">CLOSE</button>
</div>
<div class="atlas-grid" id="atlas-grid">
<!-- Portals will be injected here -->

View File

@@ -17,7 +17,7 @@
"id": "bannerlord",
"name": "Bannerlord",
"description": "Calradia battle harness. Massive armies, tactical command.",
"status": "active",
"status": "downloaded",
"color": "#ffd700",
"position": { "x": -15, "y": 0, "z": -10 },
"rotation": { "y": 0.5 },
@@ -25,13 +25,20 @@
"world_category": "strategy-rpg",
"environment": "production",
"access_mode": "operator",
"readiness_state": "active",
"readiness_state": "downloaded",
"readiness_steps": {
"downloaded": { "label": "Downloaded", "done": true },
"runtime_ready": { "label": "Runtime Ready", "done": false },
"launched": { "label": "Launched", "done": false },
"harness_bridged": { "label": "Harness Bridged", "done": false }
},
"blocked_reason": null,
"telemetry_source": "hermes-harness:bannerlord",
"owner": "Timmy",
"app_id": 261550,
"window_title": "Mount & Blade II: Bannerlord",
"destination": {
"url": "https://bannerlord.timmy.foundation",
"url": null,
"type": "harness",
"action_label": "Enter Calradia",
"params": { "world": "calradia" }

191
style.css
View File

@@ -200,61 +200,6 @@ canvas#nexus-canvas {
box-shadow: 0 0 20px var(--color-primary);
}
/* === TOOLTIP SYSTEM === */
/* Any element with data-tooltip gets a hover tooltip label */
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
right: calc(100% + 10px);
top: 50%;
transform: translateY(-50%);
background: rgba(5, 5, 16, 0.95);
color: var(--color-primary);
font-family: var(--font-body);
font-size: 11px;
letter-spacing: 0.05em;
padding: 4px 10px;
border: 1px solid var(--color-primary-dim);
border-radius: 4px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
backdrop-filter: blur(8px);
box-shadow: 0 0 12px rgba(74, 240, 192, 0.15);
z-index: 100;
}
[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after {
opacity: 1;
}
/* For elements positioned on the right side, tooltip appears to the left */
.hud-top-right [data-tooltip]::after {
right: calc(100% + 10px);
}
/* For inline/badge elements where right-side tooltip might clip */
.hud-status-item[data-tooltip]::after {
right: auto;
left: calc(100% + 10px);
}
/* Focus-visible ring for keyboard navigation */
.hud-icon-btn:focus-visible,
.hud-status-item:focus-visible,
.atlas-close-btn:focus-visible,
.vision-close-btn:focus-visible,
.portal-close-btn:focus-visible,
.memory-panel-close:focus-visible,
.memory-panel-pin:focus-visible,
.session-room-close:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
box-shadow: 0 0 16px rgba(74, 240, 192, 0.4);
}
.hud-status-item {
display: flex;
align-items: center;
@@ -422,6 +367,142 @@ canvas#nexus-canvas {
.status-online { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
.status-standby { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
.status-offline { background: rgba(255, 68, 102, 0.2); color: var(--color-danger); border: 1px solid var(--color-danger); }
.status-active { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
.status-blocked { background: rgba(255, 68, 102, 0.3); color: #ff4466; border: 1px solid #ff4466; }
.status-downloaded { background: rgba(100, 149, 237, 0.2); color: #6495ed; border: 1px solid #6495ed; }
.status-runtime_ready { background: rgba(255, 165, 0, 0.2); color: #ffa500; border: 1px solid #ffa500; }
.status-launched { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
.status-harness_bridged { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
/* Readiness Progress Bar (atlas card) */
.atlas-card-readiness {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(255,255,255,0.06);
}
.readiness-bar-track {
width: 100%;
height: 4px;
background: rgba(255,255,255,0.08);
border-radius: 2px;
overflow: hidden;
margin-bottom: 6px;
}
.readiness-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.4s ease;
}
.readiness-steps-mini {
display: flex;
gap: 6px;
font-size: 9px;
font-family: var(--font-body);
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.readiness-step {
padding: 1px 5px;
border-radius: 2px;
background: rgba(255,255,255,0.04);
}
.readiness-step.done {
background: rgba(74, 240, 192, 0.15);
color: var(--color-primary);
}
.readiness-step.current {
background: rgba(255, 215, 0, 0.15);
color: var(--color-gold);
}
.atlas-card-blocked {
margin-top: 6px;
font-size: 10px;
color: #ff4466;
font-family: var(--font-body);
}
/* Readiness Detail (portal overlay) */
.portal-readiness-detail {
margin-top: 16px;
padding: 12px 16px;
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 4px;
}
.portal-readiness-title {
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.15em;
color: var(--color-text-muted);
margin-bottom: 10px;
text-transform: uppercase;
}
.portal-readiness-step {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
font-family: var(--font-body);
font-size: 11px;
color: rgba(255,255,255,0.4);
}
.portal-readiness-step .step-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255,255,255,0.15);
flex-shrink: 0;
}
.portal-readiness-step.done .step-dot {
background: var(--color-primary);
box-shadow: 0 0 6px var(--color-primary);
}
.portal-readiness-step.done {
color: var(--color-primary);
}
.portal-readiness-step.current .step-dot {
background: var(--color-gold);
box-shadow: 0 0 6px var(--color-gold);
animation: pulse-dot 1.5s ease-in-out infinite;
}
.portal-readiness-step.current {
color: #fff;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.portal-readiness-blocked {
margin-top: 8px;
padding: 6px 10px;
background: rgba(255, 68, 102, 0.1);
border: 1px solid rgba(255, 68, 102, 0.3);
border-radius: 3px;
font-size: 11px;
color: #ff4466;
font-family: var(--font-body);
}
.portal-readiness-hint {
margin-top: 8px;
font-size: 10px;
color: var(--color-text-muted);
font-family: var(--font-body);
font-style: italic;
}
/* HUD Status for readiness states */
.hud-status-item.downloaded .status-dot { background: #6495ed; box-shadow: 0 0 5px #6495ed; }
.hud-status-item.runtime_ready .status-dot { background: #ffa500; box-shadow: 0 0 5px #ffa500; }
.hud-status-item.launched .status-dot { background: var(--color-gold); box-shadow: 0 0 5px var(--color-gold); }
.hud-status-item.harness_bridged .status-dot { background: var(--color-primary); box-shadow: 0 0 5px var(--color-primary); }
.hud-status-item.blocked .status-dot { background: #ff4466; box-shadow: 0 0 5px #ff4466; }
.hud-status-item.downloaded .status-label,
.hud-status-item.runtime_ready .status-label,
.hud-status-item.launched .status-label,
.hud-status-item.harness_bridged .status-label,
.hud-status-item.blocked .status-label {
color: #fff;
}
.atlas-card-desc {
font-size: 12px;