Files
hermes-wiki-static/vault.html.lukas
T
agentandClaude 5394f7e9bf Rollback to upstream-obv (v0.1 effective)
Vier Customization-Iterationen (v0.2 bis v0.5.1) haben auf Mobile (iOS Safari)
und Chrome Desktop jeweils neue Probleme verursacht statt sie zu lösen.

Rollback entfernt:
- vault.html: Script-Tag für vault-custom.js entfernt (upstream-original)
- server.py: ?file= query-Patch entfernt (upstream-original)
- vault-custom.js: leerer Stub mit Comment (Customizations deaktiviert)
- hermes-custom.css: leerer Stub (CSS deaktiviert)

Was bleibt:
- Repo-Struktur (README, CHANGELOG, ARCHITECTURE, CUSTOMIZATIONS, scripts/)
- Documented history der Customization-Iterationen für späteres Re-Audit
- Backup-Dateien: *.lukas (alle Original-Patches vor diesem Commit)

Begründung: Lukas braucht ein verlässlich funktionierendes Wiki, nicht
endlose Problemlösungs-Schleifen. Basis muss stimmen, bevor neue Features
ausprobiert werden.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 04:02:08 +00:00

356 lines
14 KiB
Plaintext

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Vault Viewer</title>
<link rel="stylesheet" href="hermes-custom.css">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
:root {
--bg-primary: #0a0a14; --bg-secondary: #0e0e1a; --bg-surface: #1a1a2e;
--text-primary: #cdd6f4; --text-secondary: #a6adc8; --text-muted: #6c7086;
--accent: #FFD700; --link: #FFD700; --border: #313244;
--green: #a6e3a1; --red: #f38ba8; --blue: #89b4fa; --teal: #94e2d5;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg-primary); color: var(--text-primary); font-family: -apple-system, 'Segoe UI', Inter, sans-serif; height: 100vh; overflow: hidden; }
.vault-app {
display: grid; grid-template-columns: 280px 1fr 260px;
grid-template-rows: 48px 1fr; height: 100vh;
}
.vault-app.graph-hidden { grid-template-columns: 280px 1fr; }
/* Header */
.vault-header {
grid-column: 1 / -1; background: var(--bg-secondary);
border-bottom: 1px solid var(--border); display: flex; align-items: center;
padding: 0 16px; gap: 12px;
}
.vault-header h1 { color: var(--accent); font-size: 14px; font-weight: 700; letter-spacing: 1px; }
.vault-header .search-box {
flex: 1; max-width: 400px; position: relative;
}
.vault-header input {
width: 100%; padding: 6px 12px 6px 32px; background: rgba(30,30,50,0.6);
border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary);
font-size: 13px; outline: none;
}
.vault-header input:focus { border-color: var(--accent); }
.vault-header .toggle-btn {
background: none; border: 1px solid var(--border); color: var(--text-muted);
padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px;
}
.vault-header .toggle-btn:hover { border-color: var(--accent); color: var(--accent); }
/* Sidebar file tree */
.vault-sidebar {
background: var(--bg-secondary); border-right: 1px solid var(--border);
overflow-y: auto; padding: 8px 0; font-size: 13px;
}
.tree-folder { cursor: pointer; user-select: none; }
.tree-folder-header {
display: flex; align-items: center; padding: 4px 12px; gap: 6px;
color: var(--text-secondary); border-radius: 4px; margin: 1px 4px;
}
.tree-folder-header:hover { background: rgba(255,215,0,0.05); }
.tree-folder-header .arrow { color: var(--text-muted); font-size: 10px; width: 12px; }
.tree-children { padding-left: 12px; display: none; }
.tree-children.open { display: block; }
.tree-file {
display: flex; align-items: center; padding: 4px 12px; gap: 6px;
color: var(--text-primary); cursor: pointer; border-radius: 4px; margin: 1px 4px;
font-size: 13px;
}
.tree-file:hover { background: rgba(255,215,0,0.05); }
.tree-file.active { background: rgba(255,215,0,0.1); color: var(--accent); }
/* Content area */
.vault-content {
overflow-y: auto; padding: 32px 48px; max-width: 900px;
line-height: 1.7; font-size: 15px;
}
.vault-content h1 { color: var(--accent); font-size: 28px; margin: 24px 0 12px; }
.vault-content h2 { color: var(--accent); font-size: 22px; margin: 20px 0 10px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }
.vault-content h3 { color: var(--text-primary); font-size: 18px; margin: 16px 0 8px; }
.vault-content p { margin: 8px 0; }
.vault-content a { color: var(--link); text-decoration: none; }
.vault-content a:hover { text-decoration: underline; }
.vault-content code { background: var(--bg-surface); padding: 2px 6px; border-radius: 4px; font-size: 13px; color: var(--teal); }
.vault-content pre { background: var(--bg-surface); padding: 16px; border-radius: 8px; overflow-x: auto; margin: 12px 0; }
.vault-content pre code { padding: 0; background: none; }
.vault-content blockquote { border-left: 3px solid var(--accent); padding-left: 16px; color: var(--text-secondary); margin: 12px 0; }
.vault-content ul, .vault-content ol { padding-left: 24px; }
.vault-content li { margin: 4px 0; }
.vault-content table { border-collapse: collapse; width: 100%; margin: 12px 0; }
.vault-content th, .vault-content td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; }
.vault-content th { background: var(--bg-surface); color: var(--accent); font-size: 13px; }
.vault-content img { max-width: 100%; border-radius: 8px; }
.frontmatter { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; font-size: 12px; }
.frontmatter .fm-key { color: var(--text-muted); }
.frontmatter .fm-val { color: var(--text-primary); margin-left: 8px; }
/* Graph panel */
.vault-graph {
background: var(--bg-secondary); border-left: 1px solid var(--border);
position: relative; overflow: hidden;
}
.vault-graph canvas { width: 100% !important; height: 100% !important; }
/* Search results overlay */
#search-results {
position: absolute; top: 48px; left: 50%; transform: translateX(-50%);
width: 420px; max-height: 400px; overflow-y: auto;
background: rgba(14,14,26,0.95); border: 1px solid var(--border);
border-radius: 8px; z-index: 100; display: none;
backdrop-filter: blur(12px);
}
.search-item {
padding: 8px 14px; cursor: pointer; border-bottom: 1px solid rgba(49,50,68,0.5);
}
.search-item:hover { background: rgba(255,215,0,0.05); }
.search-item .name { color: var(--accent); font-weight: 600; }
.search-item .folder { color: var(--text-muted); font-size: 11px; margin-left: 8px; }
.search-item .snippet { color: var(--text-secondary); font-size: 12px; margin-top: 2px; }
/* Welcome screen */
.welcome {
display: flex; flex-direction: column; align-items: center; justify-content: center;
height: 100%; color: var(--text-muted); text-align: center;
}
.welcome h2 { color: var(--accent); margin-bottom: 8px; }
</style>
</head>
<body>
<div class="vault-app" id="app">
<div class="vault-header">
<h1>Vault</h1>
<div class="search-box">
<input type="text" id="search-input" placeholder="Search notes..." oninput="onSearch(this.value)" onfocus="showSearch()" onblur="setTimeout(hideSearch, 200)">
</div>
<button class="toggle-btn" onclick="toggleGraph()">Graph</button>
</div>
<div class="vault-sidebar" id="sidebar"></div>
<div class="vault-content" id="content">
<div class="welcome">
<h2>Welcome to Vault Viewer</h2>
<p>Select a note from the file tree or use search.</p>
</div>
</div>
<div class="vault-graph" id="graph-panel">
<canvas id="graph-canvas"></canvas>
</div>
</div>
<div id="search-results"></div>
<script>
let currentFile = null;
let graphVisible = true;
let graphData = null;
// Load file tree
async function loadTree() {
const resp = await fetch('/api/vault/tree');
const tree = await resp.json();
document.getElementById('sidebar').innerHTML = renderTree(tree, '');
}
function renderTree(node, path) {
if (node.type === 'file') {
const fullPath = path ? path + '/' + node.name : node.name;
return `<div class="tree-file" onclick="loadFile('${fullPath.replace(/'/g, "\\'")}')" data-path="${fullPath}">${node.name.replace('.md', '')}</div>`;
}
const fullPath = path ? path + '/' + node.name : node.name;
let html = '';
if (node.name) {
html += `<div class="tree-folder">
<div class="tree-folder-header" onclick="this.nextElementSibling.classList.toggle('open');this.querySelector('.arrow').textContent=this.nextElementSibling.classList.contains('open')?'v':'>'">
<span class="arrow">></span> ${node.name}
</div>
<div class="tree-children">`;
}
(node.children || []).forEach(c => { html += renderTree(c, fullPath); });
if (node.name) html += '</div></div>';
return html;
}
// Load file content
async function loadFile(path) {
// Highlight active
document.querySelectorAll('.tree-file').forEach(el => el.classList.remove('active'));
const activeEl = document.querySelector(`.tree-file[data-path="${path}"]`);
if (activeEl) activeEl.classList.add('active');
const resp = await fetch('/api/vault/file/' + encodeURIComponent(path));
const data = await resp.json();
if (data.error) {
document.getElementById('content').innerHTML = `<p style="color:var(--red)">${data.error}</p>`;
return;
}
currentFile = path;
let html = '';
// Frontmatter
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
html += '<div class="frontmatter">';
Object.entries(data.frontmatter).forEach(([k, v]) => {
const val = Array.isArray(v) ? v.join(', ') : String(v);
html += `<div><span class="fm-key">${k}:</span><span class="fm-val">${val}</span></div>`;
});
html += '</div>';
}
// Markdown body — convert wikilinks first
let body = data.body || '';
body = body.replace(/\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g, (match, target, alias) => {
const display = alias || target;
return `<a href="#" onclick="navigateWikilink('${target.replace(/'/g, "\\'")}');return false">${display}</a>`;
});
html += marked.parse(body);
document.getElementById('content').innerHTML = html;
document.getElementById('content').scrollTop = 0;
}
// Wikilink navigation
async function navigateWikilink(target) {
// Search for the target file
const resp = await fetch('/api/vault/search?q=' + encodeURIComponent(target));
const data = await resp.json();
if (data.results && data.results.length > 0) {
// Find exact name match first
const exact = data.results.find(r => r.name.toLowerCase() === target.toLowerCase());
loadFile((exact || data.results[0]).path);
}
}
// Search
let searchTimeout = null;
function onSearch(query) {
clearTimeout(searchTimeout);
if (!query.trim()) { hideSearch(); return; }
searchTimeout = setTimeout(async () => {
const resp = await fetch('/api/vault/search?q=' + encodeURIComponent(query));
const data = await resp.json();
const el = document.getElementById('search-results');
if (!data.results || data.results.length === 0) {
el.innerHTML = '<div class="search-item"><span style="color:var(--text-muted)">No results</span></div>';
} else {
el.innerHTML = data.results.map(r =>
`<div class="search-item" onmousedown="loadFile('${r.path.replace(/'/g, "\\'")}');hideSearch()">
<div><span class="name">${r.name}</span><span class="folder">${r.folder}</span></div>
${r.snippet ? '<div class="snippet">' + r.snippet + '</div>' : ''}
</div>`
).join('');
}
el.style.display = 'block';
}, 200);
}
function showSearch() { if (document.getElementById('search-input').value) onSearch(document.getElementById('search-input').value); }
function hideSearch() { document.getElementById('search-results').style.display = 'none'; }
// Graph toggle
function toggleGraph() {
graphVisible = !graphVisible;
document.getElementById('app').classList.toggle('graph-hidden', !graphVisible);
if (graphVisible && !graphData) loadGraph();
}
// 3D Graph
let graphScene, graphCamera, graphRenderer;
async function loadGraph() {
const resp = await fetch('/api/vault/graph');
graphData = await resp.json();
const container = document.getElementById('graph-panel');
const canvas = document.getElementById('graph-canvas');
const W = container.clientWidth;
const H = container.clientHeight;
graphScene = new THREE.Scene();
graphScene.background = new THREE.Color(0x0e0e1a);
graphCamera = new THREE.PerspectiveCamera(60, W / H, 1, 2000);
graphCamera.position.set(0, 0, 300);
graphRenderer = new THREE.WebGLRenderer({ canvas, antialias: true });
graphRenderer.setSize(W, H);
// Position nodes in a 3D force layout (simplified)
const nodePositions = {};
const nodes = graphData.nodes || [];
nodes.forEach((n, i) => {
const angle = (i / nodes.length) * Math.PI * 2;
const radius = 80 + Math.random() * 120;
nodePositions[n.id] = {
x: Math.cos(angle) * radius + (Math.random() - 0.5) * 40,
y: (Math.random() - 0.5) * 100,
z: Math.sin(angle) * radius + (Math.random() - 0.5) * 40,
};
});
// Node dots
const colors = { wiki: 0x89b4fa, daily: 0xf9e2af, journal: 0xa6e3a1, projects: 0xf5c2e7, other: 0x6c7086 };
nodes.forEach(n => {
const pos = nodePositions[n.id];
const color = colors[n.type] || colors.other;
const geo = new THREE.SphereGeometry(1.5, 8, 8);
const mat = new THREE.MeshBasicMaterial({ color });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(pos.x, pos.y, pos.z);
graphScene.add(mesh);
});
// Edges
(graphData.edges || []).forEach(e => {
const s = nodePositions[e.source];
const t = nodePositions[e.target];
if (!s || !t) return;
const geo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(s.x, s.y, s.z),
new THREE.Vector3(t.x, t.y, t.z),
]);
const mat = new THREE.LineBasicMaterial({ color: 0x313244, transparent: true, opacity: 0.3 });
graphScene.add(new THREE.Line(geo, mat));
});
// Orbit
let isDrag = false, lastX = 0, lastY = 0, rotX = 0, rotY = 0;
canvas.addEventListener('mousedown', e => { isDrag = true; lastX = e.clientX; lastY = e.clientY; });
canvas.addEventListener('mousemove', e => {
if (!isDrag) return;
rotY += (e.clientX - lastX) * 0.005;
rotX += (e.clientY - lastY) * 0.005;
lastX = e.clientX; lastY = e.clientY;
});
canvas.addEventListener('mouseup', () => isDrag = false);
canvas.addEventListener('wheel', e => {
graphCamera.position.z += e.deltaY * 0.5;
graphCamera.position.z = Math.max(50, Math.min(800, graphCamera.position.z));
});
function animateGraph() {
requestAnimationFrame(animateGraph);
graphScene.rotation.y = rotY;
graphScene.rotation.x = rotX;
graphRenderer.render(graphScene, graphCamera);
}
animateGraph();
}
// Init
loadTree();
loadGraph();
</script>
<script src="vault-custom.js"></script>
</body>
</html>