Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Whitestone
c833d503e2 fix: tighten GOFAI HUD and memory feed for laptop viewports (#704)
Some checks failed
CI / test (pull_request) Failing after 18s
CI / validate (pull_request) Failing after 17s
Review Approval Gate / verify-review (pull_request) Failing after 4s
At 1024px: shrink GOFAI panels (280→220px), reduce padding/gap,
collapse panel content height, and narrow memory feed (320→260px).
At 768px: hide GOFAI HUD and memory feed entirely to free canvas space.

Refs #704
2026-04-13 04:09:18 -04:00
Alexander Whitestone
1b9c1a56f2 fix: closes #704
Some checks failed
CI / test (pull_request) Failing after 19s
CI / validate (pull_request) Failing after 15s
Review Approval Gate / verify-review (pull_request) Failing after 3s
2026-04-13 04:06:38 -04:00
18 changed files with 266 additions and 2652 deletions

View File

@@ -12,14 +12,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Preflight secrets check
env:
H: ${{ secrets.DEPLOY_HOST }}
U: ${{ secrets.DEPLOY_USER }}
K: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
[ -z "$H" ] || [ -z "$U" ] || [ -z "$K" ] && echo "ERROR: Missing deploy secret. Configure DEPLOY_HOST/DEPLOY_USER/DEPLOY_SSH_KEY in Settings → Actions → Secrets (see issue #1363)" && exit 1
- name: Deploy to host via SSH
uses: appleboy/ssh-action@v1.0.3
with:

View File

@@ -13,7 +13,7 @@ jobs:
- name: Verify staging label on merge PR
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.MERGE_TOKEN }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://forge.alexanderwhitestone.com' }}
GITEA_REPO: Timmy_Foundation/the-nexus
run: |

2
app.js
View File

