Compare commits
2 Commits
mimo/code/
...
fix/1539-1
| Author | SHA1 | Date | |
|---|---|---|---|
| 03d795cd54 | |||
|
|
d779f180ee |
6
app.js
6
app.js
@@ -734,6 +734,8 @@ async function init() {
|
||||
const response = await fetch('./portals.json');
|
||||
const portalData = await response.json();
|
||||
createPortals(portalData);
|
||||
// Initialize portal health checks (#1539)
|
||||
if (window.PortalHealthCheck) window.PortalHealthCheck.init(portals);
|
||||
} catch (e) {
|
||||
console.error('Failed to load portals.json:', e);
|
||||
addChatMessage('error', 'Portal registry offline. Check logs.');
|
||||
@@ -3572,7 +3574,9 @@ function gameLoop() {
|
||||
animateMemoryOrbs(delta);
|
||||
}
|
||||
|
||||
updatePortalTunnel(delta, elapsed);
|
||||
updatePortalTunnel(delta, elapsed);
|
||||
// Update portal health visuals (#1539)
|
||||
if (window.PortalHealthCheck) window.PortalHealthCheck.update(portals);
|
||||
|
||||
if (workshopScanMat) workshopScanMat.uniforms.uTime.value = clock.getElapsedTime();
|
||||
if (activePortal !== lastFocusedPortal) {
|
||||
|
||||
@@ -396,6 +396,7 @@
|
||||
|
||||
<script src="./boot.js"></script>
|
||||
<script src="./avatar-customization.js"></script>
|
||||
<script src="./portal-health-check.js"></script>
|
||||
<script src="./lod-system.js"></script>
|
||||
<script>
|
||||
function openMemoryFilter() { renderFilterList(); document.getElementById('memory-filter').style.display = 'flex'; }
|
||||
|
||||
168
portal-health-check.js
Normal file
168
portal-health-check.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Portal Health Check Module (#1539)
|
||||
*
|
||||
* Monitors portal status and updates visuals:
|
||||
* - Dims offline/blocked portals
|
||||
* - Shows "Offline" tooltip
|
||||
* - Auto-re-enables when status changes to online
|
||||
* - Runs check every 5 minutes
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const DIM_OPACITY = 0.15;
|
||||
const NORMAL_OPACITY = 1.0;
|
||||
const FADE_SPEED = 0.02;
|
||||
|
||||
let portalHealthStates = {}; // portal_id -> { healthy, fading, currentOpacity, targetOpacity }
|
||||
let checkTimer = null;
|
||||
|
||||
// ═══ Health Check ═══════════════════════════════════════════
|
||||
function checkPortalHealth(portals, wsConnected) {
|
||||
for (const portal of portals) {
|
||||
const id = portal.config.id;
|
||||
const status = portal.config.status || 'unknown';
|
||||
const blocked = portal.config.blocked_reason;
|
||||
const interactionReady = portal.config.interaction_ready !== false;
|
||||
|
||||
// Determine health
|
||||
const healthy = status === 'online' && !blocked && interactionReady && wsConnected;
|
||||
|
||||
if (!portalHealthStates[id]) {
|
||||
portalHealthStates[id] = { healthy: true, currentOpacity: NORMAL_OPACITY, targetOpacity: NORMAL_OPACITY };
|
||||
}
|
||||
|
||||
const state = portalHealthStates[id];
|
||||
const wasHealthy = state.healthy;
|
||||
state.healthy = healthy;
|
||||
state.targetOpacity = healthy ? NORMAL_OPACITY : DIM_OPACITY;
|
||||
|
||||
// Log state changes
|
||||
if (wasHealthy !== healthy) {
|
||||
const label = portal.config.name || id;
|
||||
if (healthy) {
|
||||
console.log(`[PortalHealth] ${label} — back online`);
|
||||
} else {
|
||||
const reason = blocked || (!wsConnected ? 'bridge offline' : `status: ${status}`);
|
||||
console.log(`[PortalHealth] ${label} — offline (${reason})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ Visual Update ══════════════════════════════════════════
|
||||
function updatePortalVisuals(portals) {
|
||||
for (const portal of portals) {
|
||||
const id = portal.config.id;
|
||||
const state = portalHealthStates[id];
|
||||
if (!state) continue;
|
||||
|
||||
// Smooth fade toward target opacity
|
||||
if (Math.abs(state.currentOpacity - state.targetOpacity) > 0.01) {
|
||||
state.currentOpacity += (state.targetOpacity - state.currentOpacity) * FADE_SPEED;
|
||||
} else {
|
||||
state.currentOpacity = state.targetOpacity;
|
||||
}
|
||||
|
||||
const opacity = state.currentOpacity;
|
||||
const dimmed = opacity < NORMAL_OPACITY * 0.5;
|
||||
|
||||
// Apply to portal meshes
|
||||
if (portal.ring) {
|
||||
portal.ring.material.opacity = opacity;
|
||||
portal.ring.material.transparent = true;
|
||||
portal.ring.material.emissiveIntensity = dimmed ? 0.2 : 1.5;
|
||||
}
|
||||
|
||||
if (portal.swirl) {
|
||||
portal.swirl.material.uniforms.uTime.value += dimmed ? 0.001 : 0.016; // Slow/stop swirl when offline
|
||||
portal.swirl.material.opacity = opacity;
|
||||
}
|
||||
|
||||
if (portal.pSystem) {
|
||||
portal.pSystem.material.opacity = opacity * 0.6;
|
||||
}
|
||||
|
||||
if (portal.light) {
|
||||
portal.light.intensity = dimmed ? 0.3 : 2;
|
||||
}
|
||||
|
||||
// Update label to show offline status
|
||||
if (portal.labelMesh && dimmed) {
|
||||
updatePortalLabel(portal, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updatePortalLabel(portal, state) {
|
||||
// Re-render label canvas with offline indicator
|
||||
const labelMesh = portal.labelMesh;
|
||||
if (!labelMesh || !labelMesh.material.map) return;
|
||||
|
||||
// We can't easily re-render a canvas texture each frame,
|
||||
// so we'll use a separate overlay sprite for offline status
|
||||
if (!portal.offlineLabel) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 68, 102, 0.8)';
|
||||
ctx.roundRect(0, 0, 256, 64, 8);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 20px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('⚠ OFFLINE', 128, 32);
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(2, 0.5, 1);
|
||||
sprite.position.y = 8.5;
|
||||
portal.group.add(sprite);
|
||||
portal.offlineLabel = sprite;
|
||||
}
|
||||
|
||||
portal.offlineLabel.visible = state.currentOpacity < NORMAL_OPACITY * 0.5;
|
||||
}
|
||||
|
||||
// ═══ Public API ═════════════════════════════════════════════
|
||||
window.PortalHealthCheck = {
|
||||
init(portalsRef) {
|
||||
// Initial check
|
||||
this.check(portalsRef, true);
|
||||
// Periodic checks
|
||||
if (checkTimer) clearInterval(checkTimer);
|
||||
checkTimer = setInterval(() => this.check(portalsRef, true), CHECK_INTERVAL);
|
||||
console.log('[PortalHealth] Initialized — checking every 5 minutes');
|
||||
},
|
||||
|
||||
check(portalsRef, wsConnected) {
|
||||
checkPortalHealth(portalsRef, wsConnected);
|
||||
},
|
||||
|
||||
update(portalsRef) {
|
||||
updatePortalVisuals(portalsRef);
|
||||
},
|
||||
|
||||
getStatus() {
|
||||
return { ...portalHealthStates };
|
||||
},
|
||||
|
||||
isHealthy(portalId) {
|
||||
return portalHealthStates[portalId]?.healthy !== false;
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (checkTimer) {
|
||||
clearInterval(checkTimer);
|
||||
checkTimer = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user