updating hidden service x2
All checks were successful
Build Org Website / build (push) Successful in 48s

This commit is contained in:
2026-05-10 15:56:02 +01:00
parent 771a73029e
commit 5dc0b93416
6 changed files with 15214 additions and 113 deletions

View File

@@ -2408,6 +2408,54 @@ HIDDEN_APP_HTML = r"""<!doctype html>
.memory-node circle { stroke: rgba(255, 246, 223, 0.82); stroke-width: 1.4; filter: drop-shadow(0 0 7px rgba(255, 232, 177, 0.18)); }
.memory-node.selected circle { stroke: #fff3c7; stroke-width: 3; }
.memory-node.dragging circle { stroke: #f3cd7a; stroke-width: 3; }
.cluster-node { cursor: pointer; }
.cluster-node circle {
stroke: rgba(255, 243, 199, 0.75);
stroke-width: 1.4;
filter: drop-shadow(0 0 16px rgba(211, 166, 77, 0.18));
}
.cluster-node text {
fill: #f7ead3;
font-size: 12px;
paint-order: stroke;
stroke: rgba(14, 11, 8, 0.86);
stroke-width: 3px;
stroke-linejoin: round;
}
.cluster-node .count { font-size: 18px; font-weight: 850; }
.cluster-halo { fill: rgba(211, 166, 77, 0.08); stroke: rgba(211, 166, 77, 0.22); stroke-dasharray: 3 8; }
.breadcrumb, .minimap, .guide-card {
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(18, 16, 13, 0.72);
color: #e7d5b8;
font-size: 13px;
}
.breadcrumb {
position: absolute;
z-index: 2;
left: 20px;
top: 112px;
padding: 8px 10px;
display: flex;
flex-wrap: wrap;
gap: 6px;
max-width: min(720px, calc(100% - 40px));
}
.breadcrumb button { min-height: 26px; padding: 0 8px; }
.minimap {
position: absolute;
z-index: 2;
right: 20px;
bottom: 18px;
width: 164px;
padding: 8px;
}
.minimap svg { width: 100%; height: 92px; display: block; }
.guide-card { padding: 10px 11px; margin-top: 10px; }
.guide-card strong { color: #ffe2a6; }
.tour-list { display: grid; gap: 7px; margin-top: 10px; }
.tour-list button { text-align: left; height: auto; min-height: 34px; padding: 8px 9px; }
.map-note {
position: absolute;
left: 20px;
@@ -2494,6 +2542,22 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<button id="newBtn" type="button">New memory</button>
<button id="saveBtn" class="primary" type="button">Save constellation</button>
</div>
<div class="guide-card">
<h2>How to Read the Observatory</h2>
<p class="subtle">This is not a database. It is a map of emotional relationships. Layers are depths, nodes are memories and hidden interactions, and threads show meaning, echoes, symbols, or continuation.</p>
<p class="subtle"><strong>Aphy:</strong> zoomed out, I show islands. zoom in, I show memories. this prevents the sky from becoming noise.</p>
<p class="subtle"><strong>Sensei Chi:</strong> depth is not importance. Depth is how quietly a thing asks to be approached.</p>
</div>
<div class="guide-card">
<h2>Guided Exploration</h2>
<div class="tour-list">
<button data-tour="lima" type="button">show hidden Lima memories</button>
<button data-tour="rare" type="button">show the rarest memories</button>
<button data-tour="future" type="button">show discoveries tied to Future Z</button>
<button data-tour="unresolved" type="button">show unresolved threads</button>
<button data-tour="connected" type="button">show emotionally connected nodes</button>
</div>
</div>
<div class="layer-key" id="layerKey"></div>
</aside>
@@ -2504,14 +2568,21 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<div id="mapSub" class="subtle">Layer 0 lives near the edge. Layer 5 rests at the quiet core.</div>
</div>
<div class="actions">
<button id="zoomOutBtn" type="button">Zoom out</button>
<button id="zoomInBtn" type="button">Zoom in</button>
<button id="backBtn" type="button">Back</button>
<button id="homeBtn" type="button">Whole map</button>
<button id="focusBtn" type="button">Focus thread</button>
<button id="undoBtn" type="button">Undo</button>
<button id="redoBtn" type="button">Redo</button>
<button id="duplicateBtn" type="button">Duplicate</button>
<button id="deleteBtn" class="danger" type="button">Delete</button>
</div>
</div>
<div id="breadcrumb" class="breadcrumb"></div>
<svg id="graph" viewBox="0 0 1000 760" role="img" aria-label="Hidden narrative constellation"></svg>
<div class="map-note" id="mapNote">Click a memory to open it. Drag one memory onto another to weave a continuation thread.</div>
<div class="minimap"><strong>Atlas</strong><svg id="minimap" viewBox="0 0 100 70"></svg></div>
</main>
<aside class="drawer">
@@ -2572,7 +2643,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<script>
const draftKey = "hiddenNarrativeObservatoryDraft:v1";
const state = { entries: [], meta: {}, mode: "graph", selectedId: "", undo: [], redo: [], draggingId: "", positions: new Map() };
const state = { entries: [], meta: {}, mode: "graph", selectedId: "", undo: [], redo: [], draggingId: "", positions: new Map(), clusterPositions: new Map(), zoom: 0, focusCluster: "", focusKind: "", focusIds: null, history: [] };
const fields = ["title","type","familyLayer","characters","tone","rarity","discoveryDifficulty","mysteryLevel","resonanceScore","pageLocation","triggerConditions","content","symbols","narrativeArcs","tags","emotionalRole","continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries","enabled","cssClassHooks","audioSettings","animationTrigger","notes"];
const $ = (id) => document.getElementById(id);
const toneColors = { warm: "#d3a64d", funny: "#7fb089", nostalgic: "#d06b78", wise: "#75a9bd", strange: "#a58ac9", soft: "#e6bd8c", hopeful: "#9fcf9f", protective: "#d98f71", melancholy: "#8fa7c6" };
@@ -2747,6 +2818,77 @@ HIDDEN_APP_HTML = r"""<!doctype html>
}
return pairs.slice(0, 520);
}
function clusterKey(entry) {
if (state.mode === "timeline") return `${entry.modifiedDate || entry.createdDate || "undated"}`.slice(0, 7);
if (state.mode === "character") return (entry.characters || [])[0] || "Unclaimed";
if (state.mode === "layer") return `Layer ${depth(entry)} - ${layerName(depth(entry))}`;
if (state.mode === "flow") return entry.triggerConditions ? `Trigger: ${entry.triggerConditions.split(/[,:]/)[0]}` : `Layer ${depth(entry)} discovery`;
return (entry.narrativeArcs || [])[0] || (entry.symbols || [])[0] || entry.emotionalTone || "Loose memories";
}
function clusterKindLabel() {
return { graph: "arc/tone island", timeline: "era", character: "territory", layer: "depth zone", flow: "discovery gate" }[state.mode] || "island";
}
function clusteredEntries(entries) {
const clusters = new Map();
entries.forEach((entry) => {
const key = clusterKey(entry);
if (!clusters.has(key)) clusters.set(key, { id: slugify(key), label: key, entries: [], depthTotal: 0, tones: new Map(), characters: new Map(), rarity: 0 });
const cluster = clusters.get(key);
cluster.entries.push(entry);
cluster.depthTotal += depth(entry);
cluster.tones.set(entry.emotionalTone || "warm", (cluster.tones.get(entry.emotionalTone || "warm") || 0) + 1);
(entry.characters || ["Unclaimed"]).forEach((name) => cluster.characters.set(name, (cluster.characters.get(name) || 0) + 1));
if (["rare", "very rare", "seasonal", "timed"].includes(entry.rarity)) cluster.rarity += 1;
});
return [...clusters.values()].map((cluster) => {
cluster.depth = cluster.entries.length ? Math.round(cluster.depthTotal / cluster.entries.length) : 1;
cluster.tone = [...cluster.tones.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || "warm";
cluster.character = [...cluster.characters.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || "";
cluster.resonance = cluster.entries.reduce((total, entry) => total + Number(entry.resonanceScore || 3), 0) / Math.max(1, cluster.entries.length);
return cluster;
});
}
function visibleScope() {
const entries = filteredEntries();
if (state.focusIds) return { entries: entries.filter((entry) => state.focusIds.has(entry.id)), clusters: [], focusedCluster: null };
if (state.focusCluster) {
const cluster = clusteredEntries(entries).find((item) => item.id === state.focusCluster);
return { entries: cluster ? cluster.entries : entries, clusters: [], focusedCluster: cluster || null };
}
if (state.zoom < 2 && !$("search").value) {
const clusters = clusteredEntries(entries)
.sort((a, b) => b.entries.length - a.entries.length)
.slice(0, state.zoom === 0 ? 26 : 48);
return { entries: [], clusters, focusedCluster: null };
}
return { entries: entries.slice(0, state.zoom === 2 ? 140 : 260), clusters: [], focusedCluster: null };
}
function relationshipPairsForClusters(clusters) {
const index = new Map();
clusters.forEach((cluster) => cluster.entries.forEach((entry) => index.set(entry.id, cluster.id)));
const counts = new Map();
clusters.forEach((cluster) => {
cluster.entries.forEach((entry) => {
["continuationLinks","chainReferences","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries"].forEach((key) => {
(entry[key] || []).forEach((id) => {
const target = index.get(id);
if (!target || target === cluster.id) return;
const edge = [cluster.id, target].sort().join("::");
counts.set(edge, (counts.get(edge) || 0) + 1);
});
});
});
});
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 80).map(([edge, count]) => {
const [source, target] = edge.split("::");
return { source, target, count };
});
}
function layoutEntries(entries) {
const cx = 500;
const cy = 400;
@@ -2777,6 +2919,41 @@ HIDDEN_APP_HTML = r"""<!doctype html>
});
});
}
function layoutClusters(clusters) {
const cx = 500;
const cy = 400;
state.clusterPositions = new Map();
const byDepth = new Map();
clusters.forEach((cluster) => {
const layer = state.mode === "timeline" || state.mode === "flow" ? 1 : cluster.depth;
if (!byDepth.has(layer)) byDepth.set(layer, []);
byDepth.get(layer).push(cluster);
});
[...byDepth.entries()].forEach(([layer, items]) => {
const radius = state.mode === "timeline" || state.mode === "flow" ? 0 : 90 + (5 - Number(layer)) * 58;
items.forEach((cluster, index) => {
let x;
let y;
if (state.mode === "timeline") {
x = 125 + (index % 4) * 215;
y = 145 + Math.floor(index / 4) * 112;
} else if (state.mode === "flow") {
x = 130 + Math.min(5, index % 6) * 145;
y = 145 + Math.floor(index / 6) * 105;
} else if (state.mode === "character") {
const angle = (Math.PI * 2 * index) / Math.max(1, items.length);
x = cx + Math.cos(angle) * 285;
y = cy + Math.sin(angle) * 205;
} else {
const angle = (Math.PI * 2 * index) / Math.max(1, items.length) + hash(cluster.id) / 8000;
x = cx + Math.cos(angle) * radius;
y = cy + Math.sin(angle) * radius * 0.72;
}
state.clusterPositions.set(cluster.id, { x, y });
});
});
}
function characterBucket(entry) {
const first = (entry.characters || [])[0] || "Other";
const index = Math.max(0, state.meta.characters.indexOf(first));
@@ -2788,20 +2965,40 @@ HIDDEN_APP_HTML = r"""<!doctype html>
return h;
}
function render() {
const entries = filteredEntries();
layoutEntries(entries);
const scope = visibleScope();
const entries = scope.entries;
const clusters = scope.clusters;
if (clusters.length) layoutClusters(clusters);
if (entries.length) layoutEntries(entries);
const graph = $("graph");
const pairs = relationshipPairs(entries);
const rings = state.mode === "timeline" || state.mode === "flow" ? "" : [0,1,2,3,4,5].map((layer) => {
const radius = 80 + (5 - layer) * 55;
return `<ellipse class="ring ${layer === 5 ? "core-glow" : ""}" cx="500" cy="400" rx="${radius}" ry="${radius * 0.72}"></ellipse><text class="ring-label" x="${505 + radius}" y="${400}">Layer ${layer} - ${html(layerName(layer))}</text>`;
}).join("");
const clusterEdges = relationshipPairsForClusters(clusters).map((pair) => {
const a = state.clusterPositions.get(pair.source);
const b = state.clusterPositions.get(pair.target);
if (!a || !b) return "";
return `<line class="edge theme" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" opacity="${Math.min(0.55, 0.12 + pair.count * 0.06)}"></line>`;
}).join("");
const pairs = entries.length ? relationshipPairs(entries).slice(0, state.focusCluster ? 160 : 90) : [];
const edges = pairs.map((pair) => {
const a = state.positions.get(pair.source);
const b = state.positions.get(pair.target);
if (!a || !b) return "";
return `<line class="edge ${html(pair.kind)}" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}"></line>`;
}).join("");
const clusterNodes = clusters.map((cluster) => {
const pos = state.clusterPositions.get(cluster.id);
const color = toneColors[cluster.tone] || "#d3a64d";
const size = Math.min(54, 20 + Math.sqrt(cluster.entries.length) * 6 + cluster.rarity);
return `<g class="cluster-node" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})">
<circle class="cluster-halo" r="${size + 13}"></circle>
<circle r="${size}" fill="${color}" opacity="0.58"></circle>
<text class="count" x="0" y="5" text-anchor="middle">${cluster.entries.length}</text>
<text x="0" y="${size + 18}" text-anchor="middle">${html(cluster.label).slice(0, 34)}</text>
</g>`;
}).join("");
const nodes = entries.map((entry) => {
const pos = state.positions.get(entry.id);
const color = toneColors[entry.emotionalTone] || "#d3a64d";
@@ -2813,7 +3010,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<text x="${size + 5}" y="-7">${html(entry.title).slice(0, 34)}</text>
</g>`;
}).join("");
graph.innerHTML = `${rings}${edges}${nodes}`;
graph.innerHTML = `${rings}${clusterEdges}${edges}${clusterNodes}${nodes}`;
graph.querySelectorAll(".cluster-node").forEach((node) => {
node.addEventListener("click", () => openCluster(node.dataset.id));
});
graph.querySelectorAll(".memory-node").forEach((node) => {
node.addEventListener("click", () => selectNode(node.dataset.id));
node.addEventListener("dragstart", () => { state.draggingId = node.dataset.id; node.classList.add("dragging"); });
@@ -2822,9 +3022,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
node.addEventListener("drop", (event) => { event.preventDefault(); connectNodes(state.draggingId, node.dataset.id); });
node.setAttribute("draggable", "true");
});
renderModeSupport(entries);
renderModeSupport(entries.length ? entries : filteredEntries(), clusters);
renderBreadcrumb(scope);
renderMinimap(scope);
}
function renderModeSupport(entries) {
function renderModeSupport(entries, clusters = []) {
const titles = {
graph: ["Emotional Graph", "Relationship web: continuations, echoes, symbols, characters, triggers."],
timeline: ["Timeline View", "Memories arranged by modified date so the constellation becomes a diary."],
@@ -2834,7 +3036,42 @@ HIDDEN_APP_HTML = r"""<!doctype html>
};
$("mapTitle").textContent = titles[state.mode][0];
$("mapSub").textContent = titles[state.mode][1];
$("mapNote").textContent = `${entries.length} memories visible. Drag one memory onto another to create a continuation thread.`;
const scopeText = clusters.length ? `${clusters.length} ${clusterKindLabel()}s visible, representing ${clusters.reduce((n, cluster) => n + cluster.entries.length, 0)} memories.` : `${entries.length} memories visible.`;
$("mapNote").textContent = `${scopeText} Zoom in or open an island to reveal detail. Drag memories to weave continuation threads.`;
}
function openCluster(clusterId) {
state.history.push({ focusCluster: state.focusCluster, zoom: state.zoom, selectedId: state.selectedId });
state.focusCluster = clusterId;
state.zoom = 3;
render();
const first = visibleScope().entries[0];
if (first) selectNode(first.id);
}
function renderBreadcrumb(scope) {
const parts = [`<button type="button" data-action="home">Whole map</button>`, `<span>${html(state.mode)}</span>`, `<span>zoom ${state.zoom}</span>`];
if (scope.focusedCluster) parts.push(`<span>${html(scope.focusedCluster.label)}</span>`);
$("breadcrumb").innerHTML = parts.join("");
$("breadcrumb").querySelector("[data-action='home']")?.addEventListener("click", resetMap);
}
function renderMinimap(scope) {
const clusters = scope.clusters.length ? scope.clusters : clusteredEntries(filteredEntries()).slice(0, 40);
layoutClusters(clusters);
$("minimap").innerHTML = clusters.map((cluster) => {
const p = state.clusterPositions.get(cluster.id);
const x = Math.max(4, Math.min(96, p.x / 10));
const y = Math.max(4, Math.min(66, p.y / 11));
return `<circle cx="${x}" cy="${y}" r="${Math.max(2, Math.min(6, Math.sqrt(cluster.entries.length)))}" fill="${toneColors[cluster.tone] || "#d3a64d"}" opacity="0.68"></circle>`;
}).join("");
}
function resetMap() {
state.focusCluster = "";
state.focusIds = null;
state.zoom = 0;
render();
}
function selectNode(id) {
const entry = state.entries.find((item) => item.id === id) || state.entries[0];
@@ -2855,6 +3092,73 @@ HIDDEN_APP_HTML = r"""<!doctype html>
render();
setStatus("thread woven. A continuation now runs between those memories.");
}
function zoomBy(delta) {
state.zoom = Math.max(0, Math.min(3, state.zoom + delta));
if (state.zoom < 2) state.focusCluster = "";
render();
}
function goBack() {
const previous = state.history.pop();
if (!previous) return;
state.focusCluster = previous.focusCluster;
state.focusIds = previous.focusIds || null;
state.zoom = previous.zoom;
state.selectedId = previous.selectedId;
render();
if (state.selectedId) fillForm(currentEntry());
}
function focusThread() {
const entry = currentEntry();
if (!entry) return;
const ids = new Set([entry.id, ...(entry.continuationLinks || []), ...(entry.echoes || []), ...(entry.thematicLinks || []), ...(entry.symbolicLinks || []), ...(entry.triggerLinks || []), ...(entry.parentLinks || []), ...(entry.childLinks || []), ...(entry.mirroredEntries || [])]);
state.history.push({ focusCluster: state.focusCluster, focusIds: state.focusIds, zoom: state.zoom, selectedId: state.selectedId });
state.focusCluster = "";
state.focusIds = ids;
state.zoom = 3;
render();
setStatus("focus mode is showing this memory's nearest thread first.");
}
function runTour(name) {
state.focusCluster = "";
state.focusIds = null;
state.zoom = 2;
if (name === "lima") {
$("characterFilter").value = "Lima";
$("rarityFilter").value = "";
$("toneFilter").value = "";
$("search").value = "";
}
if (name === "rare") {
$("rarityFilter").value = "rare";
$("characterFilter").value = "";
$("toneFilter").value = "";
$("search").value = "";
}
if (name === "future") {
$("characterFilter").value = "Future Z";
$("rarityFilter").value = "";
$("toneFilter").value = "";
$("search").value = "";
}
if (name === "unresolved") {
$("search").value = "untitled unresolved maybe hidden";
$("characterFilter").value = "";
$("rarityFilter").value = "";
$("toneFilter").value = "";
}
if (name === "connected") {
$("search").value = "echo memory warm";
$("characterFilter").value = "";
$("rarityFilter").value = "";
$("toneFilter").value = "";
}
render();
setStatus("guided path opened. adjust the filters when you want to wander differently.");
}
function renderPreview(entry) {
const body = html(entry.content || "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
$("preview").innerHTML = `${body || "<span class='subtle'>This memory is waiting for words.</span>"}<div class="pills" style="margin-top:10px"><span class="pill">${html(layerName(depth(entry)))}</span><span class="pill">${html(entry.emotionalTone)}</span><span class="pill">${html(entry.rarity)}</span>${(entry.symbols || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div>`;
@@ -2977,15 +3281,24 @@ HIDDEN_APP_HTML = r"""<!doctype html>
["search","layerFilter","characterFilter","toneFilter","rarityFilter"].forEach((id) => $(id).addEventListener("input", render));
$("newBtn").onclick = () => newEntry();
$("saveBtn").onclick = save;
$("zoomOutBtn").onclick = () => zoomBy(-1);
$("zoomInBtn").onclick = () => zoomBy(1);
$("backBtn").onclick = goBack;
$("homeBtn").onclick = resetMap;
$("focusBtn").onclick = focusThread;
$("duplicateBtn").onclick = duplicateEntry;
$("deleteBtn").onclick = deleteEntry;
$("undoBtn").onclick = () => { if (!state.undo.length) return; state.redo.push(JSON.stringify(state.entries)); restore(state.undo.pop()); };
$("redoBtn").onclick = () => { if (!state.redo.length) return; state.undo.push(JSON.stringify(state.entries)); restore(state.redo.pop()); };
$("modebar").querySelectorAll("button").forEach((button) => button.addEventListener("click", () => {
state.mode = button.dataset.mode;
state.focusCluster = "";
state.focusIds = null;
state.zoom = 0;
$("modebar").querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button));
render();
}));
document.querySelectorAll("[data-tour]").forEach((button) => button.addEventListener("click", () => runTour(button.dataset.tour)));
load();
</script>
</body>