Compare commits
1 Commits
mimo/creat
...
mimo/code/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
339f7d6ef2 |
79
app.js
79
app.js
@@ -2429,6 +2429,15 @@ function activatePortal(portal) {
|
|||||||
|
|
||||||
overlay.style.display = 'flex';
|
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) {
|
if (portal.config.destination && portal.config.destination.url) {
|
||||||
redirectBox.style.display = 'block';
|
redirectBox.style.display = 'block';
|
||||||
errorBox.style.display = 'none';
|
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">⚠ ${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() {
|
function closePortalOverlay() {
|
||||||
portalOverlayActive = false;
|
portalOverlayActive = false;
|
||||||
document.getElementById('portal-overlay').style.display = 'none';
|
document.getElementById('portal-overlay').style.display = 'none';
|
||||||
@@ -2530,12 +2570,42 @@ function populateAtlas() {
|
|||||||
|
|
||||||
const statusClass = `status-${config.status || 'online'}`;
|
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">⚠ ${config.blocked_reason}</div>`;
|
||||||
|
}
|
||||||
|
readinessHtml += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="atlas-card-header">
|
<div class="atlas-card-header">
|
||||||
<div class="atlas-card-name">${config.name}</div>
|
<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>
|
||||||
<div class="atlas-card-desc">${config.description}</div>
|
<div class="atlas-card-desc">${config.description}</div>
|
||||||
|
${readinessHtml}
|
||||||
<div class="atlas-card-footer">
|
<div class="atlas-card-footer">
|
||||||
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
|
<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>
|
<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-online-count').textContent = onlineCount;
|
||||||
document.getElementById('atlas-standby-count').textContent = standbyCount;
|
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');
|
const bannerlord = portals.find(p => p.config.id === 'bannerlord');
|
||||||
if (bannerlord) {
|
if (bannerlord) {
|
||||||
const statusEl = document.getElementById('bannerlord-status');
|
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, ' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h2 id="portal-name-display">MORROWIND</h2>
|
<h2 id="portal-name-display">MORROWIND</h2>
|
||||||
<p id="portal-desc-display">The Vvardenfell harness. Ash storms and ancient mysteries.</p>
|
<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-box" id="portal-redirect-box">
|
||||||
<div class="portal-redirect-label">REDIRECTING IN</div>
|
<div class="portal-redirect-label">REDIRECTING IN</div>
|
||||||
<div class="portal-redirect-timer" id="portal-timer">5</div>
|
<div class="portal-redirect-timer" id="portal-timer">5</div>
|
||||||
|
|||||||
13
portals.json
13
portals.json
@@ -17,7 +17,7 @@
|
|||||||
"id": "bannerlord",
|
"id": "bannerlord",
|
||||||
"name": "Bannerlord",
|
"name": "Bannerlord",
|
||||||
"description": "Calradia battle harness. Massive armies, tactical command.",
|
"description": "Calradia battle harness. Massive armies, tactical command.",
|
||||||
"status": "active",
|
"status": "downloaded",
|
||||||
"color": "#ffd700",
|
"color": "#ffd700",
|
||||||
"position": { "x": -15, "y": 0, "z": -10 },
|
"position": { "x": -15, "y": 0, "z": -10 },
|
||||||
"rotation": { "y": 0.5 },
|
"rotation": { "y": 0.5 },
|
||||||
@@ -25,13 +25,20 @@
|
|||||||
"world_category": "strategy-rpg",
|
"world_category": "strategy-rpg",
|
||||||
"environment": "production",
|
"environment": "production",
|
||||||
"access_mode": "operator",
|
"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",
|
"telemetry_source": "hermes-harness:bannerlord",
|
||||||
"owner": "Timmy",
|
"owner": "Timmy",
|
||||||
"app_id": 261550,
|
"app_id": 261550,
|
||||||
"window_title": "Mount & Blade II: Bannerlord",
|
"window_title": "Mount & Blade II: Bannerlord",
|
||||||
"destination": {
|
"destination": {
|
||||||
"url": "https://bannerlord.timmy.foundation",
|
"url": null,
|
||||||
"type": "harness",
|
"type": "harness",
|
||||||
"action_label": "Enter Calradia",
|
"action_label": "Enter Calradia",
|
||||||
"params": { "world": "calradia" }
|
"params": { "world": "calradia" }
|
||||||
|
|||||||
136
style.css
136
style.css
@@ -367,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-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-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-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 {
|
.atlas-card-desc {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user