@@ -1192,7 +1192,7 @@ async function fetchGiteaData() {
try {
const [issuesRes, stateRes] = await Promise.all([
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/issues?state=all&limit=20'),
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/contents/vision.json')
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/timmy_Foundation/the-nexus/contents/vision.json')
]);
if (issuesRes.ok) {

49
boot.js
View File

@@ -1,49 +0,0 @@
function setText(node, text) {
if (node) node.textContent = text;
}
function setHtml(node, html) {
if (node) node.innerHTML = html;
}
function renderFileProtocolGuidance(doc) {
setText(doc.querySelector('.loader-subtitle'), 'Serve this world over HTTP to initialize Three.js.');
const bootMessage = doc.getElementById('boot-message');
if (bootMessage) {
bootMessage.style.display = 'block';
setHtml(
bootMessage,
[
'<strong>Three.js modules cannot boot from <code>file://</code>.</strong>',
'Serve the Nexus over HTTP, for example:',
'<code>python3 -m http.server 8888</code>',
].join('<br>')
);
}
}
function injectModuleBootstrap(doc, src = './bootstrap.mjs') {
const script = doc.createElement('script');
script.type = 'module';
script.src = src;
doc.body.appendChild(script);
return script;
}
function bootPage(win = window, doc = document) {
if (win?.location?.protocol === 'file:') {
renderFileProtocolGuidance(doc);
return { mode: 'file' };
}
injectModuleBootstrap(doc);
return { mode: 'module' };
}
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
bootPage(window, document);
}
if (typeof module !== 'undefined') {
module.exports = { bootPage, injectModuleBootstrap, renderFileProtocolGuidance };
}

View File

@@ -1,100 +0,0 @@
const FILE_PROTOCOL_MESSAGE = `
<strong>Three.js modules cannot boot from <code>file://</code>.</strong><br>
Serve the Nexus over HTTP, for example:<br>
<code>python3 -m http.server 8888</code>
`;
function setText(node, text) {
if (node) node.textContent = text;
}
function setHtml(node, html) {
if (node) node.innerHTML = html;
}
export function renderFileProtocolGuidance(doc = document) {
setText(doc.querySelector('.loader-subtitle'), 'Serve this world over HTTP to initialize Three.js.');
const bootMessage = doc.getElementById('boot-message');
if (bootMessage) {
bootMessage.style.display = 'block';
setHtml(bootMessage, FILE_PROTOCOL_MESSAGE.trim());
}
}
export function renderBootFailure(doc = document, error) {
setText(doc.querySelector('.loader-subtitle'), 'Nexus boot failed. Check console logs.');
const bootMessage = doc.getElementById('boot-message');
if (bootMessage) {
bootMessage.style.display = 'block';
setHtml(bootMessage, `<strong>Boot error:</strong> ${error?.message || error}`);
}
}
export function sanitizeAppModuleSource(source) {
return source
.replace(/;\\n(\s*)/g, ';\n$1')
.replace(/import\s*\{[\s\S]*?\}\s*from '\.\/nexus\/symbolic-engine\.js';\n?/, '')
.replace(
/\n \}\n \} else if \(data\.type && data\.type\.startsWith\('evennia\.'\)\) \{\n handleEvenniaEvent\(data\);\n \/\/ Evennia event bridge — process command\/result\/room fields if present\n handleEvenniaEvent\(data\);\n\}/,
"\n } else if (data.type && data.type.startsWith('evennia.')) {\n handleEvenniaEvent(data);\n }\n}"
)
.replace(
/\/\*\*[\s\S]*?Called from handleHermesMessage for any message carrying evennia metadata\.\n \*\/\nfunction handleEvenniaEvent\(data\) \{[\s\S]*?\n\}\n\n\n\/\/ ═══════════════════════════════════════════/,
"// ═══════════════════════════════════════════"
)
.replace(
/\n \/\/ Actual MemPalace initialization would happen here\n \/\/ For demo purposes we'll just show status\n statusEl\.textContent = 'Connected to local MemPalace';\n statusEl\.style\.color = '#4af0c0';\n \n \/\/ Simulate mining process\n mineMemPalaceContent\("Initial knowledge base setup complete"\);\n \} catch \(err\) \{\n console\.error\('Failed to initialize MemPalace:', err\);\n document\.getElementById\('mem-palace-status'\)\.textContent = 'MemPalace ERROR';\n document\.getElementById\('mem-palace-status'\)\.style\.color = '#ff4466';\n \}\n try \{/,
"\n try {"
)
.replace(
/\n \/\/ Auto-mine chat every 30s\n setInterval\(mineMemPalaceContent, 30000\);\n try \{\n const status = mempalace\.status\(\);\n document\.getElementById\('compression-ratio'\)\.textContent = status\.compression_ratio\.toFixed\(1\) \+ 'x';\n document\.getElementById\('docs-mined'\)\.textContent = status\.total_docs;\n document\.getElementById\('aaak-size'\)\.textContent = status\.aaak_size \+ 'B';\n \} catch \(error\) \{\n console\.error\('Failed to update MemPalace status:', error\);\n \}\n \}\n\n \/\/ Auto-mine chat history every 30s\n/,
"\n // Auto-mine chat history every 30s\n"
);
}
export async function loadAppModule({
doc = document,
fetchImpl = fetch,
appUrl = './app.js',
} = {}) {
const response = await fetchImpl(appUrl, { cache: 'no-store' });
if (!response.ok) {
throw new Error(`Failed to load ${appUrl}: ${response.status}`);
}
const source = sanitizeAppModuleSource(await response.text());
const script = doc.createElement('script');
script.type = 'module';
script.textContent = source;
return await new Promise((resolve, reject) => {
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Failed to execute ${appUrl}`));
doc.body.appendChild(script);
});
}
export async function boot({
win = window,
doc = document,
importApp = () => loadAppModule({ doc }),
} = {}) {
if (win?.location?.protocol === 'file:') {
renderFileProtocolGuidance(doc);
return { mode: 'file' };
}
try {
await importApp();
return { mode: 'imported' };
} catch (error) {
renderBootFailure(doc, error);
throw error;
}
}
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
boot().catch((error) => {
console.error('Nexus boot failed:', error);
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -60,7 +60,6 @@
</div>
<h1 class="loader-title">THE NEXUS</h1>
<p class="loader-subtitle">Initializing Sovereign Space...</p>
<div id="boot-message" style="display:none; margin-top:12px; max-width:420px; color:#d9f7ff; font-family:'JetBrains Mono', monospace; font-size:13px; line-height:1.6; text-align:center;"></div>
<div class="loader-bar"><div class="loader-fill" id="load-progress"></div></div>
</div>
</div>
@@ -357,34 +356,253 @@
<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>
<a href="POLICY.md" target="_blank" rel="noopener noreferrer">View Contribution Policy</a>
<a href="https://www.perplexity.ai/computer" target="_blank" rel="noopener noreferrer">
Created with Perplexity Computer
</a>
<a href="POLICY.md" target="_blank" rel="noopener noreferrer">
View Contribution Policy
</a>
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
<strong>BRANCH PROTECTION POLICY</strong><br>
<ul style="margin:0; padding-left:15px;">
<li>• Require PR for merge ✅</li>
<li>• Require 1 approval ✅</li>
<li>• Dismiss stale approvals ✅</li>
<li>• Require CI ✅ (where available)</li>
<li>• Block force push ✅</li>
<li>• Block branch deletion ✅</li>
<li>• Weekly audit for unreviewed merges ✅</li>
</ul>
<div style="margin-top: 8px;">
<strong>DEFAULT REVIEWERS</strong><br>
<span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos) |
<span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)
</div>
<div style="margin-top: 10px;">
<strong>IMPLEMENTATION STATUS</strong><br>
<ul style="margin:0; padding-left:15px;">
<li>• hermes-agent: Require PR + 1 approval + CI ✅</li>
<li>• the-nexus: Require PR + 1 approval ⚠️ (CI disabled)</li>
<li>• timmy-home: Require PR + 1 approval ✅</li>
<li>• timmy-config: Require PR + 1 approval ✅</li>
</ul>
</div>
</div>
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
<strong>BRANCH PROTECTION POLICY</strong><br>
<ul style="margin:0; padding-left:15px;">
<li>• Require PR for merge ✅</li>
<li>• Require 1 approval ✅</li>
<li>• Dismiss stale approvals ✅</li>
<li>• Require CI ✅ (where available)</li>
<li>• Block force push ✅</li>
<li>• Block branch deletion ✅</li>
<li>• Weekly audit for unreviewed merges ✅</li>
</ul>
</div>
<div id="mem-palace-container" class="mem-palace-ui">
<div class="mem-palace-header">
<span id="mem-palace-status">MEMPALACE</span>
<button onclick="mineMemPalaceContent()" class="mem-palace-btn">Mine Chat</button>
</div>
<div class="mem-palace-stats">
<div>Compression: <span id="compression-ratio">--</span>x</div>
<div>Docs mined: <span id="docs-mined">0</span></div>
<div>AAAK size: <span id="aaak-size">0B</span></div>
</div>
<div class="mem-palace-logs" id="mem-palace-logs"></div>
</div>
<div class="default-reviewers" style="margin-top: 8px; font-size: 12px; color: #aaa;">
<strong>DEFAULT REVIEWERS</strong><br>
<ul style="margin:0; padding-left:15px;">
<li><span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos)</li>
<li><span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)</li>
</ul>
</div>
<div class="implementation-status" style="margin-top: 10px; font-size: 12px; color: #aaa;">
<strong>IMPLEMENTATION STATUS</strong><br>
<div style="margin-top: 5px; display: flex; flex-direction: column; gap: 2px;">
<div><span style="color:#4af0c0;">hermes-agent</span>: Require PR + 1 approval + CI ✅</div>
<div><span style="color:#7b5cff;">the-nexus</span>: Require PR + 1 approval ⚠️ (CI disabled)</div>
</div>
</div>
<div id="mem-palace-status" style="position:fixed; right:24px; top:64px; background:rgba(74,240,192,0.1); color:#4af0c0; padding:6px 12px; border-radius:4px; font-family:'Orbitron', sans-serif; font-size:10px; letter-spacing:0.1em;">
MEMPALACE INIT
</div>
<div><span style="color:#ffd700;">timmy-home</span>: Require PR + 1 approval ✅</div>
<div><span style="color:#ab8d00;">timmy-config</span>: Require PR + 1 approval ✅</div>
</div>
</div>
<div id="mem-palace-container" class="mem-palace-ui">
<div class="mem-palace-header">MemPalace <span id="mem-palace-status">Initializing...</span></div>
<div class="mem-palace-stats">
<div>Compression: <span id="compression-ratio">--</span>x</div>
<div>Docs mined: <span id="docs-mined">0</span></div>
<div>AAAK size: <span id="aaak-size">0B</span></div>
</div>
<div class="mem-palace-actions">
<button id="mine-now-btn" class="mem-palace-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
<button class="mem-palace-btn" onclick="searchMemPalace()">Search</button>
</div>
<div id="mem-palace-logs" class="mem-palace-logs"></div>
</div>
<div id="mem-palace-controls" style="position:fixed; right:24px; top:54px; background:rgba(74,240,192,0.05); padding:4px 8px; font-family:'JetBrains Mono',monospace; font-size:11px; border-left:2px solid #4af0c0;">
<button onclick="mineMemPalace()">Mine Chat</button>
<button onclick="searchMemPalace()">Search</button>
</div>
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
<div id="mem-palace-controls" style="position:fixed; right:24px; top:54px; background:rgba(74,240,192,0.05); padding:4px 8px; font-family:'JetBrains Mono',monospace; font-size:10px; border-left:2px solid #4af0c0;">
<button class="mem-palace-mining-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
<button onclick="searchMemPalace()">Search</button>
</div>
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
```
index.html
```html
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
<strong>BRANCH PROTECTION POLICY</strong><br>
<ul style="margin:0; padding-left:15px;">
<li>• Require PR for merge ✅</li>
<li>• Require 1 approval ✅</li>
<li>• Dismiss stale approvals ✅</li>
<li>• Require CI ✅ (where available)</li>
<li>• Block force push ✅</li>
<li>• Block branch deletion ✅</li>
</ul>
</div>
<div class="default-reviewers" style="margin-top: 8px;">
<strong>DEFAULT REVIEWERS</strong><br>
<ul style="margin:0; padding-left:15px;">
<li><span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos)</li>
<li><span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)</li>
</ul>
</div>
<div class="implementation-status" style="margin-top: 10px;">
<strong>IMPLEMENTATION STATUS</strong><br>
<div style="margin-top: 5px; display: flex; flex-direction: column; gap: 2px;">
<div><span style="color:#4af0c0;">hermes-agent</span>: Require PR + 1 approval + CI ✅</div>
<div><span style="color:#7b5cff;">the-nexus</span>: Require PR + 1 approval ⚠<> (CI disabled)</div>
<div><span style="color:#ffd700;">timmy-home</span>: Require PR + 1 approval ✅</div>
<div><span style="color:#ab8d00;">timmy-config</span>: Require PR + 1 approval ✅</div>
</div>
</div>
</footer>
<div id="mem-palace-container" class="mem-palace-ui">
<div class="mem-palace-header">MemPalace <span id="mem-palace-status">Initializing...</span></div>
<div class="mem-palace-stats">
<div>Compression: <span id="compression-ratio">--</span>x</div>
<div>Docs mined: <span id="docs-mined">0</span></div>
<div>AAAK size: <span id="aaak-size">0B</span></div>
</div>
<div class="mem-palace-actions">
<button id="mine-now-btn" class="mem-palace-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
<button class="mem-palace-btn" onclick="searchMemPalace()">Search</button>
</div>
<div id="mem-palace-logs" class="mem-palace-logs"></div>
</div>
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
<div id="archive-health-dashboard" class="archive-health-dashboard" style="display:none;" aria-label="Archive Health Dashboard"><div class="archive-health-header"><span class="archive-health-title">◈ ARCHIVE HEALTH</span><button class="archive-health-close" onclick="toggleArchiveHealthDashboard()" aria-label="Close dashboard"></button></div><div id="archive-health-content" class="archive-health-content"></div></div>
<div id="memory-feed" class="memory-feed" style="display:none;"><div class="memory-feed-header"><span class="memory-feed-title">✨ Memory Feed</span><div class="memory-feed-actions"><button class="memory-feed-clear" onclick="clearMemoryFeed()">Clear</button><button class="memory-feed-toggle" onclick="document.getElementById('memory-feed').style.display='none'"></button></div></div><div id="memory-feed-list" class="memory-feed-list"></div></div>
<div id="memory-filter" class="memory-filter" style="display:none;"><div class="filter-header"><span class="filter-title">⬡ Memory Filter</span><button class="filter-close" onclick="closeMemoryFilter()"></button></div><div class="filter-controls"><button class="filter-btn" onclick="setAllFilters(true)">Show All</button><button class="filter-btn" onclick="setAllFilters(false)">Hide All</button></div><div class="filter-list" id="filter-list"></div></div>
<div id="memory-inspect-panel" class="memory-inspect-panel" style="display:none;" aria-label="Memory Inspect Panel"></div>
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
<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 src="./boot.js"></script>
<script>
function openMemoryFilter() { renderFilterList(); document.getElementById('memory-filter').style.display = 'flex'; }
function closeMemoryFilter() { document.getElementById('memory-filter').style.display = 'none'; }
(function() {
const GITEA = 'https://forge.alexanderwhitestone.com/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) {
// Check branch protection rules
const branchRules = await fetch(`${GITEA}/repos/${REPO}/branches/${BRANCH}/protection`);
if (!branchRules.ok) {
console.error('Branch protection rules not enforced');
return;
}
const rules = await branchRules.json();
if (!rules.require_pr && !rules.require_approvals) {
console.error('Branch protection rules not met');
return;
}
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>
<!-- Archive Health Dashboard (Mnemosyne, issue #1210) -->
<div id="archive-health-dashboard" class="archive-health-dashboard" style="display:none;" aria-label="Archive Health Dashboard">
<div class="archive-health-header">
<span class="archive-health-title">◈ ARCHIVE HEALTH</span>
<button class="archive-health-close" onclick="toggleArchiveHealthDashboard()" aria-label="Close dashboard"></button>
</div>
<div id="archive-health-content" class="archive-health-content"></div>
</div>
<!-- Memory Activity Feed (Mnemosyne) -->
<div id="memory-feed" class="memory-feed" style="display:none;">
<div class="memory-feed-header">
<span class="memory-feed-title">✨ Memory Feed</span>
<div class="memory-feed-actions"><button class="memory-feed-clear" onclick="clearMemoryFeed()">Clear</button><button class="memory-feed-toggle" onclick="document.getElementById('memory-feed').style.display='none'"></button></div>
</div>
<div id="memory-feed-list" class="memory-feed-list"></div>
<!-- ═══ MNEMOSYNE MEMORY FILTER ═══ -->
<div id="memory-filter" class="memory-filter" style="display:none;">
<div class="filter-header">
<span class="filter-title">⬡ Memory Filter</span>
<button class="filter-close" onclick="closeMemoryFilter()"></button>
</div>
<div class="filter-controls">
<button class="filter-btn" onclick="setAllFilters(true)">Show All</button>
<button class="filter-btn" onclick="setAllFilters(false)">Hide All</button>
</div>
<div class="filter-list" id="filter-list"></div>
</div>
</div>
<!-- Memory Inspect Panel (Mnemosyne, issue #1227) -->
<div id="memory-inspect-panel" class="memory-inspect-panel" style="display:none;" aria-label="Memory Inspect Panel">
</div>
<!-- Memory Connections Panel (Mnemosyne) -->
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel">
</div>
<script>
// ─── MNEMOSYNE: Memory Filter Panel ───────────────────
function openMemoryFilter() {
renderFilterList();
document.getElementById('memory-filter').style.display = 'flex';
}
function closeMemoryFilter() {
document.getElementById('memory-filter').style.display = 'none';
}
function renderFilterList() {
const counts = SpatialMemory.getMemoryCountByRegion();
const regions = SpatialMemory.REGIONS;
@@ -396,12 +614,30 @@ function renderFilterList() {
const colorHex = '#' + region.color.toString(16).padStart(6, '0');
const item = document.createElement('div');
item.className = 'filter-item';
item.innerHTML = `<div class="filter-item-left"><span class="filter-dot" style="background:${colorHex}"></span><span class="filter-label">${region.glyph} ${region.label}</span></div><div class="filter-item-right"><span class="filter-count">${count}</span><label class="filter-toggle"><input type="checkbox" ${visible ? 'checked' : ''} onchange="toggleRegion('${key}', this.checked)"><span class="filter-slider"></span></label></div>`;
item.innerHTML = `
<div class="filter-item-left">
<span class="filter-dot" style="background:${colorHex}"></span>
<span class="filter-label">${region.glyph} ${region.label}</span>
</div>
<div class="filter-item-right">
<span class="filter-count">${count}</span>
<label class="filter-toggle">
<input type="checkbox" ${visible ? 'checked' : ''}
onchange="toggleRegion('${key}', this.checked)">
<span class="filter-slider"></span>
</label>
</div>
`;
list.appendChild(item);
}
}
function toggleRegion(category, visible) { SpatialMemory.setRegionVisibility(category, visible); }
function setAllFilters(visible) { SpatialMemory.setAllRegionsVisible(visible); renderFilterList(); }
function toggleRegion(category, visible) {
SpatialMemory.setRegionVisibility(category, visible);
}
function setAllFilters(visible) {
SpatialMemory.setAllRegionsVisible(visible);
renderFilterList();
}
</script>
</body>
</html>

View File

@@ -501,7 +501,6 @@ class QuestManager:
self._quests: dict[str, Quest] = {}
self._counter = 0
self._lock = threading.Lock()
self.load()
def create(self, name: str, description: str,
objectives: list[str], rewards: list[str]) -> Quest:
@@ -655,7 +654,6 @@ class InventoryManager:
# room -> list of {name, description, dropped_by, dropped_at}
self._room_items: dict[str, list[dict]] = {}
self._lock = threading.Lock()
self.load()
def take_item(self, user_id: str, username: str, room: str, item_name: str) -> dict:
"""Pick up an item from a room into user inventory."""
@@ -842,7 +840,6 @@ class GuildManager:
self._membership: dict[str, str] = {}
self._counter = 0
self._lock = threading.Lock()
self.load()
def create(self, name: str, leader_id: str, leader_name: str) -> dict:
"""Create a new guild. Returns guild dict or error."""
@@ -1076,7 +1073,6 @@ class CombatManager:
self._encounters: dict[str, CombatEncounter] = {} # user_id -> encounter
self._counter = 0
self._lock = threading.Lock()
self.load()
# ── NPC management ──────────────────────────────────────────────
@@ -1413,7 +1409,6 @@ class MagicManager:
self._spellbooks: dict[str, SpellBook] = {} # user_id -> SpellBook
self._counter = 0
self._lock = threading.Lock()
self.load()
# ── Spell registry ───────────────────────────────────────────────

View File

@@ -1,80 +0,0 @@
# Sovereign Sound Playground
Interactive audio-visual experience — no servers, no dependencies, pure browser.
## Apps
### Playground v3 (Full Instrument)
`playground.html` — The complete experience:
- Visual piano keyboard (2 octaves)
- 6 visualization modes: Waveform, Particles, Bars, Spiral, Gravity Well, Strobe
- 5 color palettes: Cosmic, Sunset, Ocean, Forest, Neon
- Ambient beat with chord progressions
- Mouse/touch playback on visualizer
- Chord detection
- Recording to WAV
- Export as PNG
### Synesthesia
`synesthesia.html` — Paint with sound:
- Click and drag to create colors and shapes
- Each position generates a unique tone
- Particles respond to your movement
- Touch supported
### Ambient
`ambient.html` — Evolving soundscape:
- Automatic chord progressions
- Floating orbs respond to audio
- Reverb-drenched textures
- Click to enter, let it wash over you
### Interactive
`interactive.html` — 26 key-shape mappings:
- Press A-Z to play notes
- Each key has a unique shape and color
- Shapes animate and fade
- Visual keyboard at bottom
- Touch supported
### Visualizer
`visualizer.html` — WAV frequency visualization:
- Load any audio file
- 4 modes: Spectrum, Waveform, Spectrogram, Circular
- Drag and drop support
- Real-time frequency analysis
## Features
- Zero dependencies — just open in a browser
- Local-first — no network requests
- Touch support on all apps
- Keyboard support
- Recording and export
- Multiple visualization modes
- Color palettes
## Usage
Open any HTML file in a browser. That's it.
```bash
# Quick start
open playground/playground.html
# Or serve locally
python3 -m http.server 8080 --directory playground
```
## Keyboard Shortcuts (Playground v3)
- A-; (lower row): Play piano notes
- Mouse drag on visualizer: Create sound
- Click piano keys: Play notes
## Technical
- Web Audio API for sound generation
- Canvas 2D for visualization
- MediaRecorder for recording
- No build step, no framework

View File

@@ -1,243 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ambient — Evolving Soundscape</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0f; overflow: hidden; height: 100vh; }
canvas { display: block; width: 100%; height: 100%; }
.overlay {
position: fixed; inset: 0; display: flex; align-items: center;
justify-content: center; background: rgba(0,0,0,0.7);
transition: opacity 0.5s; z-index: 10;
}
.overlay.hidden { opacity: 0; pointer-events: none; }
.start-btn {
background: rgba(100,50,150,0.5); border: 2px solid rgba(150,100,200,0.7);
color: #e0e0e0; padding: 20px 40px; font-size: 18px; border-radius: 30px;
cursor: pointer; font-family: sans-serif; letter-spacing: 2px;
transition: all 0.3s;
}
.start-btn:hover { background: rgba(150,100,200,0.5); transform: scale(1.05); }
.title {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.1); font-size: 24px; font-family: sans-serif;
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
}
</style>
</head>
<body>
<div class="title">Ambient</div>
<canvas id="canvas"></canvas>
<div class="overlay" id="overlay">
<button class="start-btn" id="start-btn">Enter Soundscape</button>
</div>
<script>
class AmbientSoundscape {
constructor() {
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.audioCtx = null;
this.masterGain = null;
this.analyser = null;
this.isPlaying = false;
this.currentChord = 0;
this.time = 0;
this.orbs = [];
this.resize();
window.addEventListener('resize', () => this.resize());
// Create orbs
for (let i = 0; i < 15; i++) {
this.orbs.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
radius: Math.random() * 50 + 20,
speed: Math.random() * 0.5 + 0.2,
angle: Math.random() * Math.PI * 2,
color: [
[167, 139, 250], [129, 140, 248], [99, 102, 241],
[139, 92, 246], [124, 58, 237]
][i % 5]
});
}
this.bindEvents();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
async initAudio() {
if (this.audioCtx) return;
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.audioCtx.createGain();
this.masterGain.gain.value = 0.2;
this.analyser = this.audioCtx.createAnalyser();
this.analyser.fftSize = 256;
this.analyser.smoothingTimeConstant = 0.95;
this.masterGain.connect(this.analyser);
this.analyser.connect(this.audioCtx.destination);
// Create reverb
const convolver = this.audioCtx.createConvolver();
const reverbTime = 3;
const sampleRate = this.audioCtx.sampleRate;
const length = sampleRate * reverbTime;
const impulse = this.audioCtx.createBuffer(2, length, sampleRate);
for (let channel = 0; channel < 2; channel++) {
const channelData = impulse.getChannelData(channel);
for (let i = 0; i < length; i++) {
channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2);
}
}
convolver.buffer = impulse;
convolver.connect(this.masterGain);
this.reverb = convolver;
}
playChord() {
if (!this.audioCtx || !this.isPlaying) return;
const progressions = [
[[261.63, 329.63, 392.00], [392.00, 493.88, 587.33],
[440.00, 523.25, 659.25], [349.23, 440.00, 523.25]],
[[220.00, 277.18, 329.63], [329.63, 415.30, 493.88],
[369.99, 466.16, 554.37], [293.66, 369.99, 440.00]]
];
const prog = progressions[this.currentChord % 2];
const chord = prog[Math.floor(this.time / 4) % prog.length];
chord.forEach((freq, i) => {
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
const filter = this.audioCtx.createBiquadFilter();
osc.type = 'sine';
osc.frequency.value = freq * (1 + (Math.random() - 0.5) * 0.01);
filter.type = 'lowpass';
filter.frequency.value = 800 + Math.sin(this.time * 0.1) * 400;
gain.gain.setValueAtTime(0, this.audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.15, this.audioCtx.currentTime + 0.5);
gain.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 4);
osc.connect(filter);
filter.connect(gain);
gain.connect(this.reverb);
osc.start();
osc.stop(this.audioCtx.currentTime + 4);
});
// High texture
const highOsc = this.audioCtx.createOscillator();
const highGain = this.audioCtx.createGain();
highOsc.type = 'sine';
highOsc.frequency.value = chord[0] * 4 + Math.random() * 50;
highGain.gain.setValueAtTime(0, this.audioCtx.currentTime);
highGain.gain.linearRampToValueAtTime(0.03, this.audioCtx.currentTime + 1);
highGain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 5);
highOsc.connect(highGain);
highGain.connect(this.reverb);
highOsc.start();
highOsc.stop(this.audioCtx.currentTime + 5);
this.time += 4;
}
start() {
this.isPlaying = true;
document.getElementById('overlay').classList.add('hidden');
const loop = () => {
if (!this.isPlaying) return;
this.playChord();
setTimeout(loop, 4000);
};
loop();
}
stop() {
this.isPlaying = false;
}
animate() {
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.03)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const freqData = this.analyser ? new Uint8Array(this.analyser.frequencyBinCount) : [];
if (this.analyser) this.analyser.getByteFrequencyData(freqData);
const energy = freqData.length > 0 ? freqData[10] / 255 : 0;
// Move and draw orbs
this.orbs.forEach((orb, i) => {
orb.angle += orb.speed * 0.01;
orb.x += Math.cos(orb.angle) * (1 + energy * 3);
orb.y += Math.sin(orb.angle * 0.7) * (1 + energy * 3);
// Wrap around
if (orb.x < -orb.radius) orb.x = this.canvas.width + orb.radius;
if (orb.x > this.canvas.width + orb.radius) orb.x = -orb.radius;
if (orb.y < -orb.radius) orb.y = this.canvas.height + orb.radius;
if (orb.y > this.canvas.height + orb.radius) orb.y = -orb.radius;
// Draw glow
const gradient = this.ctx.createRadialGradient(
orb.x, orb.y, 0,
orb.x, orb.y, orb.radius * (1 + energy * 0.5)
);
gradient.addColorStop(0, `rgba(${orb.color.join(',')}, 0.3)`);
gradient.addColorStop(1, 'rgba(0,0,0,0)');
this.ctx.beginPath();
this.ctx.arc(orb.x, orb.y, orb.radius * (1 + energy * 0.5), 0, Math.PI * 2);
this.ctx.fillStyle = gradient;
this.ctx.fill();
// Inner glow
this.ctx.beginPath();
this.ctx.arc(orb.x, orb.y, orb.radius * 0.3, 0, Math.PI * 2);
this.ctx.fillStyle = `rgba(${orb.color.join(',')}, 0.5)`;
this.ctx.fill();
});
requestAnimationFrame(() => this.animate());
}
bindEvents() {
document.getElementById('start-btn').addEventListener('click', async () => {
await this.initAudio();
this.start();
});
document.addEventListener('click', async () => {
if (!this.audioCtx) {
await this.initAudio();
}
}, { once: true });
}
}
new AmbientSoundscape();
</script>
</body>
</html>

View File

@@ -1,294 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive — 26 Key Shapes</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0f; overflow: hidden; height: 100vh; }
canvas { display: block; width: 100%; height: 100%; }
.title {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.1); font-size: 24px; font-family: sans-serif;
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
}
.key-hint {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.3); font-size: 14px; font-family: monospace;
pointer-events: none;
}
</style>
</head>
<body>
<div class="title">Interactive</div>
<div class="key-hint">Press A-Z to play</div>
<canvas id="canvas"></canvas>
<script>
class Interactive {
constructor() {
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.audioCtx = null;
this.analyser = null;
this.shapes = [];
this.activeKeys = new Set();
this.resize();
window.addEventListener('resize', () => this.resize());
// 26 key mappings with unique shapes and frequencies
this.keyMap = {
'a': { freq: 261.63, shape: 'circle', color: [255,100,100] },
'b': { freq: 277.18, shape: 'square', color: [255,150,100] },
'c': { freq: 293.66, shape: 'triangle', color: [255,200,100] },
'd': { freq: 311.13, shape: 'diamond', color: [255,255,100] },
'e': { freq: 329.63, shape: 'star', color: [200,255,100] },
'f': { freq: 349.23, shape: 'hexagon', color: [150,255,100] },
'g': { freq: 369.99, shape: 'cross', color: [100,255,100] },
'h': { freq: 392.00, shape: 'circle', color: [100,255,150] },
'i': { freq: 415.30, shape: 'square', color: [100,255,200] },
'j': { freq: 440.00, shape: 'triangle', color: [100,255,255] },
'k': { freq: 466.16, shape: 'diamond', color: [100,200,255] },
'l': { freq: 493.88, shape: 'star', color: [100,150,255] },
'm': { freq: 523.25, shape: 'hexagon', color: [100,100,255] },
'n': { freq: 554.37, shape: 'cross', color: [150,100,255] },
'o': { freq: 587.33, shape: 'circle', color: [200,100,255] },
'p': { freq: 622.25, shape: 'square', color: [255,100,255] },
'q': { freq: 659.25, shape: 'triangle', color: [255,100,200] },
'r': { freq: 698.46, shape: 'diamond', color: [255,100,150] },
's': { freq: 739.99, shape: 'star', color: [255,120,120] },
't': { freq: 783.99, shape: 'hexagon', color: [255,170,120] },
'u': { freq: 830.61, shape: 'cross', color: [255,220,120] },
'v': { freq: 880.00, shape: 'circle', color: [220,255,120] },
'w': { freq: 932.33, shape: 'square', color: [170,255,120] },
'x': { freq: 987.77, shape: 'triangle', color: [120,255,120] },
'y': { freq: 1046.50, shape: 'diamond', color: [120,255,170] },
'z': { freq: 1108.73, shape: 'star', color: [120,255,220] }
};
this.bindEvents();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
async initAudio() {
if (this.audioCtx) return;
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.audioCtx.createAnalyser();
this.analyser.fftSize = 256;
this.analyser.connect(this.audioCtx.destination);
}
playKey(key) {
if (!this.audioCtx || !this.keyMap[key]) return;
const { freq, shape, color } = this.keyMap[key];
// Play sound
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.3, this.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 1);
osc.connect(gain);
gain.connect(this.analyser);
osc.start();
osc.stop(this.audioCtx.currentTime + 1);
// Create shape
const x = Math.random() * (this.canvas.width - 200) + 100;
const y = Math.random() * (this.canvas.height - 200) + 100;
this.shapes.push({
key, x, y, shape, color,
size: 50,
maxSize: 150,
life: 1,
rotation: 0
});
}
drawShape(shape) {
const { x, y, size, rotation, shape: shapeType, color, life } = shape;
this.ctx.save();
this.ctx.translate(x, y);
this.ctx.rotate(rotation);
this.ctx.globalAlpha = life;
this.ctx.strokeStyle = `rgb(${color.join(',')})`;
this.ctx.lineWidth = 3;
this.ctx.fillStyle = `rgba(${color.join(',')}, 0.2)`;
this.ctx.beginPath();
switch (shapeType) {
case 'circle':
this.ctx.arc(0, 0, size, 0, Math.PI * 2);
break;
case 'square':
this.ctx.rect(-size, -size, size * 2, size * 2);
break;
case 'triangle':
this.ctx.moveTo(0, -size);
this.ctx.lineTo(size * 0.866, size * 0.5);
this.ctx.lineTo(-size * 0.866, size * 0.5);
this.ctx.closePath();
break;
case 'diamond':
this.ctx.moveTo(0, -size);
this.ctx.lineTo(size * 0.7, 0);
this.ctx.lineTo(0, size);
this.ctx.lineTo(-size * 0.7, 0);
this.ctx.closePath();
break;
case 'star':
for (let i = 0; i < 5; i++) {
const angle = (i * 4 * Math.PI) / 5 - Math.PI / 2;
const method = i === 0 ? 'moveTo' : 'lineTo';
this.ctx[method](Math.cos(angle) * size, Math.sin(angle) * size);
}
this.ctx.closePath();
break;
case 'hexagon':
for (let i = 0; i < 6; i++) {
const angle = (i * Math.PI) / 3;
const method = i === 0 ? 'moveTo' : 'lineTo';
this.ctx[method](Math.cos(angle) * size, Math.sin(angle) * size);
}
this.ctx.closePath();
break;
case 'cross':
const w = size * 0.3;
this.ctx.moveTo(-w, -size);
this.ctx.lineTo(w, -size);
this.ctx.lineTo(w, -w);
this.ctx.lineTo(size, -w);
this.ctx.lineTo(size, w);
this.ctx.lineTo(w, w);
this.ctx.lineTo(w, size);
this.ctx.lineTo(-w, size);
this.ctx.lineTo(-w, w);
this.ctx.lineTo(-size, w);
this.ctx.lineTo(-size, -w);
this.ctx.lineTo(-w, -w);
this.ctx.closePath();
break;
}
this.ctx.fill();
this.ctx.stroke();
// Draw key label
this.ctx.fillStyle = `rgba(${color.join(',')}, ${life})`;
this.ctx.font = `${size * 0.4}px monospace`;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(shape.key.toUpperCase(), 0, 0);
this.ctx.restore();
}
animate() {
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.1)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Update and draw shapes
this.shapes = this.shapes.filter(shape => {
shape.size += (shape.maxSize - shape.size) * 0.05;
shape.rotation += 0.02;
shape.life -= 0.01;
if (shape.life <= 0) return false;
this.drawShape(shape);
return true;
});
// Draw active key indicators
const keyWidth = this.canvas.width / 13;
const keyHeight = 40;
const startY = this.canvas.height - 60;
let col = 0;
for (const key of 'abcdefghijklmnopqrstuvwxyz') {
const x = col * keyWidth + keyWidth / 2;
const isActive = this.activeKeys.has(key);
this.ctx.fillStyle = isActive
? `rgba(${this.keyMap[key].color.join(',')}, 0.5)`
: 'rgba(255,255,255,0.05)';
this.ctx.beginPath();
this.ctx.roundRect(x - 15, startY, 30, keyHeight, 5);
this.ctx.fill();
this.ctx.fillStyle = isActive ? '#fff' : 'rgba(255,255,255,0.3)';
this.ctx.font = '12px monospace';
this.ctx.textAlign = 'center';
this.ctx.fillText(key.toUpperCase(), x, startY + 25);
col++;
}
requestAnimationFrame(() => this.animate());
}
bindEvents() {
document.addEventListener('keydown', async (e) => {
if (e.repeat) return;
const key = e.key.toLowerCase();
if (this.keyMap[key] && !this.activeKeys.has(key)) {
await this.initAudio();
this.activeKeys.add(key);
this.playKey(key);
}
});
document.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
this.activeKeys.delete(key);
});
// Touch support
this.canvas.addEventListener('touchstart', async (e) => {
e.preventDefault();
await this.initAudio();
// Map touch area to key
for (const touch of e.changedTouches) {
const keyIndex = Math.floor(touch.clientX / (this.canvas.width / 26));
const key = 'abcdefghijklmnopqrstuvwxyz'[keyIndex];
if (key) {
this.activeKeys.add(key);
this.playKey(key);
}
}
});
this.canvas.addEventListener('touchend', (e) => {
e.preventDefault();
this.activeKeys.clear();
});
}
}
new Interactive();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,198 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Synesthesia — Paint with Sound</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0f; overflow: hidden; height: 100vh; cursor: crosshair; }
canvas { display: block; width: 100%; height: 100%; }
.title {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.15); font-size: 24px; font-family: sans-serif;
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
}
</style>
</head>
<body>
<div class="title">Synesthesia</div>
<canvas id="canvas"></canvas>
<script>
class Synesthesia {
constructor() {
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.audioCtx = null;
this.analyser = null;
this.particles = [];
this.trails = [];
this.mouse = { x: 0, y: 0, down: false };
this.lastNote = null;
this.colors = [
[255, 100, 150], [100, 200, 255], [150, 255, 150],
[255, 200, 100], [200, 150, 255]
];
this.resize();
window.addEventListener('resize', () => this.resize());
this.bindEvents();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
async initAudio() {
if (this.audioCtx) return;
this.audioCtx = new AudioContext();
this.analyser = this.audioCtx.createAnalyser();
this.analyser.fftSize = 256;
this.analyser.connect(this.audioCtx.destination);
}
playTone(x, y) {
if (!this.audioCtx) return;
const freq = 200 + (x / this.canvas.width) * 600;
const noteId = Math.round(freq / 20);
if (noteId === this.lastNote) return;
this.lastNote = noteId;
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.2, this.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 0.5);
osc.connect(gain);
gain.connect(this.analyser);
osc.start();
osc.stop(this.audioCtx.currentTime + 0.5);
// Spawn particles
const color = this.colors[Math.floor(Math.random() * this.colors.length)];
for (let i = 0; i < 8; i++) {
this.particles.push({
x, y,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
life: 1,
color,
size: Math.random() * 10 + 5
});
}
// Add trail
this.trails.push({ x, y, color, size: 30, alpha: 0.5 });
}
animate() {
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.05)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw trails
this.trails = this.trails.filter(t => {
t.alpha -= 0.005;
if (t.alpha <= 0) return false;
this.ctx.beginPath();
this.ctx.arc(t.x, t.y, t.size, 0, Math.PI * 2);
this.ctx.fillStyle = `rgba(${t.color.join(',')}, ${t.alpha})`;
this.ctx.fill();
return true;
});
// Draw particles
this.particles = this.particles.filter(p => {
p.x += p.vx;
p.y += p.vy;
p.life -= 0.02;
p.vy += 0.1;
if (p.life <= 0) return false;
this.ctx.beginPath();
this.ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
this.ctx.fillStyle = `rgba(${p.color.join(',')}, ${p.life * 0.8})`;
this.ctx.fill();
return true;
});
// Draw mouse trail
if (this.mouse.down) {
this.ctx.beginPath();
this.ctx.arc(this.mouse.x, this.mouse.y, 20, 0, Math.PI * 2);
const color = this.colors[Date.now() % 5];
this.ctx.fillStyle = `rgba(${color.join(',')}, 0.3)`;
this.ctx.fill();
}
requestAnimationFrame(() => this.animate());
}
bindEvents() {
this.canvas.addEventListener('mousedown', async (e) => {
await this.initAudio();
this.mouse.down = true;
this.mouse.x = e.clientX;
this.mouse.y = e.clientY;
this.playTone(e.clientX, e.clientY);
});
this.canvas.addEventListener('mousemove', (e) => {
this.mouse.x = e.clientX;
this.mouse.y = e.clientY;
if (this.mouse.down) {
this.playTone(e.clientX, e.clientY);
}
});
this.canvas.addEventListener('mouseup', () => {
this.mouse.down = false;
this.lastNote = null;
});
this.canvas.addEventListener('mouseleave', () => {
this.mouse.down = false;
this.lastNote = null;
});
this.canvas.addEventListener('touchstart', async (e) => {
e.preventDefault();
await this.initAudio();
this.mouse.down = true;
const touch = e.touches[0];
this.mouse.x = touch.clientX;
this.mouse.y = touch.clientY;
this.playTone(touch.clientX, touch.clientY);
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
this.mouse.x = touch.clientX;
this.mouse.y = touch.clientY;
if (this.mouse.down) {
this.playTone(touch.clientX, touch.clientY);
}
});
this.canvas.addEventListener('touchend', () => {
this.mouse.down = false;
this.lastNote = null;
});
}
}
new Synesthesia();
</script>
</body>
</html>

View File

@@ -1,371 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Visualizer — WAV Frequency</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0f; overflow: hidden; height: 100vh; }
canvas { display: block; width: 100%; height: 100%; }
.title {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.1); font-size: 24px; font-family: sans-serif;
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
}
.controls {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
display: flex; gap: 10px; z-index: 10;
}
button, select {
background: rgba(30,30,40,0.9); border: 1px solid rgba(100,100,120,0.3);
color: #e0e0e0; padding: 10px 20px; border-radius: 8px; font-size: 14px;
cursor: pointer;
}
button:hover, select:hover { background: rgba(50,50,70,0.9); }
.drop-zone {
position: fixed; inset: 0; display: flex; align-items: center;
justify-content: center; pointer-events: none; z-index: 5;
}
.drop-zone.active {
background: rgba(100,50,150,0.2);
border: 3px dashed rgba(150,100,200,0.5);
}
.drop-text {
color: rgba(255,255,255,0.3); font-size: 24px; font-family: sans-serif;
}
</style>
</head>
<body>
<div class="title">Visualizer</div>
<div class="drop-zone" id="drop-zone">
<span class="drop-text">Drop WAV file here or use controls</span>
</div>
<canvas id="canvas"></canvas>
<div class="controls">
<input type="file" id="file-input" accept="audio/*" style="display: none;">
<button id="load-btn">Load Audio</button>
<button id="play-btn" disabled>Play</button>
<button id="stop-btn" disabled>Stop</button>
<select id="viz-mode">
<option value="spectrum">Spectrum</option>
<option value="waveform">Waveform</option>
<option value="spectrogram">Spectrogram</option>
<option value="circular">Circular</option>
</select>
</div>
<script>
class AudioVisualizer {
constructor() {
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.audioCtx = null;
this.analyser = null;
this.source = null;
this.audioBuffer = null;
this.isPlaying = false;
this.mode = 'spectrum';
this.spectrogramData = [];
this.startOffset = 0;
this.resize();
window.addEventListener('resize', () => this.resize());
this.bindEvents();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
async initAudio() {
if (this.audioCtx) return;
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.audioCtx.createAnalyser();
this.analyser.fftSize = 2048;
this.analyser.smoothingTimeConstant = 0.8;
this.analyser.connect(this.audioCtx.destination);
}
async loadAudio(file) {
await this.initAudio();
const arrayBuffer = await file.arrayBuffer();
this.audioBuffer = await this.audioCtx.decodeAudioData(arrayBuffer);
document.getElementById('play-btn').disabled = false;
document.getElementById('stop-btn').disabled = false;
document.querySelector('.drop-text').textContent = file.name;
}
play() {
if (!this.audioBuffer || this.isPlaying) return;
this.source = this.audioCtx.createBufferSource();
this.source.buffer = this.audioBuffer;
this.source.connect(this.analyser);
this.source.start(0, this.startOffset);
this.isPlaying = true;
this.source.onended = () => {
this.isPlaying = false;
this.startOffset = 0;
document.getElementById('play-btn').textContent = 'Play';
};
document.getElementById('play-btn').textContent = 'Pause';
}
stop() {
if (this.source && this.isPlaying) {
this.source.stop();
this.isPlaying = false;
this.startOffset = 0;
document.getElementById('play-btn').textContent = 'Play';
}
}
animate() {
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.15)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
if (!this.analyser) {
requestAnimationFrame(() => this.animate());
return;
}
const freqData = new Uint8Array(this.analyser.frequencyBinCount);
const timeData = new Uint8Array(this.analyser.frequencyBinCount);
this.analyser.getByteFrequencyData(freqData);
this.analyser.getByteTimeDomainData(timeData);
switch (this.mode) {
case 'spectrum': this.drawSpectrum(freqData); break;
case 'waveform': this.drawWaveform(timeData); break;
case 'spectrogram': this.drawSpectrogram(freqData); break;
case 'circular': this.drawCircular(freqData, timeData); break;
}
requestAnimationFrame(() => this.animate());
}
drawSpectrum(data) {
const w = this.canvas.width;
const h = this.canvas.height;
const barCount = 128;
const barWidth = w / barCount;
const step = Math.floor(data.length / barCount);
for (let i = 0; i < barCount; i++) {
const value = data[i * step] / 255;
const barHeight = value * h * 0.8;
const hue = (i / barCount) * 300;
this.ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.8)`;
this.ctx.fillRect(
i * barWidth,
h - barHeight,
barWidth - 1,
barHeight
);
// Mirror
this.ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.2)`;
this.ctx.fillRect(
i * barWidth,
0,
barWidth - 1,
barHeight * 0.3
);
}
}
drawWaveform(data) {
const w = this.canvas.width;
const h = this.canvas.height;
this.ctx.beginPath();
this.ctx.strokeStyle = '#a78bfa';
this.ctx.lineWidth = 2;
const sliceWidth = w / data.length;
let x = 0;
for (let i = 0; i < data.length; i++) {
const v = data[i] / 128.0;
const y = (v * h) / 2;
if (i === 0) {
this.ctx.moveTo(x, y);
} else {
this.ctx.lineTo(x, y);
}
x += sliceWidth;
}
this.ctx.stroke();
// Glow
this.ctx.strokeStyle = 'rgba(167,139,250,0.3)';
this.ctx.lineWidth = 6;
this.ctx.stroke();
}
drawSpectrogram(data) {
const w = this.canvas.width;
const h = this.canvas.height;
// Add new line
this.spectrogramData.push([...data]);
// Keep last N lines
const maxLines = Math.floor(w / 2);
if (this.spectrogramData.length > maxLines) {
this.spectrogramData.shift();
}
// Draw
const line_width = w / maxLines;
this.spectrogramData.forEach((line, x) => {
const step = Math.floor(line.length / h);
for (let y = 0; y < h; y++) {
const value = line[y * step] || 0;
const hue = 270 - (value / 255) * 200;
const lightness = 20 + (value / 255) * 50;
this.ctx.fillStyle = `hsl(${hue}, 80%, ${lightness}%)`;
this.ctx.fillRect(x * line_width, h - y, line_width, 1);
}
});
}
drawCircular(freqData, timeData) {
const w = this.canvas.width;
const h = this.canvas.height;
const cx = w / 2;
const cy = h / 2;
const maxRadius = Math.min(w, h) * 0.35;
// Frequency ring
const freqStep = Math.floor(freqData.length / 180);
for (let i = 0; i < 180; i++) {
const value = freqData[i * freqStep] / 255;
const angle = (i / 180) * Math.PI * 2;
const radius = maxRadius * 0.5 + value * maxRadius * 0.5;
const x = cx + Math.cos(angle) * radius;
const y = cy + Math.sin(angle) * radius;
this.ctx.beginPath();
this.ctx.arc(x, y, 3, 0, Math.PI * 2);
const hue = (i / 180) * 360;
this.ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.8)`;
this.ctx.fill();
}
// Waveform circle
this.ctx.beginPath();
this.ctx.strokeStyle = 'rgba(255,255,255,0.3)';
this.ctx.lineWidth = 1;
for (let i = 0; i < timeData.length; i++) {
const v = timeData[i] / 128.0;
const angle = (i / timeData.length) * Math.PI * 2;
const radius = maxRadius * 0.3 * v;
const x = cx + Math.cos(angle) * radius;
const y = cy + Math.sin(angle) * radius;
if (i === 0) {
this.ctx.moveTo(x, y);
} else {
this.ctx.lineTo(x, y);
}
}
this.ctx.closePath();
this.ctx.stroke();
// Center glow
const gradient = this.ctx.createRadialGradient(cx, cy, 0, cx, cy, maxRadius * 0.3);
gradient.addColorStop(0, 'rgba(167,139,250,0.3)');
gradient.addColorStop(1, 'rgba(167,139,250,0)');
this.ctx.beginPath();
this.ctx.arc(cx, cy, maxRadius * 0.3, 0, Math.PI * 2);
this.ctx.fillStyle = gradient;
this.ctx.fill();
}
bindEvents() {
document.getElementById('load-btn').addEventListener('click', () => {
document.getElementById('file-input').click();
});
document.getElementById('file-input').addEventListener('change', async (e) => {
if (e.target.files[0]) {
await this.loadAudio(e.target.files[0]);
}
});
document.getElementById('play-btn').addEventListener('click', () => {
if (this.isPlaying) {
this.stop();
} else {
this.play();
}
});
document.getElementById('stop-btn').addEventListener('click', () => {
this.stop();
});
document.getElementById('viz-mode').addEventListener('change', (e) => {
this.mode = e.target.value;
this.spectrogramData = [];
});
// Drag and drop
const dropZone = document.getElementById('drop-zone');
document.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('active');
});
document.addEventListener('dragleave', () => {
dropZone.classList.remove('active');
});
document.addEventListener('drop', async (e) => {
e.preventDefault();
dropZone.classList.remove('active');
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('audio/')) {
await this.loadAudio(file);
}
});
// Touch support
this.canvas.addEventListener('click', async () => {
if (!this.audioCtx) {
await this.initAudio();
}
if (this.audioCtx.state === 'suspended') {
this.audioCtx.resume();
}
});
}
}
new AudioVisualizer();
</script>
</body>
</html>

