rename
All checks were successful
Build Org Website / build (push) Successful in 48s

This commit is contained in:
2026-05-10 16:29:25 +01:00
parent 52a0160198
commit 527bc66b05
7 changed files with 22337 additions and 1107 deletions

View File

@@ -121,11 +121,11 @@ HIDDEN_CONTENT_TYPES = [
"loading screen message",
"secret interaction",
"hidden tooltip",
"Future Z message",
"Young Z memory fragment",
"Sensei Chi wisdom entry",
"Aphy system message",
"Lima note/message",
"future z message",
"young z memory fragment",
"sensei chi wisdom entry",
"aphy system message",
"lima note/message",
"dream sequence",
"terminal log",
"fake error message",
@@ -142,7 +142,69 @@ HIDDEN_CONTENT_TYPES = [
"keyboard secret",
]
HIDDEN_CHARACTERS = ["Lima", "Aphy", "Sensei Chi", "Young Z", "Future Z", "Z"]
CHARACTER_REGISTRY = {
"young z": {
"id": "young z",
"displayLabel": "young z",
"aliases": ["Young Z", "young z", "young-z", "young_z", "young"],
"territoryColor": "#d8a95a",
"symbol": "Y",
"motifs": ["crayon sun", "blanket cape", "childhood desk"],
"themes": ["childhood", "play", "memory", "safety"],
"affinities": ["z", "future z", "aphy", "lima"],
},
"z": {
"id": "z",
"displayLabel": "z",
"aliases": ["Z", "z", "zaine"],
"territoryColor": "#d6c38a",
"symbol": "Z",
"motifs": ["archive", "website", "home"],
"themes": ["selfhood", "return", "making"],
"affinities": ["lima", "young z", "future z"],
},
"aphy": {
"id": "aphy",
"displayLabel": "aphy",
"aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"],
"territoryColor": "#7fb089",
"symbol": "A",
"motifs": ["console", "diagnostic", "backup"],
"themes": ["humor", "systems", "care through tools"],
"affinities": ["z", "lima", "sensei chi"],
},
"lima": {
"id": "lima",
"displayLabel": "lima",
"aliases": ["Lima", "lima"],
"territoryColor": "#d06b78",
"symbol": "L",
"motifs": ["warmth", "kitchen light", "ring"],
"themes": ["love", "home", "grounding"],
"affinities": ["z", "aphy", "future z", "young z"],
},
"sensei chi": {
"id": "sensei chi",
"displayLabel": "sensei chi",
"aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"],
"territoryColor": "#75a9bd",
"symbol": "S",
"motifs": ["tea", "garden", "quiet lesson"],
"themes": ["reflection", "patience", "wisdom"],
"affinities": ["aphy", "future z"],
},
"future z": {
"id": "future z",
"displayLabel": "future z",
"aliases": ["Future Z", "future z", "future-z", "future_z", "future"],
"territoryColor": "#a58ac9",
"symbol": "F",
"motifs": ["clock", "age 40", "future log"],
"themes": ["time", "reassurance", "continuity"],
"affinities": ["z", "young z", "lima", "sensei chi"],
},
}
HIDDEN_CHARACTERS = list(CHARACTER_REGISTRY)
HIDDEN_TONES = ["warm", "funny", "nostalgic", "wise", "strange", "soft", "hopeful", "protective", "melancholy"]
HIDDEN_RARITIES = ["common", "uncommon", "rare", "very rare", "seasonal", "timed"]
HIDDEN_LAYER_DEPTHS = [
@@ -159,17 +221,17 @@ HIDDEN_LAYER_DEPTHS = [
{
"id": "2",
"name": "Memory Layer",
"meaning": "Young Z memories, Lima notes, nostalgia, and emotional fragments.",
"meaning": "young z memories, lima notes, nostalgia, and emotional fragments.",
},
{
"id": "3",
"name": "Reflection Layer",
"meaning": "Sensei Chi philosophy, Aphy conversations, and introspection.",
"meaning": "sensei chi philosophy, aphy conversations, and introspection.",
},
{
"id": "4",
"name": "Time Layer",
"meaning": "Future Z logs, time anomalies, long-term revisits, and future/past echoes.",
"meaning": "future z logs, time anomalies, long-term revisits, and future/past echoes.",
},
{
"id": "5",
@@ -773,11 +835,67 @@ def js_literal_to_json(value: str) -> Any:
return json.loads(cleaned)
def character_alias_lookup() -> dict[str, str]:
aliases = {}
for canonical, config in CHARACTER_REGISTRY.items():
aliases[canonical] = canonical
for alias in config["aliases"]:
aliases[slugify(str(alias)).replace("-", " ")] = canonical
aliases[str(alias).strip().lower()] = canonical
return aliases
def normalize_character_name(value: Any) -> str | None:
raw = str(value or "").strip()
if not raw:
return None
simplified = slugify(raw).replace("-", " ")
return character_alias_lookup().get(raw.lower()) or character_alias_lookup().get(simplified)
def normalize_character_list(values: Any) -> list[str]:
if isinstance(values, str):
raw_values = re.split(r"[,/]", values)
elif isinstance(values, list):
raw_values = values
else:
raw_values = []
normalized = []
for item in raw_values:
canonical = normalize_character_name(item)
if canonical and canonical not in normalized:
normalized.append(canonical)
return normalized
def normalize_character_text_refs(value: Any) -> Any:
if isinstance(value, list):
return [normalize_character_text_refs(item) for item in value]
if isinstance(value, dict):
return {key: normalize_character_text_refs(item) for key, item in value.items()}
if not isinstance(value, str):
return value
text = value
replacements = []
for canonical, config in CHARACTER_REGISTRY.items():
for alias in config["aliases"]:
if alias == canonical:
continue
if alias.lower() in {"young", "future", "sensei"}:
continue
replacements.append((alias, canonical))
replacements.sort(key=lambda item: len(item[0]), reverse=True)
for alias, canonical in replacements:
pattern = r"(?<![A-Za-z0-9_-])" + re.escape(alias) + r"(?![A-Za-z0-9_-])"
text = re.sub(pattern, canonical, text, flags=re.IGNORECASE)
return text
def detect_hidden_characters(text: str) -> list[str]:
lowered = text.lower()
found = []
normalized_text = normalize_character_text_refs(text).lower()
for character in HIDDEN_CHARACTERS:
if character.lower() in lowered:
if re.search(r"(?<![A-Za-z0-9_-])" + re.escape(character) + r"(?![A-Za-z0-9_-])", normalized_text):
found.append(character)
return found
@@ -887,7 +1005,7 @@ def migrate_hidden_entries_from_js() -> list[dict[str, Any]]:
for index, (query, route) in enumerate(search_routes.items()):
add(make_hidden_entry("search route", route, index, title=f"Route: {query}", category="search", pageLocation=route, triggerConditions=query, query=query))
for index, item in enumerate(keyboard_secrets + long_keyboard_secrets):
content = item.get("message") or item.get("route") or ("play tiny Aphy song" if item.get("song") else "")
content = item.get("message") or item.get("route") or ("play tiny aphy song" if item.get("song") else "")
add(make_hidden_entry("keyboard secret", content, index, title=f"Keyboard: {item.get('phrase')}", category="keyboard", pageLocation=item.get("route", ""), triggerConditions=item.get("phrase", ""), keyboard=item))
return entries
@@ -896,9 +1014,10 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None
today = datetime.now().date().isoformat()
entry = existing.copy() if existing else {}
entry.update(raw)
title = str(entry.get("title") or "").strip()
content = str(entry.get("content") or "").replace("\r\n", "\n")
title = str(normalize_character_text_refs(entry.get("title") or "")).strip()
content = str(normalize_character_text_refs(entry.get("content") or "")).replace("\r\n", "\n")
content_type = str(entry.get("type") or "quote").strip()
content_type = str(normalize_character_text_refs(content_type))
if content_type not in HIDDEN_CONTENT_TYPES:
raise ValueError(f"Unsupported hidden content type: {content_type}")
if not title:
@@ -909,18 +1028,30 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None
entry["title"] = title
entry["type"] = content_type
entry["content"] = content
entry["characters"] = [str(item).strip() for item in entry.get("characters", []) if str(item).strip()]
detected_characters = detect_hidden_characters(" ".join([
title,
content,
str(entry.get("triggerConditions") or ""),
str(entry.get("notes") or ""),
str(entry.get("category") or ""),
]))
entry["characters"] = normalize_character_list(entry.get("characters", []))
for character in detected_characters:
if character not in entry["characters"]:
entry["characters"].append(character)
if not entry["characters"]:
entry["characters"] = ["z"]
entry["emotionalTone"] = str(entry.get("emotionalTone") or "warm")
entry["rarity"] = str(entry.get("rarity") or "common")
entry["triggerConditions"] = str(entry.get("triggerConditions") or "")
entry["triggerConditions"] = str(normalize_character_text_refs(entry.get("triggerConditions") or ""))
entry["tags"] = normalise_tags(entry.get("tags", []))
entry["category"] = str(entry.get("category") or content_type)
entry["category"] = str(normalize_character_text_refs(entry.get("category") or content_type))
entry["pageLocation"] = str(entry.get("pageLocation") or "")
entry["familyLayer"] = str(entry.get("familyLayer") or "")
entry["enabled"] = bool(entry.get("enabled", True))
entry["createdDate"] = str(entry.get("createdDate") or today)
entry["modifiedDate"] = today
entry["notes"] = str(entry.get("notes") or "")
entry["notes"] = str(normalize_character_text_refs(entry.get("notes") or ""))
entry["audioSettings"] = str(entry.get("audioSettings") or "")
entry["animationTrigger"] = str(entry.get("animationTrigger") or "")
entry["cssClassHooks"] = str(entry.get("cssClassHooks") or "")
@@ -944,7 +1075,10 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None
"symbolicLinks",
"triggerLinks",
]:
entry[key] = [str(item).strip() for item in entry.get(key, []) if str(item).strip()]
entry[key] = [str(normalize_character_text_refs(item)).strip() for item in entry.get(key, []) if str(item).strip()]
for key in ["dialogue", "keyboard"]:
if key in entry:
entry[key] = normalize_character_text_refs(entry[key])
return entry
@@ -970,6 +1104,8 @@ def load_hidden_store() -> dict[str, Any]:
"migratedFromJs": migrated,
"types": HIDDEN_CONTENT_TYPES,
"characters": HIDDEN_CHARACTERS,
"characterRegistry": CHARACTER_REGISTRY,
"validation": validate_hidden_integrity(normalized),
"tones": HIDDEN_TONES,
"rarities": HIDDEN_RARITIES,
"layers": HIDDEN_LAYER_DEPTHS,
@@ -978,6 +1114,36 @@ def load_hidden_store() -> dict[str, Any]:
}
def validate_hidden_integrity(entries: list[dict[str, Any]]) -> dict[str, Any]:
ids = {entry["id"] for entry in entries}
unknown_characters = []
orphan_nodes = []
stale_links = []
for entry in entries:
characters = entry.get("characters", [])
if not characters:
orphan_nodes.append(entry["id"])
for character in characters:
if character not in CHARACTER_REGISTRY:
unknown_characters.append({"entry": entry["id"], "character": character})
for key in ["chainReferences", "continuationLinks", "parentLinks", "childLinks", "echoes", "mirroredEntries", "thematicLinks", "symbolicLinks", "triggerLinks"]:
for target in entry.get(key, []):
if target and target not in ids and not str(target).startswith("/"):
stale_links.append({"entry": entry["id"], "field": key, "target": target})
return {
"ok": not unknown_characters and not orphan_nodes and not stale_links,
"unknownCharacters": unknown_characters[:50],
"orphanNodes": orphan_nodes[:50],
"staleLinks": stale_links[:50],
"summary": {
"unknownCharacterCount": len(unknown_characters),
"orphanNodeCount": len(orphan_nodes),
"staleLinkCount": len(stale_links),
},
"repairPolicy": "Unknown aliases are normalized through the registry. Empty character lists are repaired to z so every memory has a territory.",
}
def hidden_architecture_recommendations() -> dict[str, Any]:
return {
"storage": "Use assets/content/hidden-details.json as the friendly source of truth for narrative nodes, then regenerate the editable constants in assets/scripts/hidden-details.js.",
@@ -1005,7 +1171,7 @@ def generated_hidden_content_block(entries: list[dict[str, Any]]) -> str:
entry["content"] for entry in hidden_entries_by_type(entries, "rare event")
if "night" in f"{entry.get('category', '')} {entry.get('triggerConditions', '')}".lower()
]
quote_like = hidden_contents(entries, "quote") + hidden_contents(entries, "Sensei Chi wisdom entry") + hidden_contents(entries, "Aphy system message") + hidden_contents(entries, "Lima note/message") + hidden_contents(entries, "Future Z message") + hidden_contents(entries, "Young Z memory fragment")
quote_like = hidden_contents(entries, "quote") + hidden_contents(entries, "sensei chi wisdom entry") + hidden_contents(entries, "aphy system message") + hidden_contents(entries, "lima note/message") + hidden_contents(entries, "future z message") + hidden_contents(entries, "young z memory fragment")
lore = {
"quotes": quote_like,
"conversations": [
@@ -1046,7 +1212,7 @@ def generated_hidden_content_block(entries: list[dict[str, Any]]) -> str:
secret["phrase"] = secret.get("phrase") or entry.get("triggerConditions") or entry.get("title", "")
if entry.get("pageLocation"):
secret["route"] = entry.get("pageLocation")
elif entry.get("content") == "play tiny Aphy song":
elif entry.get("content") == "play tiny aphy song":
secret["song"] = True
else:
secret["message"] = entry.get("content", "")
@@ -2534,7 +2700,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<aside class="left">
<h1>Memory Observatory</h1>
<div class="subtle">A constellation map for the hidden soul of the website.</div>
<div id="status" class="status">Aphy is dimming the room lights.</div>
<div id="status" class="status">aphy is dimming the room lights.</div>
<div class="modebar" id="modebar">
<button data-mode="graph" class="active">Emotional graph</button>
<button data-mode="timeline">Timeline</button>
@@ -2556,19 +2722,23 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<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>
<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="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="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="guide-card">
<h2>Integrity</h2>
<div id="integrityStatus" class="subtle">checking character territories.</div>
</div>
<div class="layer-key" id="layerKey"></div>
</aside>
@@ -2608,7 +2778,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<label class="full">Name<input id="title" /></label>
<label>Kind<select id="type"></select></label>
<label>Depth<select id="familyLayer"></select></label>
<label>Characters<input id="characters" placeholder="Lima, Aphy" /></label>
<label>Characters<input id="characters" placeholder="lima, aphy" /></label>
<label>Tone<select id="tone"></select></label>
<label>Rarity<select id="rarity"></select></label>
<label>Discovery<select id="discoveryDifficulty" placeholder="gentle, patient, hidden" /></label>
@@ -2660,7 +2830,6 @@ HIDDEN_APP_HTML = r"""<!doctype html>
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" };
const characterSymbols = { Lima: "L", Aphy: "A", "Sensei Chi": "S", "Young Z": "Y", "Future Z": "F", Z: "Z" };
function html(value) {
return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
@@ -2668,6 +2837,25 @@ HIDDEN_APP_HTML = r"""<!doctype html>
function splitList(value) {
return String(value || "").split(",").map((item) => item.trim()).filter(Boolean);
}
function normalizeCharacterName(value) {
const raw = String(value || "").trim();
if (!raw) return "";
const simplified = slugify(raw).replace(/-/g, " ");
const registry = state.meta.characterRegistry || {};
for (const [id, config] of Object.entries(registry)) {
const aliases = [id, ...(config.aliases || [])].map((item) => String(item).toLowerCase());
if (aliases.includes(raw.toLowerCase()) || aliases.map((item) => slugify(item).replace(/-/g, " ")).includes(simplified)) return id;
}
return "";
}
function normalizeCharacters(value) {
const normalized = [];
splitList(value).forEach((item) => {
const character = normalizeCharacterName(item);
if (character && !normalized.includes(character)) normalized.push(character);
});
return normalized.length ? normalized : ["z"];
}
function slugify(value) {
return String(value || "memory").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "memory";
}
@@ -2761,9 +2949,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
function depth(entry) {
const value = String(entry.familyLayer || "").match(/[0-5]/);
if (value) return Number(value[0]);
if ((entry.characters || []).includes("Future Z")) return 4;
if ((entry.characters || []).includes("Sensei Chi")) return 3;
if ((entry.characters || []).includes("Young Z") || (entry.characters || []).includes("Lima")) return 2;
if ((entry.characters || []).includes("future z")) return 4;
if ((entry.characters || []).includes("sensei chi")) return 3;
if ((entry.characters || []).includes("young z") || (entry.characters || []).includes("lima")) return 2;
return entry.rarity === "rare" || entry.rarity === "very rare" ? 5 : 1;
}
function currentEntry() {
@@ -2777,7 +2965,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
title: $("title").value.trim() || "Untitled memory",
type: $("type").value,
familyLayer: $("familyLayer").value,
characters: splitList($("characters").value),
characters: normalizeCharacters($("characters").value),
emotionalTone: $("tone").value,
rarity: $("rarity").value,
discoveryDifficulty: $("discoveryDifficulty").value,
@@ -2896,7 +3084,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
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 === "character") return (entry.characters || [])[0] || "z";
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";
@@ -2915,7 +3103,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
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));
(entry.characters || ["z"]).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) => {
@@ -3057,10 +3245,16 @@ HIDDEN_APP_HTML = r"""<!doctype html>
}
}
function characterBucket(entry) {
const first = (entry.characters || [])[0] || "Other";
const first = (entry.characters || [])[0] || "z";
const index = Math.max(0, state.meta.characters.indexOf(first));
return index % 6;
}
function characterSymbol(character) {
return state.meta.characterRegistry?.[character]?.symbol || "*";
}
function characterColor(character) {
return state.meta.characterRegistry?.[character]?.territoryColor || "";
}
function hash(value) {
let h = 0;
for (let i = 0; i < value.length; i += 1) h = (h * 31 + value.charCodeAt(i)) % 9999;
@@ -3094,7 +3288,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const occupied = [];
const clusterNodes = clusters.map((cluster) => {
const pos = state.clusterPositions.get(cluster.id);
const color = toneColors[cluster.tone] || "#d3a64d";
const color = state.mode === "character" ? (characterColor(cluster.label) || toneColors[cluster.tone] || "#d3a64d") : (toneColors[cluster.tone] || "#d3a64d");
const size = Math.min(120, 48 + Math.sqrt(cluster.entries.length) * 18 + cluster.rarity * 3);
const label = labelForCluster(cluster, pos, size, occupied);
return `<g class="cluster-node" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})">
@@ -3108,7 +3302,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
if (!isWorldVisible(pos.x, pos.y, 220)) return "";
const color = toneColors[entry.emotionalTone] || "#d3a64d";
const size = 18 + Math.min(18, Number(entry.resonanceScore || 3) * 2.2) + (entry.rarity === "rare" || entry.rarity === "very rare" ? 8 : 0);
const char = characterSymbols[(entry.characters || [])[0]] || "*";
const char = characterSymbol((entry.characters || [])[0]);
const label = labelForEntry(entry, pos, size, occupied);
return `<g class="memory-node${entry.id === state.selectedId ? " selected" : ""}${entry.enabled === false ? " resting" : ""}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})">
<circle r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.42 : 0.92}"></circle>
@@ -3137,7 +3331,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
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."],
character: ["Character View", "Nodes gather around Lima, Aphy, Sensei Chi, Young Z, Future Z, and Z."],
character: ["Character View", "Nodes gather around lima, aphy, sensei chi, young z, future z, and z."],
layer: ["Layer Dive", "Depth zones show the emotional pressure from surface reality to the core."],
flow: ["Discovery Flow", "A possible path from ordinary encounter to patient secret."],
};
@@ -3147,6 +3341,15 @@ HIDDEN_APP_HTML = r"""<!doctype html>
$("mapNote").textContent = `${scopeText} Zoom in or open an island to reveal detail. Drag memories to weave continuation threads.`;
}
function renderValidation() {
const validation = state.meta.validation || {};
const summary = validation.summary || {};
const ok = validation.ok && !summary.unknownCharacterCount && !summary.orphanNodeCount && !summary.staleLinkCount;
$("integrityStatus").innerHTML = ok
? "all memories belong to canonical character territories. no orphan nodes, unknown aliases, or stale relationship links found."
: `needs attention: ${summary.unknownCharacterCount || 0} unknown characters, ${summary.orphanNodeCount || 0} orphan memories, ${summary.staleLinkCount || 0} stale links. ${html(validation.repairPolicy || "")}`;
}
function isWorldVisible(x, y, pad = 0) {
const p = worldToScreen(x, y);
return p.x >= -pad && p.x <= SCREEN.width + pad && p.y >= -pad && p.y <= SCREEN.height + pad;
@@ -3320,7 +3523,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
state.focusIds = null;
state.zoom = 2;
if (name === "lima") {
$("characterFilter").value = "Lima";
$("characterFilter").value = "lima";
$("rarityFilter").value = "";
$("toneFilter").value = "";
$("search").value = "";
@@ -3332,7 +3535,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
$("search").value = "";
}
if (name === "future") {
$("characterFilter").value = "Future Z";
$("characterFilter").value = "future z";
$("rarityFilter").value = "";
$("toneFilter").value = "";
$("search").value = "";
@@ -3444,8 +3647,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
try {
const data = await api("/api/hidden", { method: "POST", body: JSON.stringify({ entries: state.entries }) });
state.entries = data.entries;
state.meta = data;
localStorage.removeItem(draftKey);
setStatus(data.message || "constellation stored safely.");
renderValidation();
render();
} catch (err) {
setStatus(err.message);
@@ -3462,6 +3667,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
state.selectedId = draft.selectedId || "";
}
populateControls();
renderValidation();
setStatus(data.migratedFromJs ? "Existing hidden details became a memory constellation." : "observatory open.");
selectNode(state.selectedId || state.entries[0]?.id);
fitCameraTo({ minX: 250, minY: 220, maxX: WORLD.width - 250, maxY: WORLD.height - 220 }, false);