From 0ef179385d8b982a3a8fedfef546dc23d6d01dd8 Mon Sep 17 00:00:00 2001 From: Zaine Arch Date: Sun, 10 May 2026 15:20:34 +0100 Subject: [PATCH] adding a hidden authoring service support --- authoring_server.py | 961 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 961 insertions(+) diff --git a/authoring_server.py b/authoring_server.py index 029a9de..9ab65de 100755 --- a/authoring_server.py +++ b/authoring_server.py @@ -64,6 +64,9 @@ POSTS_DIR = ROOT / "posts" LIMA_DIR = ROOT / "lima" IMAGE_ASSETS_DIR = ROOT / "assets" / "images" HZONE_ASSETS_DIR = IMAGE_ASSETS_DIR / "hzone" +HIDDEN_DETAILS_JS = ROOT / "assets" / "scripts" / "hidden-details.js" +HIDDEN_CONTENT_JSON = ROOT / "assets" / "content" / "hidden-details.json" +HIDDEN_BACKUP_DIR = ROOT / "backups" / "hidden-details" EXCLUDED_CONTENT_DIR_NAMES = { ".agents", ".codex", @@ -109,6 +112,40 @@ MONTH_NAMES = [ "december", ] +HIDDEN_CONTENT_TYPES = [ + "quote", + "poem", + "hidden dialogue", + "journal entry", + "rare event", + "loading screen message", + "secret interaction", + "hidden tooltip", + "Future Z message", + "Young Z memory fragment", + "Sensei Chi wisdom entry", + "Aphy system message", + "Lima note/message", + "dream sequence", + "terminal log", + "fake error message", + "recurring joke", + "seasonal event", + "weather-based event", + "hover message", + "hidden achievement", + "guestbook entry", + "hidden conversation", + "family layer", + "search toast", + "search route", + "keyboard secret", +] + +HIDDEN_CHARACTERS = ["Lima", "Aphy", "Sensei Chi", "Young Z", "Future Z", "Z"] +HIDDEN_TONES = ["warm", "funny", "nostalgic", "wise", "strange", "soft", "hopeful", "protective", "melancholy"] +HIDDEN_RARITIES = ["common", "uncommon", "rare", "very rare", "seasonal", "timed"] + def slugify(value: str) -> str: slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") @@ -659,6 +696,370 @@ def save_upload(filename: str, payload: bytes, page_path: str = "") -> dict[str, } +def find_js_const_literal(source: str, name: str) -> str: + marker = f"const {name} =" + start = source.find(marker) + if start == -1: + raise ValueError(f"Could not find const {name}.") + value_start = source.find("=", start) + 1 + while value_start < len(source) and source[value_start].isspace(): + value_start += 1 + opener = source[value_start] + pairs = {"[": "]", "{": "}"} + if opener not in pairs: + raise ValueError(f"const {name} is not an array or object.") + closer = pairs[opener] + depth = 0 + in_string = False + escape = False + for index in range(value_start, len(source)): + char = source[index] + if in_string: + if escape: + escape = False + elif char == "\\": + escape = True + elif char == '"': + in_string = False + continue + if char == '"': + in_string = True + elif char == opener: + depth += 1 + elif char == closer: + depth -= 1 + if depth == 0: + return source[value_start:index + 1] + raise ValueError(f"Could not parse const {name}.") + + +def js_literal_to_json(value: str) -> Any: + cleaned = re.sub(r"//.*", "", value) + cleaned = re.sub(r"(/\*.*?\*/)", "", cleaned, flags=re.DOTALL) + cleaned = re.sub(r"([{\[,]\s*)([A-Za-z_$][\w$]*)\s*:", r'\1"\2":', cleaned) + cleaned = re.sub(r",(\s*[\]}])", r"\1", cleaned) + return json.loads(cleaned) + + +def detect_hidden_characters(text: str) -> list[str]: + lowered = text.lower() + found = [] + for character in HIDDEN_CHARACTERS: + if character.lower() in lowered: + found.append(character) + return found + + +def hidden_entry_id(content_type: str, index: int, title: str) -> str: + return f"{slugify(content_type)}-{index + 1:03d}-{slugify(title)[:42]}" + + +def hidden_title(content_type: str, content: str, index: int) -> str: + text = re.sub(r"\s+", " ", content).strip() + if not text: + return f"{content_type.title()} {index + 1}" + return text[:58] + ("..." if len(text) > 58 else "") + + +def make_hidden_entry(content_type: str, content: str, index: int, **extra: Any) -> dict[str, Any]: + now_iso = datetime.now().date().isoformat() + title = str(extra.pop("title", "") or hidden_title(content_type, content, index)) + characters = extra.pop("characters", None) or detect_hidden_characters(f"{title} {content}") + tags = extra.pop("tags", None) or [slugify(character) for character in characters] + entry = { + "id": hidden_entry_id(content_type, index, title), + "type": content_type, + "title": title, + "content": content, + "characters": characters, + "emotionalTone": extra.pop("emotionalTone", "warm"), + "rarity": extra.pop("rarity", "common"), + "triggerConditions": extra.pop("triggerConditions", ""), + "tags": tags, + "category": extra.pop("category", content_type), + "pageLocation": extra.pop("pageLocation", ""), + "familyLayer": extra.pop("familyLayer", ""), + "enabled": extra.pop("enabled", True), + "createdDate": extra.pop("createdDate", now_iso), + "modifiedDate": extra.pop("modifiedDate", now_iso), + "notes": extra.pop("notes", ""), + "audioSettings": extra.pop("audioSettings", ""), + "animationTrigger": extra.pop("animationTrigger", ""), + "cssClassHooks": extra.pop("cssClassHooks", ""), + "chainReferences": extra.pop("chainReferences", []), + "continuationLinks": extra.pop("continuationLinks", []), + } + entry.update(extra) + return entry + + +def migrate_hidden_entries_from_js() -> list[dict[str, Any]]: + source = HIDDEN_DETAILS_JS.read_text(encoding="utf-8") + family_layers = js_literal_to_json(find_js_const_literal(source, "familyLayers")) + details = js_literal_to_json(find_js_const_literal(source, "details")) + poems = js_literal_to_json(find_js_const_literal(source, "poems")) + greetings = js_literal_to_json(find_js_const_literal(source, "greetings")) + night_messages = js_literal_to_json(find_js_const_literal(source, "nightMessages")) + lore = js_literal_to_json(find_js_const_literal(source, "lore")) + search_toasts = js_literal_to_json(find_js_const_literal(source, "SEARCH_TOASTS")) + search_routes = js_literal_to_json(find_js_const_literal(source, "SEARCH_ROUTES")) + keyboard_secrets = js_literal_to_json(find_js_const_literal(source, "KEYBOARD_SECRETS")) + long_keyboard_secrets = js_literal_to_json(find_js_const_literal(source, "LONG_KEYBOARD_SECRETS")) + + entries: list[dict[str, Any]] = [] + add = entries.append + for index, item in enumerate(family_layers): + add(make_hidden_entry("family layer", item, index, familyLayer=str(index), category="Family Layer Index")) + for index, item in enumerate(details): + add(make_hidden_entry("hidden tooltip", item, index, category="footer and whispers")) + for index, item in enumerate(poems): + add(make_hidden_entry("poem", item, index, category="poems", emotionalTone="soft")) + for index, item in enumerate(greetings): + add(make_hidden_entry("loading screen message", item, index, category="homepage greeting")) + for index, item in enumerate(night_messages): + add(make_hidden_entry("rare event", item, index, category="late night", rarity="timed", triggerConditions="hour >= 22 or hour < 5")) + for key, content_type in [ + ("quotes", "quote"), + ("journals", "journal entry"), + ("warnings", "fake error message"), + ("dreams", "dream sequence"), + ("cassettes", "terminal log"), + ("fakeUsers", "guestbook entry"), + ("homepageTakeovers", "rare event"), + ]: + for index, item in enumerate(lore.get(key, [])): + add(make_hidden_entry(content_type, item, index, category=key, rarity="rare" if key in {"warnings", "dreams", "homepageTakeovers"} else "common")) + for index, item in enumerate(lore.get("conversations", [])): + title = " / ".join(str(part) for part in item[::2]) or f"Conversation {index + 1}" + add(make_hidden_entry("hidden conversation", "\n".join(str(part) for part in item), index, title=title, category="conversations", dialogue=item)) + for index, (season, item) in enumerate((lore.get("seasonal", {}) or {}).items()): + add(make_hidden_entry("seasonal event", item, index, title=f"{season.title()} note", category="seasonal", rarity="seasonal", triggerConditions=f"season is {season}", season=season)) + for index, item in enumerate(lore.get("roomLinks", [])): + href, label = item + add(make_hidden_entry("secret interaction", label, index, title=label, category="room links", pageLocation=href, triggerConditions="secret link injected into page")) + for index, (query, message) in enumerate(search_toasts.items()): + add(make_hidden_entry("search toast", message, index, title=f"Search: {query}", category="search", triggerConditions=query, query=query)) + 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 "") + 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 + + +def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None = None) -> dict[str, Any]: + 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") + content_type = str(entry.get("type") or "quote").strip() + if content_type not in HIDDEN_CONTENT_TYPES: + raise ValueError(f"Unsupported hidden content type: {content_type}") + if not title: + raise ValueError("Every hidden entry needs a title.") + if not content and content_type not in {"search route"}: + raise ValueError(f"{title} needs content.") + entry["id"] = slugify(str(entry.get("id") or title)) + 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()] + entry["emotionalTone"] = str(entry.get("emotionalTone") or "warm") + entry["rarity"] = str(entry.get("rarity") or "common") + entry["triggerConditions"] = str(entry.get("triggerConditions") or "") + entry["tags"] = normalise_tags(entry.get("tags", [])) + entry["category"] = str(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["audioSettings"] = str(entry.get("audioSettings") or "") + entry["animationTrigger"] = str(entry.get("animationTrigger") or "") + entry["cssClassHooks"] = str(entry.get("cssClassHooks") or "") + entry["chainReferences"] = [str(item).strip() for item in entry.get("chainReferences", []) if str(item).strip()] + entry["continuationLinks"] = [str(item).strip() for item in entry.get("continuationLinks", []) if str(item).strip()] + return entry + + +def load_hidden_store() -> dict[str, Any]: + migrated = False + if HIDDEN_CONTENT_JSON.exists(): + data = json.loads(HIDDEN_CONTENT_JSON.read_text(encoding="utf-8")) + entries = data.get("entries", []) + else: + entries = migrate_hidden_entries_from_js() + migrated = True + data = { + "schemaVersion": 1, + "generatedFrom": "assets/scripts/hidden-details.js", + "generatedAt": datetime.now().isoformat(timespec="seconds"), + "entries": entries, + } + normalized = [normalize_hidden_entry(entry, entry) for entry in entries] + return { + "schemaVersion": 1, + "source": HIDDEN_DETAILS_JS.relative_to(ROOT).as_posix(), + "contentPath": HIDDEN_CONTENT_JSON.relative_to(ROOT).as_posix(), + "migratedFromJs": migrated, + "types": HIDDEN_CONTENT_TYPES, + "characters": HIDDEN_CHARACTERS, + "tones": HIDDEN_TONES, + "rarities": HIDDEN_RARITIES, + "entries": normalized, + "recommendations": hidden_architecture_recommendations(), + } + + +def hidden_architecture_recommendations() -> dict[str, Any]: + return { + "storage": "Use assets/content/hidden-details.json as the friendly source of truth and regenerate the editable constants in assets/scripts/hidden-details.js.", + "backups": "Every save writes timestamped backups for both JSON and JS under backups/hidden-details/.", + "versioning": "Commit the JSON and generated JS together so the live site and authoring history stay aligned.", + "collaboration": "For simultaneous editing, add per-entry modified timestamps and resolve conflicts by entry id before writing.", + "scalability": "The schema is entry-based, so future rooms, arcs, audio cues, and relationship graphs can be added without rewriting the editor.", + } + + +def hidden_entries_by_type(entries: list[dict[str, Any]], content_type: str) -> list[dict[str, Any]]: + return [entry for entry in entries if entry.get("enabled", True) and entry.get("type") == content_type] + + +def hidden_contents(entries: list[dict[str, Any]], content_type: str) -> list[str]: + return [str(entry.get("content", "")) for entry in hidden_entries_by_type(entries, content_type)] + + +def generated_hidden_content_block(entries: list[dict[str, Any]]) -> str: + family_layers = hidden_contents(entries, "family layer") + details = hidden_contents(entries, "hidden tooltip") + hidden_contents(entries, "hover message") + poems = hidden_contents(entries, "poem") + greetings = hidden_contents(entries, "loading screen message") + night_messages = [ + 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") + lore = { + "quotes": quote_like, + "conversations": [ + entry.get("dialogue") if isinstance(entry.get("dialogue"), list) else [line for line in str(entry.get("content", "")).splitlines() if line.strip()] + for entry in hidden_entries_by_type(entries, "hidden conversation") + ], + "journals": hidden_contents(entries, "journal entry"), + "warnings": hidden_contents(entries, "fake error message"), + "dreams": hidden_contents(entries, "dream sequence"), + "cassettes": hidden_contents(entries, "terminal log"), + "fakeUsers": hidden_contents(entries, "guestbook entry"), + "seasonal": { + str(entry.get("season") or entry.get("triggerConditions") or entry.get("title", "")).lower().replace("season is ", ""): entry.get("content", "") + for entry in hidden_entries_by_type(entries, "seasonal event") + }, + "homepageTakeovers": [ + entry["content"] for entry in hidden_entries_by_type(entries, "rare event") + if "homepage" in f"{entry.get('category', '')} {entry.get('triggerConditions', '')}".lower() + ], + "roomLinks": [ + [entry.get("pageLocation", ""), entry.get("content", "") or entry.get("title", "")] + for entry in hidden_entries_by_type(entries, "secret interaction") + if entry.get("pageLocation") + ], + } + search_toasts = { + entry.get("query") or entry.get("triggerConditions") or entry.get("title", ""): entry.get("content", "") + for entry in hidden_entries_by_type(entries, "search toast") + } + search_routes = { + entry.get("query") or entry.get("triggerConditions") or entry.get("title", ""): entry.get("pageLocation") or entry.get("content", "") + for entry in hidden_entries_by_type(entries, "search route") + } + keyboard_secrets = [] + long_keyboard_secrets = [] + for entry in hidden_entries_by_type(entries, "keyboard secret"): + secret = dict(entry.get("keyboard") or {}) + 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": + secret["song"] = True + else: + secret["message"] = entry.get("content", "") + target = long_keyboard_secrets if len(secret["phrase"]) > 8 or " " in secret["phrase"] else keyboard_secrets + target.append(secret) + + def js_const(name: str, value: Any) -> str: + return f" const {name} = {json.dumps(value, ensure_ascii=False, indent=4)};\n" + + return ( + " // -----------------------------\n" + " // EDITABLE CONTENT\n" + " // -----------------------------\n" + " // Generated by the hidden narrative authoring page.\n" + " // Friendly source of truth: assets/content/hidden-details.json\n\n" + f"{js_const('familyLayers', family_layers)}\n" + f"{js_const('details', details)}\n" + f"{js_const('poems', poems)}\n" + f"{js_const('greetings', greetings)}\n" + f"{js_const('nightMessages', night_messages)}\n" + f"{js_const('lore', lore)}\n" + " // Exact search text -> toast message.\n" + f"{js_const('SEARCH_TOASTS', search_toasts)}\n" + " // Exact search text -> hidden page route.\n" + f"{js_const('SEARCH_ROUTES', search_routes)}\n" + " // Short typed phrases. Stored in sessionStorage as a rolling key chain.\n" + f"{js_const('KEYBOARD_SECRETS', keyboard_secrets)}\n" + " // Longer typed phrases and character names.\n" + f"{js_const('LONG_KEYBOARD_SECRETS', long_keyboard_secrets)}\n" + ) + + +def backup_hidden_file(path: Path, stamp: str) -> None: + if not path.exists(): + return + HIDDEN_BACKUP_DIR.mkdir(parents=True, exist_ok=True) + target = HIDDEN_BACKUP_DIR / f"{stamp}-{path.name}" + target.write_text(path.read_text(encoding="utf-8"), encoding="utf-8") + + +def replace_hidden_editable_block(source: str, entries: list[dict[str, Any]]) -> str: + start = source.find(" // -----------------------------\n // EDITABLE CONTENT") + end = source.find(" // -----------------------------\n // STATE HELPERS") + if start == -1 or end == -1 or end <= start: + raise ValueError("Could not find editable hidden content block.") + return source[:start] + generated_hidden_content_block(entries) + "\n" + source[end:] + + +def save_hidden_store(payload: dict[str, Any]) -> dict[str, Any]: + current = {entry["id"]: entry for entry in load_hidden_store()["entries"]} + entries = [normalize_hidden_entry(entry, current.get(str(entry.get("id", "")))) for entry in payload.get("entries", [])] + ids = [entry["id"] for entry in entries] + if len(ids) != len(set(ids)): + raise ValueError("Entry ids must be unique.") + stamp = datetime.now().strftime("%Y%m%d-%H%M%S") + backup_hidden_file(HIDDEN_DETAILS_JS, stamp) + backup_hidden_file(HIDDEN_CONTENT_JSON, stamp) + HIDDEN_CONTENT_JSON.parent.mkdir(parents=True, exist_ok=True) + HIDDEN_CONTENT_JSON.write_text( + json.dumps( + { + "schemaVersion": 1, + "generatedAt": datetime.now().isoformat(timespec="seconds"), + "entries": entries, + }, + ensure_ascii=False, + indent=2, + ) + "\n", + encoding="utf-8", + ) + source = HIDDEN_DETAILS_JS.read_text(encoding="utf-8") + HIDDEN_DETAILS_JS.write_text(replace_hidden_editable_block(source, entries), encoding="utf-8") + saved = load_hidden_store() + saved["message"] = "stored safely. future-you will probably smile at this one." + saved["backupStamp"] = stamp + return saved + + def parse_upload_form(content_type: str, body: bytes) -> tuple[str, bytes, str]: if not content_type.lower().startswith("multipart/form-data"): raise ValueError("Uploads must use multipart/form-data.") @@ -961,6 +1362,7 @@ APP_HTML = r""" author.zainezq.com + Hidden narrative desk
@@ -1779,6 +2181,543 @@ APP_HTML = r""" """ +HIDDEN_APP_HTML = r""" + + + + + Hidden Narrative Desk + + + +
+ + +
+
+
+

New hidden entry

+
Forms, not raw JavaScript.
+
+
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+
+
+

Live Preview

+
+
+
+ + +
+ + + + +""" + + class Handler(BaseHTTPRequestHandler): server_version = "OrgAuthoring/1.0" @@ -1804,6 +2743,14 @@ class Handler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(body) return + if parsed.path == "/hidden": + body = HIDDEN_APP_HTML.encode("utf-8") + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return if parsed.path == "/api/pages": try: self.send_json(list_pages()) @@ -1827,6 +2774,12 @@ class Handler(BaseHTTPRequestHandler): if parsed.path == "/api/build": self.send_json(BUILD_QUEUE.snapshot()) return + if parsed.path == "/api/hidden": + try: + self.send_json(load_hidden_store()) + except Exception as exc: + self.send_json({"error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR) + return self.send_error(HTTPStatus.NOT_FOUND) def do_POST(self) -> None: @@ -1841,6 +2794,14 @@ class Handler(BaseHTTPRequestHandler): except Exception as exc: self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) return + if self.path == "/api/hidden": + try: + length = int(self.headers.get("Content-Length", "0")) + data = json.loads(self.rfile.read(length).decode("utf-8")) + self.send_json(save_hidden_store(data)) + except Exception as exc: + self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) + return if self.path != "/api/page": self.send_error(HTTPStatus.NOT_FOUND) return