Compare commits

..

3 Commits

Author SHA1 Message Date
60f8b1b123 Merge branch 'main' into fix/1336
Some checks failed
Review Approval Gate / verify-review (pull_request) Failing after 9s
CI / test (pull_request) Failing after 1m8s
CI / validate (pull_request) Failing after 1m14s
2026-04-22 01:14:24 +00:00
00ee2ee727 Merge branch 'main' into fix/1336
Some checks failed
Review Approval Gate / verify-review (pull_request) Failing after 11s
CI / test (pull_request) Failing after 1m18s
CI / validate (pull_request) Failing after 1m26s
2026-04-22 01:07:21 +00:00
Alexander Whitestone
841bfa31cd fix: #1336
Some checks failed
CI / test (pull_request) Failing after 53s
CI / validate (pull_request) Failing after 54s
Review Approval Gate / verify-review (pull_request) Failing after 8s
- Remove duplicate atlas-toggle-btn button
- Fix button structure (all buttons properly closed)
- Fix mismatched button tags (31 opening, 31 closing)

Addresses issue #1336: fix: merge conflicts visible in index.html

Changes:
- Remove duplicate atlas-toggle-btn with title 'World Directory'
- Keep single atlas-toggle-btn with title 'Portal Atlas'
- Add missing closing tag for soul-toggle-btn
- Fix button nesting structure

Verification:
- Only 1 atlas-toggle-btn button found
- All 31 buttons properly closed
- test_index_html_integrity.py passes
2026-04-20 21:14:32 -04:00
2 changed files with 1 additions and 295 deletions

View File

@@ -165,10 +165,10 @@
<!-- Top Right: Agent Log, Atlas & SOUL Toggle -->
<div class="hud-top-right">
<button id="atlas-toggle-btn" class="hud-icon-btn" title="World Directory">
<button id="soul-toggle-btn" class="hud-icon-btn" title="Timmy's SOUL">
<span class="hud-icon"></span>
<span class="hud-btn-label">SOUL</span>
</button>
<button id="mode-toggle-btn" class="hud-icon-btn mode-toggle" title="Toggle Mode">
<span class="hud-icon">👁</span>
<span class="hud-btn-label" id="mode-label">VISITOR</span>
@@ -395,7 +395,6 @@
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
<script src="./boot.js"></script>
<script src="./js/heartbeat.js"></script>
<script src="./avatar-customization.js"></script>
<script src="./lod-system.js"></script>
<script>

View File