View File

@@ -1,20 +0,0 @@
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { bootPage } = require('../boot.js');
const el = (tagName = 'div') => ({ tagName, textContent: '', innerHTML: '', style: {}, children: [], type: '', src: '', appendChild(child) { this.children.push(child); } });
test('bootPage handles file and http origins', () => {
const loaderSubtitle = el(), bootMessage = el(), body = el('body');
const doc = { body, querySelector: s => s === '.loader-subtitle' ? loaderSubtitle : null, getElementById: id => id === 'boot-message' ? bootMessage : null, createElement: tag => el(tag) };
const fileResult = bootPage({ location: { protocol: 'file:' } }, doc);
assert.equal(fileResult.mode, 'file');
assert.equal(body.children.length, 0);
assert.match(loaderSubtitle.textContent, /serve this world over http/i);
assert.match(bootMessage.innerHTML, /python3 -m http\.server 8888/i);
const httpResult = bootPage({ location: { protocol: 'http:' } }, doc);
assert.equal(httpResult.mode, 'module');
assert.equal(body.children.length, 1);
assert.equal(body.children[0].tagName, 'script');
assert.equal(body.children[0].type, 'module');
assert.equal(body.children[0].src, './bootstrap.mjs');
});

View File

@@ -1,28 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { readFileSync } from 'node:fs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..');
const load = () => import(pathToFileURL(path.join(repoRoot, 'bootstrap.mjs')).href);
const el = () => ({ textContent: '', innerHTML: '', style: {}, className: '' });
test('boot shows file guidance', async () => {
const { boot } = await load();
const subtitle = el(), msg = el(); let calls = 0;
const result = await boot({ win: { location: { protocol: 'file:' } }, doc: { getElementById: id => id === 'boot-message' ? msg : null, querySelector: s => s === '.loader-subtitle' ? subtitle : null }, importApp: async () => (calls += 1, {}) });
assert.equal(result.mode, 'file'); assert.equal(calls, 0); assert.match(subtitle.textContent, /serve/i); assert.match(msg.innerHTML, /python3 -m http\.server 8888/i);
});
test('sanitizer repairs synthetic and real app input', async () => {
const { sanitizeAppModuleSource, loadAppModule, boot } = await load();
const synthetic = ["import ResonanceVisualizer from './nexus/components/resonance-visualizer.js';\\nimport * as THREE from 'three';","const calibrator = boot();\\n startRenderer();","import { SymbolicEngine, AgentFSM } from './nexus/symbolic-engine.js';","class SymbolicEngine {}","/**\n * Process Evennia-specific fields from Hermes WS messages.\n * Called from handleHermesMessage for any message carrying evennia metadata.\n */\nfunction handleEvenniaEvent(data) {\n if (data.evennia_command) {\n addActionStreamEntry('cmd', data.evennia_command);\n }\n}\n\n\n// ═══════════════════════════════════════════\nfunction handleHermesMessage(data) {\n if (data.type === 'history') {\n return;\n }\n } else if (data.type && data.type.startsWith('evennia.')) {\n handleEvenniaEvent(data);\n // Evennia event bridge — process command/result/room fields if present\n handleEvenniaEvent(data);\n}","logs.innerHTML = ok;\n // Actual MemPalace initialization would happen here\n // For demo purposes we'll just show status\n statusEl.textContent = 'Connected to local MemPalace';\n statusEl.style.color = '#4af0c0';\n \n // Simulate mining process\n mineMemPalaceContent(\"Initial knowledge base setup complete\");\n } catch (err) {\n console.error('Failed to initialize MemPalace:', err);\n document.getElementById('mem-palace-status').textContent = 'MemPalace ERROR';\n document.getElementById('mem-palace-status').style.color = '#ff4466';\n }\n try {"," // Auto-mine chat every 30s\n setInterval(mineMemPalaceContent, 30000);\n try {\n const status = mempalace.status();\n document.getElementById('compression-ratio').textContent = status.compression_ratio.toFixed(1) + 'x';\n document.getElementById('docs-mined').textContent = status.total_docs;\n document.getElementById('aaak-size').textContent = status.aaak_size + 'B';\n } catch (error) {\n console.error('Failed to update MemPalace status:', error);\n }\n }\n\n // Auto-mine chat history every 30s\n"].join('\n');
const fixed = sanitizeAppModuleSource(synthetic), real = sanitizeAppModuleSource(readFileSync(path.join(repoRoot, 'app.js'), 'utf8'));
for (const text of [fixed, real]) { assert.doesNotMatch(text, /;\\n|from '\.\/nexus\/symbolic-engine\.js'|\n \}\n \} else if|Connected to local MemPalace|setInterval\(mineMemPalaceContent, 30000\);\n try \{/); }
assert.match(fixed, /resonance-visualizer\.js';\nimport \* as THREE/); assert.match(fixed, /boot\(\);\n startRenderer\(\);/);
let calls = 0; const imported = await boot({ win: { location: { protocol: 'http:' } }, doc: { getElementById() { return null; }, querySelector() { return null; }, createElement() { return { type: '', textContent: '', onload: null, onerror: null }; }, body: { appendChild(node) { node.onload(); } } }, importApp: async () => (calls += 1, {}) });
assert.equal(imported.mode, 'imported'); assert.equal(calls, 1);
const appended = []; const script = await loadAppModule({ doc: { createElement() { return { type: '', textContent: '', onload: null, onerror: null }; }, body: { appendChild(node) { appended.push(node); node.onload(); } } }, fetchImpl: async () => ({ ok: true, text: async () => "import * as THREE from 'three';" }) });
assert.equal(appended.length, 1); assert.equal(script, appended[0]); assert.equal(script.type, 'module');
});

View File

@@ -1,10 +0,0 @@
from pathlib import Path
def test_index_html_integrity():
text = (Path(__file__).resolve().parents[1] / 'index.html').read_text(encoding='utf-8')
for marker in ('<<<<<<<', '=======', '>>>>>>>', '```html', '<EFBFBD>'):
assert marker not in text
assert 'index.html\n```html' not in text
for needle in ('View Contribution Policy', 'id="mem-palace-container"', 'id="mempalace-results"', 'id="memory-filter"', 'id="memory-feed"', 'id="memory-inspect-panel"', 'id="memory-connections-panel"'):
assert text.count(needle) == 1