Compare commits
2 Commits
gofai-know
...
nexus-hear
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d6d536a1c | |||
| 3091d3cdd9 |
@@ -75,160 +75,12 @@ const orbitState = {
|
||||
|
||||
let flyY = 2;
|
||||
|
||||
// ═══ SOVEREIGN SYMBOLIC ENGINE (GOFAI) ═══
|
||||
class SymbolicEngine {
|
||||
constructor() {
|
||||
this.facts = new Map();
|
||||
this.rules = [];
|
||||
this.reasoningLog = [];
|
||||
}
|
||||
|
||||
addFact(key, value) {
|
||||
this.facts.set(key, value);
|
||||
}
|
||||
|
||||
addRule(condition, action, description) {
|
||||
this.rules.push({ condition, action, description });
|
||||
}
|
||||
|
||||
reason() {
|
||||
this.rules.forEach(rule => {
|
||||
if (rule.condition(this.facts)) {
|
||||
const result = rule.action(this.facts);
|
||||
if (result) {
|
||||
this.logReasoning(rule.description, result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logReasoning(ruleDesc, outcome) {
|
||||
const entry = {
|
||||
timestamp: Date.now(),
|
||||
rule: ruleDesc,
|
||||
outcome: outcome
|
||||
};
|
||||
this.reasoningLog.unshift(entry);
|
||||
if (this.reasoningLog.length > 5) this.reasoningLog.pop();
|
||||
|
||||
// Update HUD if available
|
||||
const container = document.getElementById('symbolic-log-content');
|
||||
if (container) {
|
||||
const logDiv = document.createElement('div');
|
||||
logDiv.className = 'symbolic-log-entry';
|
||||
logDiv.innerHTML = `<span class="symbolic-rule">[RULE] ${ruleDesc}</span><span class="symbolic-outcome">→ ${outcome}</span>`;
|
||||
container.prepend(logDiv);
|
||||
if (container.children.length > 5) container.lastElementChild.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AgentFSM {
|
||||
constructor(agentId, initialState) {
|
||||
this.agentId = agentId;
|
||||
this.state = initialState;
|
||||
this.transitions = {};
|
||||
}
|
||||
|
||||
addTransition(fromState, toState, condition) {
|
||||
if (!this.transitions[fromState]) this.transitions[fromState] = [];
|
||||
this.transitions[fromState].push({ toState, condition });
|
||||
}
|
||||
|
||||
update(facts) {
|
||||
const possibleTransitions = this.transitions[this.state] || [];
|
||||
for (const transition of possibleTransitions) {
|
||||
if (transition.condition(facts)) {
|
||||
console.log(`[FSM] Agent ${this.agentId} transitioning: ${this.state} -> ${transition.toState}`);
|
||||
this.state = transition.toState;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ SOVEREIGN KNOWLEDGE GRAPH (SEMANTIC MEMORY) ═══
|
||||
class KnowledgeGraph {
|
||||
constructor() {
|
||||
this.nodes = new Map(); // id -> { data }
|
||||
this.edges = []; // { from, to, relation }
|
||||
}
|
||||
|
||||
addNode(id, type, metadata = {}) {
|
||||
this.nodes.set(id, { id, type, ...metadata });
|
||||
}
|
||||
|
||||
addEdge(from, to, relation) {
|
||||
this.edges.push({ from, to, relation });
|
||||
}
|
||||
|
||||
query(from, relation) {
|
||||
return this.edges
|
||||
.filter(e => e.from === from && e.relation === relation)
|
||||
.map(e => this.nodes.get(e.to));
|
||||
}
|
||||
|
||||
getRelated(id) {
|
||||
return this.edges.filter(e => e.from === id || e.to === id);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ BLACKBOARD ARCHITECTURE (COLLABORATIVE INTELLIGENCE) ═══
|
||||
class Blackboard {
|
||||
constructor() {
|
||||
this.data = {};
|
||||
this.subscribers = [];
|
||||
}
|
||||
|
||||
write(key, value, source) {
|
||||
const oldValue = this.data[key];
|
||||
this.data[key] = value;
|
||||
this.notify(key, value, oldValue, source);
|
||||
}
|
||||
|
||||
read(key) {
|
||||
return this.data[key];
|
||||
}
|
||||
|
||||
subscribe(callback) {
|
||||
this.subscribers.push(callback);
|
||||
}
|
||||
|
||||
notify(key, value, oldValue, source) {
|
||||
this.subscribers.forEach(sub => sub(key, value, oldValue, source));
|
||||
|
||||
// Log to HUD
|
||||
const container = document.getElementById('blackboard-log-content');
|
||||
if (container) {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'blackboard-entry';
|
||||
entry.innerHTML = `<span class="bb-source">[${source}]</span> <span class="bb-key">${key}</span>: <span class="bb-value">${JSON.stringify(value)}</span>`;
|
||||
container.prepend(entry);
|
||||
if (container.children.length > 8) container.lastElementChild.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let knowledgeGraph;
|
||||
let blackboard;
|
||||
let symbolicEngine;
|
||||
let agentFSMs = {};
|
||||
|
||||
// ═══ INIT ═══
|
||||
async function init() {
|
||||
clock = new THREE.Clock();
|
||||
playerPos = new THREE.Vector3(0, 2, 12);
|
||||
playerRot = new THREE.Euler(0, 0, 0, 'YXZ');
|
||||
|
||||
// Initialize GOFAI Stack
|
||||
knowledgeGraph = new KnowledgeGraph();
|
||||
blackboard = new Blackboard();
|
||||
symbolicEngine = new SymbolicEngine();
|
||||
|
||||
setupKnowledgeBase();
|
||||
setupSymbolicRules();
|
||||
|
||||
const canvas = document.getElementById('nexus-canvas');
|
||||
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
@@ -334,85 +186,6 @@ function updateLoad(pct) {
|
||||
if (fill) fill.style.width = pct + '%';
|
||||
}
|
||||
|
||||
function setupKnowledgeBase() {
|
||||
// Define Core Entities
|
||||
knowledgeGraph.addNode('nexus', 'location', { name: 'The Nexus Core' });
|
||||
knowledgeGraph.addNode('timmy', 'agent', { name: 'Timmy' });
|
||||
knowledgeGraph.addNode('evonia', 'layer', { name: 'Evonia System' });
|
||||
|
||||
// Define Relationships
|
||||
knowledgeGraph.addEdge('timmy', 'nexus', 'is_at');
|
||||
knowledgeGraph.addEdge('nexus', 'evonia', 'monitored_by');
|
||||
|
||||
// Initialize Blackboard with system defaults
|
||||
blackboard.write('system_status', 'INITIALIZING', 'SYSTEM');
|
||||
blackboard.write('threat_level', 0, 'SYSTEM');
|
||||
}
|
||||
|
||||
function setupSymbolicRules() {
|
||||
// Facts: Energy, Stability, Portal Status
|
||||
symbolicEngine.addFact('energy', 100);
|
||||
symbolicEngine.addFact('stability', 1.0);
|
||||
symbolicEngine.addFact('activePortals', 0);
|
||||
|
||||
// Rule: Low Energy Recovery
|
||||
symbolicEngine.addRule(
|
||||
(facts) => facts.get('energy') < 20,
|
||||
(facts) => {
|
||||
facts.set('mode', 'RECOVERY');
|
||||
return 'Diverting power to core systems';
|
||||
},
|
||||
'Low Energy Protocol'
|
||||
);
|
||||
|
||||
// Rule: Stability Alert
|
||||
symbolicEngine.addRule(
|
||||
(facts) => facts.get('stability') < 0.5,
|
||||
(facts) => {
|
||||
facts.set('alert', 'CRITICAL');
|
||||
return 'Initiating matrix stabilization';
|
||||
},
|
||||
'Stability Safeguard'
|
||||
);
|
||||
|
||||
// FSMs for Agents
|
||||
agentFSMs['timmy'] = new AgentFSM('timmy', 'IDLE');
|
||||
agentFSMs['timmy'].addTransition('IDLE', 'ANALYZING', (facts) => facts.get('activePortals') > 0);
|
||||
agentFSMs['timmy'].addTransition('ANALYZING', 'IDLE', (facts) => facts.get('activePortals') === 0);
|
||||
agentFSMs['timmy'].addTransition('IDLE', 'ALERT', (facts) => facts.get('stability') < 0.7);
|
||||
agentFSMs['timmy'].addTransition('ALERT', 'IDLE', (facts) => facts.get('stability') >= 0.7);
|
||||
}
|
||||
|
||||
function updateSymbolicAI(delta, elapsed) {
|
||||
// Sync facts from world state
|
||||
const terminal = batcaveTerminals.find(t => t.title === 'NEXUS COMMAND');
|
||||
if (terminal && terminal.lastState) {
|
||||
const state = terminal.lastState;
|
||||
symbolicEngine.addFact('energy', state.tower.energy);
|
||||
symbolicEngine.addFact('stability', state.matrix.stability);
|
||||
symbolicEngine.addFact('activePortals', portals.filter(p => p.config.status === 'online').length);
|
||||
|
||||
// Update Blackboard
|
||||
blackboard.write('nexus_energy', state.tower.energy, 'NEXUS_COMMAND');
|
||||
blackboard.write('nexus_stability', state.matrix.stability, 'NEXUS_COMMAND');
|
||||
}
|
||||
|
||||
// Run reasoning engine
|
||||
if (Math.floor(elapsed * 2) > Math.floor((elapsed - delta) * 2)) { // Every 0.5s
|
||||
symbolicEngine.reason();
|
||||
Object.values(agentFSMs).forEach(fsm => fsm.update(symbolicEngine.facts));
|
||||
|
||||
// Update Knowledge Graph based on portal status
|
||||
portals.forEach(p => {
|
||||
const nodeId = `portal_${p.config.id}`;
|
||||
if (!knowledgeGraph.nodes.has(nodeId)) {
|
||||
knowledgeGraph.addNode(nodeId, 'portal', { name: p.config.name });
|
||||
}
|
||||
knowledgeGraph.addEdge(nodeId, 'nexus', p.config.status === 'online' ? 'connected_to' : 'disconnected_from');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ PERFORMANCE BUDGET ═══
|
||||
function detectPerformanceTier() {
|
||||
const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || window.innerWidth < 768;
|
||||
@@ -834,8 +607,6 @@ function updateNexusCommand(state) {
|
||||
const terminal = batcaveTerminals.find(t => t.title === 'NEXUS COMMAND');
|
||||
if (!terminal) return;
|
||||
|
||||
terminal.lastState = state; // Store for symbolic engine
|
||||
|
||||
const lines = [
|
||||
`> STATUS: ${state.tower.status.toUpperCase()}`,
|
||||
`> ENERGY: ${state.tower.energy}%`,
|
||||
@@ -1986,7 +1757,6 @@ function gameLoop() {
|
||||
|
||||
updateAshStorm(delta, elapsed);
|
||||
updateNexusHeartbeat(delta, elapsed);
|
||||
updateSymbolicAI(delta, elapsed);
|
||||
|
||||
const mode = NAV_MODES[navModeIdx];
|
||||
const chatActive = document.activeElement === document.getElementById('chat-input');
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<!--
|
||||
______ __
|
||||
/ ____/___ ____ ___ ____ __ __/ /____ _____
|
||||
/ / / __ \/ __ `__ \/ __ \/ / / / __/ _ \/ ___/
|
||||
/ /___/ /_/ / / / / / / /_/ / /_/ / /_/ __/ /
|
||||
\____/\____/_/ /_/ /_/ .___/\__,_/\__/\___/_/
|
||||
/_/
|
||||
Created with Perplexity Computer
|
||||
https://www.perplexity.ai/computer
|
||||
-->
|
||||
<meta name="generator" content="Perplexity Computer">
|
||||
<meta name="author" content="Perplexity Computer">
|
||||
<meta property="og:see_also" content="https://www.perplexity.ai/computer">
|
||||
<link rel="author" href="https://www.perplexity.ai/computer">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Nexus — Timmy's Sovereign Home</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js",
|
||||
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Loading Screen -->
|
||||
<div id="loading-screen">
|
||||
<div class="loader-content">
|
||||
<div class="loader-sigil">
|
||||
<svg viewBox="0 0 120 120" width="120" height="120">
|
||||
<defs>
|
||||
<linearGradient id="sigil-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#4af0c0"/>
|
||||
<stop offset="100%" stop-color="#7b5cff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="60" cy="60" r="55" fill="none" stroke="url(#sigil-grad)" stroke-width="1.5" opacity="0.4"/>
|
||||
<circle cx="60" cy="60" r="45" fill="none" stroke="url(#sigil-grad)" stroke-width="1" opacity="0.3">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="360 60 60" dur="8s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<polygon points="60,15 95,80 25,80" fill="none" stroke="#4af0c0" stroke-width="1.5" opacity="0.6">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="-360 60 60" dur="12s" repeatCount="indefinite"/>
|
||||
</polygon>
|
||||
<circle cx="60" cy="60" r="8" fill="#4af0c0" opacity="0.8">
|
||||
<animate attributeName="r" values="6;10;6" dur="2s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="loader-title">THE NEXUS</h1>
|
||||
<p class="loader-subtitle">Initializing Sovereign Space...</p>
|
||||
<div class="loader-bar"><div class="loader-fill" id="load-progress"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HUD Overlay -->
|
||||
<div id="hud" class="game-ui" style="display:none;">
|
||||
<!-- Top Left: Debug & Heartbeat -->
|
||||
<div class="hud-top-left">
|
||||
<div id="debug-overlay" class="hud-debug"></div>
|
||||
<div id="nexus-heartbeat" class="hud-heartbeat" title="Nexus Pulse">
|
||||
<div class="heartbeat-pulse"></div>
|
||||
<div class="heartbeat-label">NEXUS PULSE</div>
|
||||
<div id="heartbeat-value" class="heartbeat-value">0.00 Hz</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Center: Location -->
|
||||
<div class="hud-location" aria-live="polite">
|
||||
<span class="hud-location-icon" aria-hidden="true">◈</span>
|
||||
<span id="hud-location-text">The Nexus</span>
|
||||
</div>
|
||||
|
||||
<!-- Top Right: Agent Log & Atlas Toggle -->
|
||||
<div class="hud-top-right">
|
||||
<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" title="Bannerlord Readiness">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-label">BANNERLORD</span>
|
||||
</div>
|
||||
<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>
|
||||
<div class="hud-symbolic-log" id="hud-symbolic-log" aria-label="Sovereign Symbolic Engine">
|
||||
<div class="symbolic-log-header">SYMBOLIC REASONING</div>
|
||||
<div id="symbolic-log-content" class="symbolic-log-content"></div>
|
||||
</div>
|
||||
<div class="hud-blackboard-log" id="hud-blackboard-log" aria-label="Blackboard Architecture">
|
||||
<div class="blackboard-log-header">BLACKBOARD (SHARED MEMORY)</div>
|
||||
<div id="blackboard-log-content" class="blackboard-log-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom: Chat Interface -->
|
||||
<div id="chat-panel" class="chat-panel">
|
||||
<div class="chat-header">
|
||||
<span class="chat-status-dot"></span>
|
||||
<span>Timmy Terminal</span>
|
||||
<button id="chat-toggle" class="chat-toggle-btn" aria-label="Toggle chat">▼</button>
|
||||
</div>
|
||||
<div id="chat-messages" class="chat-messages">
|
||||
<div class="chat-msg chat-msg-system">
|
||||
<span class="chat-msg-prefix">[NEXUS]</span> Sovereign space initialized. Timmy is observing.
|
||||
</div>
|
||||
<div class="chat-msg chat-msg-timmy">
|
||||
<span class="chat-msg-prefix">[TIMMY]</span> Welcome to the Nexus, Alexander. All systems nominal.
|
||||
</div>
|
||||
</div>
|
||||
<div id="chat-quick-actions" class="chat-quick-actions">
|
||||
<button class="quick-action-btn" data-action="status">System Status</button>
|
||||
<button class="quick-action-btn" data-action="agents">Agent Check</button>
|
||||
<button class="quick-action-btn" data-action="portals">Portal Atlas</button>
|
||||
<button class="quick-action-btn" data-action="help">Help</button>
|
||||
</div>
|
||||
<div class="chat-input-row">
|
||||
<input type="text" id="chat-input" class="chat-input" placeholder="Speak to Timmy..." autocomplete="off">
|
||||
<button id="chat-send" class="chat-send-btn" aria-label="Send message">→</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls hint + nav mode -->
|
||||
<div class="hud-controls">
|
||||
<span>WASD</span> move <span>Mouse</span> look <span>Enter</span> chat
|
||||
<span>V</span> mode: <span id="nav-mode-label">WALK</span>
|
||||
<span id="nav-mode-hint" class="nav-mode-hint"></span>
|
||||
<span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot"></span></span>
|
||||
</div>
|
||||
|
||||
<!-- Portal Hint -->
|
||||
<div id="portal-hint" class="portal-hint" style="display:none;">
|
||||
<div class="portal-hint-key">F</div>
|
||||
<div class="portal-hint-text">Enter <span id="portal-hint-name"></span></div>
|
||||
</div>
|
||||
|
||||
<!-- Vision Hint -->
|
||||
<div id="vision-hint" class="vision-hint" style="display:none;">
|
||||
<div class="vision-hint-key">E</div>
|
||||
<div class="vision-hint-text">Read <span id="vision-hint-title"></span></div>
|
||||
</div>
|
||||
|
||||
<!-- Vision Overlay -->
|
||||
<div id="vision-overlay" class="vision-overlay" style="display:none;">
|
||||
<div class="vision-overlay-content">
|
||||
<div class="vision-overlay-header">
|
||||
<div class="vision-overlay-status" id="vision-status-dot"></div>
|
||||
<div class="vision-overlay-title" id="vision-overlay-title">VISION POINT</div>
|
||||
</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">CLOSE</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Portal Activation Overlay -->
|
||||
<div id="portal-overlay" class="portal-overlay" style="display:none;">
|
||||
<div class="portal-overlay-content">
|
||||
<div class="portal-overlay-header">
|
||||
<div class="portal-overlay-status" id="portal-status-dot"></div>
|
||||
<div class="portal-overlay-title" id="portal-overlay-title">PORTAL ACTIVATED</div>
|
||||
</div>
|
||||
<h2 id="portal-name-display">MORROWIND</h2>
|
||||
<p id="portal-desc-display">The Vvardenfell harness. Ash storms and ancient mysteries.</p>
|
||||
<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">CLOSE</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Portal Atlas Overlay -->
|
||||
<div id="atlas-overlay" class="atlas-overlay" style="display:none;">
|
||||
<div class="atlas-content">
|
||||
<div class="atlas-header">
|
||||
<div class="atlas-title">
|
||||
<span class="atlas-icon">🌐</span>
|
||||
<h2>PORTAL ATLAS</h2>
|
||||
</div>
|
||||
<button id="atlas-close-btn" class="atlas-close-btn">CLOSE</button>
|
||||
</div>
|
||||
<div class="atlas-grid" id="atlas-grid">
|
||||
<!-- Portals will be injected here -->
|
||||
</div>
|
||||
<div class="atlas-footer">
|
||||
<div class="atlas-status-summary">
|
||||
<span class="status-indicator online"></span> <span id="atlas-online-count">0</span> ONLINE
|
||||
|
||||
<span class="status-indicator standby"></span> <span id="atlas-standby-count">0</span> STANDBY
|
||||
</div>
|
||||
<div class="atlas-hint">Click a portal to focus or teleport</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Click to Enter -->
|
||||
<div id="enter-prompt" style="display:none;">
|
||||
<div class="enter-content">
|
||||
<h2>Enter The Nexus</h2>
|
||||
<p>Click anywhere to begin</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="nexus-canvas"></canvas>
|
||||
|
||||
<footer class="nexus-footer">
|
||||
<a href="https://www.perplexity.ai/computer" target="_blank" rel="noopener noreferrer">
|
||||
Created with Perplexity Computer
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="./app.js"></script>
|
||||
|
||||
<!-- Live Refresh: polls Gitea for new commits on main, reloads when SHA changes -->
|
||||
<div id="live-refresh-banner" style="
|
||||
display:none; position:fixed; top:0; left:0; right:0; z-index:9999;
|
||||
background:linear-gradient(90deg,#4af0c0,#7b5cff);
|
||||
color:#050510; font-family:'JetBrains Mono',monospace; font-size:13px;
|
||||
padding:8px 16px; text-align:center; font-weight:600;
|
||||
">⚡ NEW DEPLOYMENT DETECTED — Reloading in <span id="lr-countdown">5</span>s…</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const GITEA = 'http://143.198.27.163:3000/api/v1';
|
||||
const REPO = 'Timmy_Foundation/the-nexus';
|
||||
const BRANCH = 'main';
|
||||
const INTERVAL = 30000; // poll every 30s
|
||||
|
||||
let knownSha = null;
|
||||
|
||||
async function fetchLatestSha() {
|
||||
try {
|
||||
const r = await fetch(`${GITEA}/repos/${REPO}/branches/${BRANCH}`, { cache: 'no-store' });
|
||||
if (!r.ok) return null;
|
||||
const d = await r.json();
|
||||
return d.commit && d.commit.id ? d.commit.id : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
const sha = await fetchLatestSha();
|
||||
if (!sha) return;
|
||||
if (knownSha === null) { knownSha = sha; return; }
|
||||
if (sha !== knownSha) {
|
||||
knownSha = sha;
|
||||
const banner = document.getElementById('live-refresh-banner');
|
||||
const countdown = document.getElementById('lr-countdown');
|
||||
banner.style.display = 'block';
|
||||
let t = 5;
|
||||
const tick = setInterval(() => {
|
||||
t--;
|
||||
countdown.textContent = t;
|
||||
if (t <= 0) { clearInterval(tick); location.reload(); }
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling after page is interactive
|
||||
fetchLatestSha().then(sha => { knownSha = sha; });
|
||||
setInterval(poll, INTERVAL);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
203
server.ts
Normal file
203
server.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import express from 'express';
|
||||
import { createServer as createViteServer } from 'vite';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import 'dotenv/config';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { createServer } from 'http';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Primary (Local) Gitea
|
||||
const GITEA_URL = process.env.GITEA_URL || 'http://localhost:3000/api/v1';
|
||||
const GITEA_TOKEN = process.env.GITEA_TOKEN || '';
|
||||
|
||||
// Backup (Remote) Gitea
|
||||
const REMOTE_GITEA_URL = process.env.REMOTE_GITEA_URL || 'http://143.198.27.163:3000/api/v1';
|
||||
const REMOTE_GITEA_TOKEN = process.env.REMOTE_GITEA_TOKEN || '';
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const PORT = 3000;
|
||||
|
||||
// WebSocket Server for Hermes/Evennia Bridge
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
const clients = new Set<WebSocket>();
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
clients.add(ws);
|
||||
console.log(`Client connected to Nexus Bridge. Total: ${clients.size}`);
|
||||
|
||||
ws.on('close', () => {
|
||||
clients.remove(ws);
|
||||
console.log(`Client disconnected. Total: ${clients.size}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Simulate Evennia Heartbeat (Source of Truth)
|
||||
setInterval(() => {
|
||||
const heartbeat = {
|
||||
type: 'heartbeat',
|
||||
frequency: 0.5 + Math.random() * 0.2, // 0.5Hz to 0.7Hz
|
||||
intensity: 0.8 + Math.random() * 0.4,
|
||||
timestamp: Date.now(),
|
||||
source: 'evonia-layer'
|
||||
};
|
||||
const message = JSON.stringify(heartbeat);
|
||||
clients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(message);
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
// Diagnostic Endpoint for Agent Inspection
|
||||
app.get('/api/diagnostic/inspect', async (req, res) => {
|
||||
console.log('Diagnostic request received');
|
||||
try {
|
||||
const REPO_OWNER = 'google';
|
||||
const REPO_NAME = 'timmy-tower';
|
||||
|
||||
const [stateRes, issuesRes] = await Promise.all([
|
||||
fetch(`${GITEA_URL}/repos/${REPO_OWNER}/${REPO_NAME}/contents/world_state.json`, {
|
||||
headers: { 'Authorization': `token ${GITEA_TOKEN}` }
|
||||
}),
|
||||
fetch(`${GITEA_URL}/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=all`, {
|
||||
headers: { 'Authorization': `token ${GITEA_TOKEN}` }
|
||||
})
|
||||
]);
|
||||
|
||||
let worldState = null;
|
||||
if (stateRes.ok) {
|
||||
const content = await stateRes.json();
|
||||
worldState = JSON.parse(Buffer.from(content.content, 'base64').toString());
|
||||
} else if (stateRes.status !== 404) {
|
||||
console.error(`Failed to fetch world state: ${stateRes.status} ${stateRes.statusText}`);
|
||||
}
|
||||
|
||||
let issues = [];
|
||||
if (issuesRes.ok) {
|
||||
issues = await issuesRes.json();
|
||||
} else {
|
||||
console.error(`Failed to fetch issues: ${issuesRes.status} ${issuesRes.statusText}`);
|
||||
}
|
||||
|
||||
res.json({
|
||||
worldState,
|
||||
issues,
|
||||
repoExists: stateRes.status !== 404,
|
||||
connected: GITEA_TOKEN !== ''
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Diagnostic error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Helper for Gitea Proxy
|
||||
const createGiteaProxy = (baseUrl: string, token: string) => async (req: express.Request, res: express.Response) => {
|
||||
const path = req.params[0] + (req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '');
|
||||
const url = `${baseUrl}/${path}`;
|
||||
|
||||
if (!token) {
|
||||
console.warn(`Gitea Proxy Warning: No token provided for ${baseUrl}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: req.method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `token ${token}`,
|
||||
},
|
||||
body: ['GET', 'HEAD'].includes(req.method) ? undefined : JSON.stringify(req.body),
|
||||
});
|
||||
|
||||
const data = await response.text();
|
||||
res.status(response.status).send(data);
|
||||
} catch (error: any) {
|
||||
console.error(`Gitea Proxy Error (${baseUrl}):`, error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Gitea Proxy - Primary (Local)
|
||||
app.get('/api/gitea/check', async (req, res) => {
|
||||
try {
|
||||
const response = await fetch(`${GITEA_URL}/user`, {
|
||||
headers: { 'Authorization': `token ${GITEA_TOKEN}` }
|
||||
});
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
res.json({ status: 'connected', user: user.username });
|
||||
} else {
|
||||
res.status(response.status).json({ status: 'error', message: `Gitea returned ${response.status}` });
|
||||
}
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ status: 'error', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.all('/api/gitea/*', createGiteaProxy(GITEA_URL, GITEA_TOKEN));
|
||||
|
||||
// Gitea Proxy - Backup (Remote)
|
||||
app.get('/api/gitea-remote/check', async (req, res) => {
|
||||
try {
|
||||
const response = await fetch(`${REMOTE_GITEA_URL}/user`, {
|
||||
headers: { 'Authorization': `token ${REMOTE_GITEA_TOKEN}` }
|
||||
});
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
res.json({ status: 'connected', user: user.username });
|
||||
} else {
|
||||
res.status(response.status).json({ status: 'error', message: `Gitea returned ${response.status}` });
|
||||
}
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ status: 'error', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.all('/api/gitea-remote/*', createGiteaProxy(REMOTE_GITEA_URL, REMOTE_GITEA_TOKEN));
|
||||
|
||||
// WebSocket Upgrade Handler
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
const pathname = new URL(request.url!, `http://${request.headers.host}`).pathname;
|
||||
if (pathname === '/api/world/ws') {
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Health Check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
// Vite middleware for development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: 'spa',
|
||||
});
|
||||
app.use(vite.middlewares);
|
||||
} else {
|
||||
const distPath = path.join(process.cwd(), 'dist');
|
||||
app.use(express.static(distPath));
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(distPath, 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
httpServer.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
Reference in New Issue
Block a user