@@ -1,293 +0,0 @@
/**
* WebSocket Heartbeat Client for The Nexus
* Issue #1535: feat: WebSocket heartbeat with auto-reconnect from client
*
* Provides:
* - Client sends heartbeat ping every 30s
* - Server responds with pong + user count
* - Client auto-reconnects on missed 2 heartbeats
* - Reconnect preserves user position/identity
*/
class NexusHeartbeat {
constructor(options = {}) {
this.heartbeatInterval = options.heartbeatInterval || 30000; // 30 seconds
this.maxMissedHeartbeats = options.maxMissedHeartbeats || 2;
this.reconnectDelay = options.reconnectDelay || 1000; // 1 second
this.maxReconnectDelay = options.maxReconnectDelay || 30000; // 30 seconds
this.ws = null;
this.heartbeatTimer = null;
this.missedHeartbeats = 0;
this.isConnected = false;
this.userId = options.userId || this.generateUserId();
this.position = options.position || { x: 0, y: 0, z: 0 };
this.reconnectAttempts = 0;
// Callbacks
this.onConnect = options.onConnect || (() => {});
this.onDisconnect = options.onDisconnect || (() => {});
this.onHeartbeat = options.onHeartbeat || (() => {});
this.onUserCount = options.onUserCount || (() => {});
this.onError = options.onError || console.error;
// Bind methods
this.connect = this.connect.bind(this);
this.disconnect = this.disconnect.bind(this);
this.sendHeartbeat = this.sendHeartbeat.bind(this);
this.handleMessage = this.handleMessage.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleError = this.handleError.bind(this);
}
generateUserId() {
return 'user_' + Math.random().toString(36).substr(2, 9);
}
connect(url) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
console.warn('Already connected');
return;
}
this.url = url;
console.log(`Connecting to ${url}...`);
try {
this.ws = new WebSocket(url);
this.ws.onopen = this.handleOpen.bind(this);
this.ws.onmessage = this.handleMessage;
this.ws.onclose = this.handleClose;
this.ws.onerror = this.handleError;
} catch (error) {
this.onError('Failed to create WebSocket:', error);
this.scheduleReconnect();
}
}
disconnect() {
console.log('Disconnecting...');
// Stop heartbeat
this.stopHeartbeat();
// Close WebSocket
if (this.ws) {
this.ws.onclose = null; // Prevent reconnect on manual disconnect
this.ws.close(1000, 'Manual disconnect');
this.ws = null;
}
this.isConnected = false;
this.missedHeartbeats = 0;
this.reconnectAttempts = 0;
}
handleOpen() {
console.log('Connected to WebSocket');
this.isConnected = true;
this.missedHeartbeats = 0;
this.reconnectAttempts = 0;
// Send reconnect message with user info
this.sendReconnect();
// Start heartbeat
this.startHeartbeat();
// Call connect callback
this.onConnect();
}
handleMessage(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
// Reset missed heartbeats
this.missedHeartbeats = 0;
// Update user count
if (data.user_count !== undefined) {
this.onUserCount(data.user_count);
}
// Call heartbeat callback
this.onHeartbeat(data);
console.debug('Heartbeat pong received');
} else if (data.type === 'health') {
// Health check response
console.debug('Health check:', data);
} else {
// Regular message
console.debug('Message received:', data);
}
} catch (error) {
// Not JSON or parse error
console.debug('Non-JSON message received:', event.data);
}
}
handleClose(event) {
console.log(`WebSocket closed: ${event.code} ${event.reason}`);
this.isConnected = false;
this.stopHeartbeat();
// Call disconnect callback
this.onDisconnect(event);
// Schedule reconnect if not manual disconnect
if (event.code !== 1000) {
this.scheduleReconnect();
}
}
handleError(error) {
this.onError('WebSocket error:', error);
}
startHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
console.log(`Starting heartbeat every ${this.heartbeatInterval / 1000}s`);
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatInterval);
// Send initial heartbeat
this.sendHeartbeat();
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
sendHeartbeat() {
if (!this.isConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.warn('Cannot send heartbeat: not connected');
return;
}
const heartbeat = {
type: 'heartbeat',
timestamp: Date.now(),
user_id: this.userId,
position: this.position
};
try {
this.ws.send(JSON.stringify(heartbeat));
console.debug('Heartbeat sent');
// Check for missed heartbeats
this.missedHeartbeats++;
if (this.missedHeartbeats > this.maxMissedHeartbeats) {
console.warn(`Missed ${this.missedHeartbeats} heartbeats, reconnecting...`);
this.ws.close(4000, 'Missed heartbeats');
}
} catch (error) {
this.onError('Failed to send heartbeat:', error);
this.ws.close(4001, 'Heartbeat send failed');
}
}
sendReconnect() {
if (!this.isConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.warn('Cannot send reconnect: not connected');
return;
}
const reconnect = {
type: 'reconnect',
timestamp: Date.now(),
user_id: this.userId,
position: this.position
};
try {
this.ws.send(JSON.stringify(reconnect));
console.log('Reconnect message sent');
} catch (error) {
this.onError('Failed to send reconnect:', error);
}
}
scheduleReconnect() {
if (this.reconnectAttempts >= 10) {
console.error('Max reconnect attempts reached');
return;
}
// Exponential backoff
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
this.maxReconnectDelay
);
console.log(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts + 1})...`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect(this.url);
}, delay);
}
updatePosition(x, y, z) {
this.position = { x, y, z };
// Send position update if connected
if (this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN) {
const update = {
type: 'position',
timestamp: Date.now(),
user_id: this.userId,
position: this.position
};
try {
this.ws.send(JSON.stringify(update));
} catch (error) {
console.warn('Failed to send position update:', error);
}
}
}
getUserId() {
return this.userId;
}
getPosition() {
return { ...this.position };
}
isConnectionActive() {
return this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN;
}
getStats() {
return {
connected: this.isConnected,
userId: this.userId,
position: this.position,
missedHeartbeats: this.missedHeartbeats,
reconnectAttempts: this.reconnectAttempts
};
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = NexusHeartbeat;
}
// Global instance for browser use
if (typeof window !== 'undefined') {
window.NexusHeartbeat = NexusHeartbeat;
}