diff --git a/app.js b/app.js
index 10156247..a1b05585 100644
--- a/app.js
+++ b/app.js
@@ -2498,6 +2498,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';
@@ -2519,6 +2528,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 = '
Cannot enter yet — ${stepKeys.length - doneCount} step${stepKeys.length - doneCount > 1 ? 's' : ''} remaining.
`;
+ }
+
+ container.innerHTML = html;
+}
+
function closePortalOverlay() {
portalOverlayActive = false;
document.getElementById('portal-overlay').style.display = 'none';
@@ -2599,12 +2639,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 = `The Vvardenfell harness. Ash storms and ancient mysteries.
+
REDIRECTING IN
5
diff --git a/portals.json b/portals.json
index 6b7e2870..b870c62d 100644
--- a/portals.json
+++ b/portals.json
@@ -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" }
diff --git a/style.css b/style.css
index e171946f..1a459a7e 100644
--- a/style.css
+++ b/style.css
@@ -422,6 +422,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;