@@ -2017,9 +2083,11 @@ HIDDEN_APP_HTML = r"""
const draftKey = "hiddenNarrativeObservatoryDraft:v1";
const WORLD = { width: 4600, height: 3300, cx: 2300, cy: 1650 };
const SCREEN = { width: 1000, height: 760 };
- const state = { entries: [], stories: [], meta: {}, mode: "graph", editorMode: "memory", selectedId: "", selectedStoryId: "", editingSectionId: "", readerIndex: 0, undo: [], redo: [], draggingId: "", positions: new Map(), clusterPositions: new Map(), zoom: 0, focusCluster: "", focusKind: "", focusIds: null, focusLabel: "", history: [], explorationUndo: [], explorationRedo: [], camera: { x: 0, y: 0, k: 0.22 }, pan: null, drag: null, pendingDrag: null, renderTimer: 0 };
+ const state = { entries: [], stories: [], lifeArcs: [], meta: {}, mode: "graph", editorMode: "memory", selectedId: "", selectedStoryId: "", selectedArcId: "", selectedChapterId: "", editingSectionId: "", readerIndex: 0, undo: [], redo: [], draggingId: "", positions: new Map(), clusterPositions: new Map(), zoom: 0, focusCluster: "", focusKind: "", focusIds: null, focusLabel: "", history: [], explorationUndo: [], explorationRedo: [], camera: { x: 0, y: 0, k: 0.22 }, pan: null, drag: null, pendingDrag: null, renderTimer: 0 };
const fields = ["title","type","contentClass","familyLayer","characters","tone","rarity","discoveryDifficulty","mysteryLevel","resonanceScore","pageLocation","triggerConditions","content","surfaces","observatoryRole","canonicalUse","symbols","narrativeArcs","tags","emotionalRole","continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries","enabled","cssClassHooks","audioSettings","animationTrigger","notes"];
- const storyFields = ["storyTitle","storyDescription","storyTone","storyDiscovery","storyCharacters","storySymbols","storyMarkers","storyLayers","storyUnlock","storyHidden"];
+ const storyFields = ["storyTitle","storyDescription","storyTone","storyDiscovery","storyCharacters","storySymbols","storyMarkers","storyLayers","storyUnlock","storyHidden"];
+ const arcFields = ["arcTitle","arcDomain","arcVisibility","arcTone","arcThemes","arcSummary","arcOrgWeb"];
+ const chapterFields = ["chapterTitle","chapterTimeframe","chapterTone","chapterPurpose","chapterStoryDraft"];
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 appearanceLabels = {
@@ -2052,17 +2120,27 @@ HIDDEN_APP_HTML = r"""
function selectedOptions(id) {
return Array.from($(id).selectedOptions || []).map((option) => option.value).filter(Boolean);
}
- function storyById(id = state.selectedStoryId) {
- return state.stories.find((story) => story.id === id);
- }
- function storiesForEntry(entryId) {
- return state.stories.filter((story) => (story.nodes || []).includes(entryId));
- }
- function storyItems(story) {
+ function storyById(id = state.selectedStoryId) {
+ return state.stories.find((story) => story.id === id);
+ }
+ function arcById(id = state.selectedArcId) {
+ return state.lifeArcs.find((arc) => arc.id === id);
+ }
+ function chapterById(id = state.selectedChapterId) {
+ const arc = arcById();
+ return (arc?.chapters || []).find((chapter) => chapter.id === id);
+ }
+ function storiesForEntry(entryId) {
+ return state.stories.filter((story) => (story.nodes || []).includes(entryId));
+ }
+ function storyItems(story) {
if (!story) return [];
if (Array.isArray(story.items) && story.items.length) return story.items;
- return (story.nodes || []).map((id) => ({ kind: "memory", id }));
- }
+ return (story.nodes || []).map((id) => ({ kind: "memory", id }));
+ }
+ function arcMomentCount(arc) {
+ return (arc?.chapters || []).reduce((total, chapter) => total + (chapter.moments || []).length, 0);
+ }
function entryById(id) {
return state.entries.find((entry) => entry.id === id);
}
@@ -2248,20 +2326,24 @@ HIDDEN_APP_HTML = r"""
window.clearTimeout(state.renderTimer);
state.renderTimer = window.setTimeout(render, 90);
}
- function snapshot() {
- state.undo.push(JSON.stringify({ entries: state.entries, stories: state.stories, selectedStoryId: state.selectedStoryId }));
+ function snapshot() {
+ state.undo.push(JSON.stringify({ entries: state.entries, stories: state.stories, lifeArcs: state.lifeArcs, selectedStoryId: state.selectedStoryId, selectedArcId: state.selectedArcId, selectedChapterId: state.selectedChapterId }));
state.undo = state.undo.slice(-60);
state.redo = [];
}
function restore(serialized) {
- const data = JSON.parse(serialized);
- state.entries = data.entries || data || [];
- state.stories = data.stories || state.stories || [];
- state.selectedStoryId = data.selectedStoryId || state.selectedStoryId || state.stories[0]?.id || "";
- rememberDraft();
- render();
- selectNode(state.selectedId || state.entries[0]?.id);
- fillStoryForm(storyById());
+ const data = JSON.parse(serialized);
+ state.entries = data.entries || data || [];
+ state.stories = data.stories || state.stories || [];
+ state.lifeArcs = data.lifeArcs || state.lifeArcs || [];
+ state.selectedStoryId = data.selectedStoryId || state.selectedStoryId || state.stories[0]?.id || "";
+ state.selectedArcId = data.selectedArcId || state.selectedArcId || state.lifeArcs[0]?.id || "";
+ state.selectedChapterId = data.selectedChapterId || state.selectedChapterId || arcById()?.chapters?.[0]?.id || "";
+ rememberDraft();
+ render();
+ selectNode(state.selectedId || state.entries[0]?.id);
+ fillStoryForm(storyById());
+ fillArcForm(arcById());
}
function filterState() {
return {
@@ -2325,10 +2407,10 @@ HIDDEN_APP_HTML = r"""
applyExploration(previous);
setStatus("one exploration step undone.");
}
- function rememberDraft() {
- localStorage.setItem(draftKey, JSON.stringify({ entries: state.entries, stories: state.stories, selectedId: state.selectedId, selectedStoryId: state.selectedStoryId, savedAt: Date.now() }));
- $("autosave").textContent = "local draft kept warm.";
- }
+ function rememberDraft() {
+ localStorage.setItem(draftKey, JSON.stringify({ entries: state.entries, stories: state.stories, lifeArcs: state.lifeArcs, selectedId: state.selectedId, selectedStoryId: state.selectedStoryId, selectedArcId: state.selectedArcId, selectedChapterId: state.selectedChapterId, savedAt: Date.now() }));
+ $("autosave").textContent = "local draft kept warm.";
+ }
function depth(entry) {
const value = String(entry.familyLayer || "").match(/[0-5]/);
if (value) return Number(value[0]);
@@ -2417,16 +2499,219 @@ HIDDEN_APP_HTML = r"""
markers: selectedOptions("storyMarkers"),
};
}
- function applyStoryFormToState() {
- if (!state.selectedStoryId) return;
+ function applyStoryFormToState() {
+ if (!state.selectedStoryId) return;
const index = state.stories.findIndex((story) => story.id === state.selectedStoryId);
if (index < 0) return;
state.stories[index] = storyFromForm();
state.selectedStoryId = state.stories[index].id;
rememberDraft();
- renderStoryBuilder();
- render();
- }
+ renderStoryBuilder();
+ render();
+ }
+ function arcFromForm() {
+ const current = arcById() || {};
+ return {
+ ...current,
+ id: current.id || `arc-${slugify($("arcTitle").value || "life-arc")}-${Date.now()}`,
+ title: $("arcTitle").value.trim() || "Untitled life arc",
+ domain: $("arcDomain").value,
+ summary: $("arcSummary").value,
+ tone: $("arcTone").value || "warm",
+ themes: splitList($("arcThemes").value),
+ orgWebConnection: $("arcOrgWeb").value,
+ visibility: $("arcVisibility").value || "private",
+ chapters: current.chapters || [],
+ };
+ }
+ function chapterFromForm() {
+ const current = chapterById() || {};
+ return {
+ ...current,
+ id: current.id || `chapter-${slugify($("chapterTitle").value || "chapter")}-${Date.now()}`,
+ title: $("chapterTitle").value.trim() || "Untitled chapter",
+ timeframe: $("chapterTimeframe").value,
+ tone: $("chapterTone").value || arcById()?.tone || "warm",
+ purpose: $("chapterPurpose").value,
+ storyDraft: $("chapterStoryDraft").value,
+ linkedStoryId: current.linkedStoryId || "",
+ moments: current.moments || [],
+ };
+ }
+ function applyArcFormToState() {
+ if (!state.selectedArcId) return;
+ const index = state.lifeArcs.findIndex((arc) => arc.id === state.selectedArcId);
+ if (index < 0) return;
+ state.lifeArcs[index] = arcFromForm();
+ state.selectedArcId = state.lifeArcs[index].id;
+ rememberDraft();
+ renderArcStudio();
+ }
+ function applyChapterFormToState() {
+ const arc = arcById();
+ if (!arc || !state.selectedChapterId) return;
+ const index = (arc.chapters || []).findIndex((chapter) => chapter.id === state.selectedChapterId);
+ if (index < 0) return;
+ arc.chapters[index] = chapterFromForm();
+ state.selectedChapterId = arc.chapters[index].id;
+ rememberDraft();
+ renderArcStudio();
+ }
+ function fillArcForm(arc) {
+ if (!arc) {
+ arcFields.concat(chapterFields).forEach((id) => { if ($(id)) $(id).value = ""; });
+ $("arcList").innerHTML = "";
+ $("chapterList").innerHTML = "";
+ $("momentList").innerHTML = "";
+ return;
+ }
+ state.selectedArcId = arc.id;
+ $("arcTitle").value = arc.title || "";
+ $("arcDomain").value = arc.domain || "";
+ $("arcSummary").value = arc.summary || "";
+ $("arcTone").value = arc.tone || "warm";
+ $("arcThemes").value = (arc.themes || []).join(", ");
+ $("arcOrgWeb").value = arc.orgWebConnection || "";
+ $("arcVisibility").value = arc.visibility || "private";
+ if (!state.selectedChapterId || !(arc.chapters || []).some((chapter) => chapter.id === state.selectedChapterId)) {
+ state.selectedChapterId = arc.chapters?.[0]?.id || "";
+ }
+ fillChapterForm(chapterById());
+ renderArcStudio();
+ }
+ function fillChapterForm(chapter) {
+ if (!chapter) {
+ chapterFields.forEach((id) => { if ($(id)) $(id).value = ""; });
+ $("momentList").innerHTML = "
Create a chapter before logging moments.";
+ return;
+ }
+ state.selectedChapterId = chapter.id;
+ $("chapterTitle").value = chapter.title || "";
+ $("chapterTimeframe").value = chapter.timeframe || "";
+ $("chapterTone").value = chapter.tone || arcById()?.tone || "warm";
+ $("chapterPurpose").value = chapter.purpose || "";
+ $("chapterStoryDraft").value = chapter.storyDraft || "";
+ renderArcStudio();
+ }
+ function newArc() {
+ snapshot();
+ const arc = {
+ id: `arc-life-arc-${Date.now()}`,
+ title: "Untitled life arc",
+ domain: "",
+ summary: "",
+ tone: "warm",
+ themes: [],
+ orgWebConnection: "",
+ visibility: "private",
+ chapters: [],
+ createdDate: new Date().toISOString().slice(0, 10),
+ modifiedDate: new Date().toISOString().slice(0, 10),
+ };
+ state.lifeArcs.unshift(arc);
+ state.selectedArcId = arc.id;
+ state.selectedChapterId = "";
+ fillArcForm(arc);
+ rememberDraft();
+ }
+ function deleteArc() {
+ const arc = arcById();
+ if (!arc || !confirm(`Let "${arc.title}" rest outside the arc studio?`)) return;
+ snapshot();
+ state.lifeArcs = state.lifeArcs.filter((item) => item.id !== arc.id);
+ state.selectedArcId = state.lifeArcs[0]?.id || "";
+ state.selectedChapterId = arcById()?.chapters?.[0]?.id || "";
+ fillArcForm(arcById());
+ rememberDraft();
+ }
+ function newChapter() {
+ if (!arcById()) newArc();
+ const arc = arcById();
+ if (!arc) return;
+ snapshot();
+ const chapter = {
+ id: `chapter-${slugify(arc.title)}-${Date.now()}`,
+ title: "Untitled chapter",
+ timeframe: "",
+ tone: arc.tone || "warm",
+ purpose: "",
+ storyDraft: "",
+ linkedStoryId: "",
+ moments: [],
+ };
+ arc.chapters = [chapter, ...(arc.chapters || [])];
+ state.selectedChapterId = chapter.id;
+ fillChapterForm(chapter);
+ rememberDraft();
+ }
+ function deleteChapter() {
+ const arc = arcById();
+ const chapter = chapterById();
+ if (!arc || !chapter || !confirm(`Remove chapter "${chapter.title}" from this arc?`)) return;
+ snapshot();
+ arc.chapters = (arc.chapters || []).filter((item) => item.id !== chapter.id);
+ state.selectedChapterId = arc.chapters[0]?.id || "";
+ fillChapterForm(chapterById());
+ rememberDraft();
+ }
+ function addMoment() {
+ const chapter = chapterById();
+ if (!chapter) return;
+ const text = $("momentText").value.trim();
+ const lesson = $("momentLesson").value.trim();
+ if (!text && !lesson) {
+ setStatus("a moment needs either an event or a lesson.");
+ return;
+ }
+ snapshot();
+ chapter.moments = [{
+ id: `moment-${Date.now()}`,
+ date: $("momentDate").value,
+ kind: $("momentKind").value || "moment",
+ text,
+ feeling: $("momentFeeling").value,
+ lesson,
+ metric: $("momentMetric").value,
+ sourcePointer: $("momentSource").value,
+ visibility: $("momentVisibility").value || "private",
+ linkedStoryId: $("momentStoryLink").value,
+ linkedEntryId: $("momentEntryLink").value,
+ }, ...(chapter.moments || [])];
+ ["momentText","momentFeeling","momentMetric","momentLesson","momentSource"].forEach((id) => $(id).value = "");
+ renderArcStudio();
+ rememberDraft();
+ }
+ function deleteMoment(momentId) {
+ const chapter = chapterById();
+ if (!chapter) return;
+ snapshot();
+ chapter.moments = (chapter.moments || []).filter((moment) => moment.id !== momentId);
+ renderArcStudio();
+ rememberDraft();
+ }
+ function renderArcStudio() {
+ const arc = arcById();
+ const chapter = chapterById();
+ $("arcList").innerHTML = state.lifeArcs.length
+ ? state.lifeArcs.map((item) => `
`).join("")
+ : "
No life arcs yet.";
+ $("arcList").querySelectorAll("[data-arc]").forEach((button) => button.addEventListener("click", () => {
+ state.selectedArcId = button.dataset.arc;
+ state.selectedChapterId = arcById()?.chapters?.[0]?.id || "";
+ fillArcForm(arcById());
+ }));
+ $("chapterList").innerHTML = arc?.chapters?.length
+ ? arc.chapters.map((item) => `
`).join("")
+ : "
No chapters in this arc yet.";
+ $("chapterList").querySelectorAll("[data-chapter]").forEach((button) => button.addEventListener("click", () => {
+ state.selectedChapterId = button.dataset.chapter;
+ fillChapterForm(chapterById());
+ }));
+ $("momentList").innerHTML = chapter?.moments?.length
+ ? chapter.moments.map((moment) => `
${html(moment.date || moment.kind || "moment")}${html(moment.text || moment.lesson)}${[moment.feeling, moment.metric, moment.sourcePointer, moment.visibility].filter(Boolean).map(html).join(" / ")}
`).join("")
+ : "
Log compact moments here, then turn selected material into the chapter story draft.";
+ $("momentList").querySelectorAll("[data-delete-moment]").forEach((button) => button.addEventListener("click", () => deleteMoment(button.dataset.deleteMoment)));
+ }
function fillStoryForm(story) {
if (!story) {
$("storyCover").innerHTML = "
Create a story to shape a route through the observatory.";
@@ -3902,9 +4187,11 @@ HIDDEN_APP_HTML = r"""
const opt = (value, label = value) => `
`;
$("type").innerHTML = state.meta.types.map((value) => opt(value)).join("");
$("contentClass").innerHTML = (state.meta.contentClasses || ["fragment","memory"]).map((value) => opt(value)).join("");
- $("tone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
- $("storyTone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
- $("storyDiscovery").innerHTML = (state.meta.discoveryStyles || ["gradual"]).map((value) => opt(value)).join("");
+ $("tone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
+ $("storyTone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
+ $("arcTone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
+ $("chapterTone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
+ $("storyDiscovery").innerHTML = (state.meta.discoveryStyles || ["gradual"]).map((value) => opt(value)).join("");
$("storyMarkers").innerHTML = (state.meta.storyMarkers || []).map((value) => opt(value)).join("");
$("rarity").innerHTML = state.meta.rarities.map((value) => opt(value)).join("");
$("layerFilter").innerHTML = opt("", "All depths") + (state.meta.layers || []).map((layer) => opt(layer.id, `Layer ${layer.id} - ${layer.name}`)).join("");
@@ -3918,7 +4205,7 @@ HIDDEN_APP_HTML = r"""
const territory = config.observatory?.territory || "memory territory";
return `
`;
}).join("");
- $("characterAtlas").querySelectorAll("[data-character]").forEach((button) => button.addEventListener("click", () => {
+ $("characterAtlas").querySelectorAll("[data-character]").forEach((button) => button.addEventListener("click", () => {
snapshotExploration(`character territory: ${button.dataset.character}`);
$("characterFilter").value = button.dataset.character;
state.mode = "character";
@@ -3928,22 +4215,34 @@ HIDDEN_APP_HTML = r"""
$("modebar").querySelectorAll("button").forEach((item) => item.classList.toggle("active", item.dataset.mode === state.mode));
render();
window.setTimeout(() => fitCameraTo(boundsForEntries(visibleScope().entries), true), 0);
- }));
- renderStoryBuilder();
- }
- async function save() {
- applyFormToState();
- applyStoryFormToState();
- setStatus("Saving the constellation.");
- try {
- const data = await api("/api/hidden", { method: "POST", body: JSON.stringify({ entries: state.entries, stories: state.stories }) });
- state.entries = data.entries;
- state.stories = data.stories || [];
- state.meta = data;
+ }));
+ refreshArcLinkOptions();
+ renderStoryBuilder();
+ renderArcStudio();
+ }
+ function refreshArcLinkOptions() {
+ const opt = (value, label = value) => `
`;
+ $("momentStoryLink").innerHTML = opt("", "No story link") + state.stories.map((story) => opt(story.id, story.title)).join("");
+ $("momentEntryLink").innerHTML = opt("", "No memory link") + state.entries.map((entry) => opt(entry.id, entry.title)).join("");
+ }
+ async function save() {
+ applyFormToState();
+ applyStoryFormToState();
+ applyArcFormToState();
+ applyChapterFormToState();
+ setStatus("Saving the constellation.");
+ try {
+ const data = await api("/api/hidden", { method: "POST", body: JSON.stringify({ entries: state.entries, stories: state.stories, lifeArcs: state.lifeArcs }) });
+ state.entries = data.entries;
+ state.stories = data.stories || [];
+ state.lifeArcs = data.lifeArcs || [];
+ state.meta = data;
localStorage.removeItem(draftKey);
setStatus(data.queuedBuild ? "constellation stored safely. asset republish queued." : (data.message || "constellation stored safely."));
- renderValidation();
- render();
+ renderValidation();
+ refreshArcLinkOptions();
+ renderArcStudio();
+ render();
} catch (err) {
setStatus(err.message);
}
@@ -3952,21 +4251,28 @@ HIDDEN_APP_HTML = r"""
try {
const data = await api("/api/hidden");
state.meta = data;
- state.entries = data.entries || [];
- state.stories = data.stories || [];
- const draft = JSON.parse(localStorage.getItem(draftKey) || "null");
- if (draft?.entries?.length && confirm("A local constellation draft exists. Restore it?")) {
- state.entries = draft.entries;
- state.stories = draft.stories || state.stories;
- state.selectedId = draft.selectedId || "";
- state.selectedStoryId = draft.selectedStoryId || "";
- }
+ state.entries = data.entries || [];
+ state.stories = data.stories || [];
+ state.lifeArcs = data.lifeArcs || [];
+ const draft = JSON.parse(localStorage.getItem(draftKey) || "null");
+ if ((draft?.entries?.length || draft?.stories?.length || draft?.lifeArcs?.length) && confirm("A local constellation draft exists. Restore it?")) {
+ state.entries = draft.entries;
+ state.stories = draft.stories || state.stories;
+ state.lifeArcs = draft.lifeArcs || state.lifeArcs;
+ state.selectedId = draft.selectedId || "";
+ state.selectedStoryId = draft.selectedStoryId || "";
+ state.selectedArcId = draft.selectedArcId || "";
+ state.selectedChapterId = draft.selectedChapterId || "";
+ }
populateControls();
renderValidation();
- state.selectedStoryId = state.selectedStoryId || state.stories[0]?.id || "";
- setStatus(data.migratedRelationshipsToStories ? "legacy relationships were gathered into calmer story routes." : data.migratedFromJs ? "Existing hidden details became a memory constellation." : "observatory open.");
- selectNode(state.selectedId || state.entries[0]?.id);
- fillStoryForm(storyById());
+ state.selectedStoryId = state.selectedStoryId || state.stories[0]?.id || "";
+ state.selectedArcId = state.selectedArcId || state.lifeArcs[0]?.id || "";
+ state.selectedChapterId = state.selectedChapterId || arcById()?.chapters?.[0]?.id || "";
+ setStatus(data.migratedRelationshipsToStories ? "legacy relationships were gathered into calmer story routes." : data.migratedFromJs ? "Existing hidden details became a memory constellation." : "observatory open.");
+ selectNode(state.selectedId || state.entries[0]?.id);
+ fillStoryForm(storyById());
+ fillArcForm(arcById());
fitCameraTo({ minX: 250, minY: 220, maxX: WORLD.width - 250, maxY: WORLD.height - 220 }, false);
render();
} catch (err) {
@@ -4068,10 +4374,18 @@ HIDDEN_APP_HTML = r"""
applyTypeDefaults();
applyFormToState();
});
- storyFields.forEach((id) => {
- $(id).addEventListener("input", applyStoryFormToState);
- $(id).addEventListener("change", () => { snapshot(); applyStoryFormToState(); });
- });
+ storyFields.forEach((id) => {
+ $(id).addEventListener("input", applyStoryFormToState);
+ $(id).addEventListener("change", () => { snapshot(); applyStoryFormToState(); });
+ });
+ arcFields.forEach((id) => {
+ $(id).addEventListener("input", applyArcFormToState);
+ $(id).addEventListener("change", () => { snapshot(); applyArcFormToState(); });
+ });
+ chapterFields.forEach((id) => {
+ $(id).addEventListener("input", applyChapterFormToState);
+ $(id).addEventListener("change", () => { snapshot(); applyChapterFormToState(); });
+ });
let filterSnapshotTimer = 0;
["search","layerFilter","characterFilter","toneFilter","rarityFilter"].forEach((id) => {
$(id).addEventListener("focus", () => {
@@ -4092,7 +4406,12 @@ HIDDEN_APP_HTML = r"""
});
});
$("newBtn").onclick = () => newEntry();
- $("newStoryBtn").onclick = () => newStory();
+ $("newStoryBtn").onclick = () => newStory();
+ $("newArcBtn").onclick = newArc;
+ $("newChapterBtn").onclick = newChapter;
+ $("deleteArcBtn").onclick = deleteArc;
+ $("deleteChapterBtn").onclick = deleteChapter;
+ $("addMomentBtn").onclick = addMoment;
$("storyFromSelectionBtn").onclick = () => newStory(state.selectedId);
$("addSectionBtn").onclick = addStorySection;
$("saveSectionBtn").onclick = saveStorySection;
@@ -4113,8 +4432,8 @@ HIDDEN_APP_HTML = r"""
$("readerNextBtn").onclick = () => stepStoryReader(1);
$("duplicateBtn").onclick = duplicateEntry;
$("deleteBtn").onclick = deleteEntry;
- $("undoBtn").onclick = () => { if (!state.undo.length) return; state.redo.push(JSON.stringify({ entries: state.entries, stories: state.stories, selectedStoryId: state.selectedStoryId })); restore(state.undo.pop()); };
- $("redoBtn").onclick = () => { if (!state.redo.length) return; state.undo.push(JSON.stringify({ entries: state.entries, stories: state.stories, selectedStoryId: state.selectedStoryId })); restore(state.redo.pop()); };
+ $("undoBtn").onclick = () => { if (!state.undo.length) return; state.redo.push(JSON.stringify({ entries: state.entries, stories: state.stories, lifeArcs: state.lifeArcs, selectedStoryId: state.selectedStoryId, selectedArcId: state.selectedArcId, selectedChapterId: state.selectedChapterId })); restore(state.undo.pop()); };
+ $("redoBtn").onclick = () => { if (!state.redo.length) return; state.undo.push(JSON.stringify({ entries: state.entries, stories: state.stories, lifeArcs: state.lifeArcs, selectedStoryId: state.selectedStoryId, selectedArcId: state.selectedArcId, selectedChapterId: state.selectedChapterId })); restore(state.redo.pop()); };
$("modebar").querySelectorAll("button").forEach((button) => button.addEventListener("click", () => {
snapshotExploration(`switch to ${button.dataset.mode}`);
state.mode = button.dataset.mode;
diff --git a/src/tests/test_authoring_server.py b/src/tests/test_authoring_server.py
index 7b8b78c..d4e0e79 100755
--- a/src/tests/test_authoring_server.py
+++ b/src/tests/test_authoring_server.py
@@ -8,10 +8,11 @@ from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
-import authoring_service.build as build_server
-import authoring_service.config as config_server
-import authoring_service.content as server
-import authoring_service.utils as utils_server
+import authoring_service.build as build_server
+import authoring_service.config as config_server
+import authoring_service.content as server
+import authoring_service.hidden as hidden_server
+import authoring_service.utils as utils_server
class AuthoringServerTestCase(unittest.TestCase):
@@ -123,12 +124,79 @@ class UtilityTests(AuthoringServerTestCase):
filename, payload, page_path = server.parse_upload_form(content_type, body)
- self.assertEqual(filename, "photo.png")
- self.assertEqual(payload, b"image bytes")
- self.assertEqual(page_path, "lima/index.md")
-
-
-class PageRenderingTests(AuthoringServerTestCase):
+ self.assertEqual(filename, "photo.png")
+ self.assertEqual(payload, b"image bytes")
+ self.assertEqual(page_path, "lima/index.md")
+
+
+class HiddenLifeArcTests(unittest.TestCase):
+ def setUp(self):
+ self.tmp = tempfile.TemporaryDirectory()
+ self.root = Path(self.tmp.name)
+ self.content_json = self.root / "assets" / "content" / "hidden-details.json"
+ self.hidden_js = self.root / "assets" / "scripts" / "features" / "hidden-details.js"
+ self.backup_dir = self.root / "backups" / "hidden-details"
+ self.content_json.parent.mkdir(parents=True)
+ self.hidden_js.parent.mkdir(parents=True)
+ self.hidden_js.write_text(
+ "(function(){\n"
+ " // -----------------------------\n"
+ " // EDITABLE CONTENT\n"
+ " const familyLayers = [];\n"
+ " // -----------------------------\n"
+ " // STATE HELPERS\n"
+ "})();\n",
+ encoding="utf-8",
+ )
+ self.content_json.write_text('{"schemaVersion":3,"entries":[],"stories":[],"lifeArcs":[]}', encoding="utf-8")
+ self.patchers = [
+ mock.patch.object(hidden_server, "ROOT", self.root),
+ mock.patch.object(hidden_server, "HIDDEN_CONTENT_JSON", self.content_json),
+ mock.patch.object(hidden_server, "HIDDEN_DETAILS_JS", self.hidden_js),
+ mock.patch.object(hidden_server, "HIDDEN_BACKUP_DIR", self.backup_dir),
+ ]
+ for patcher in self.patchers:
+ patcher.start()
+
+ def tearDown(self):
+ for patcher in reversed(self.patchers):
+ patcher.stop()
+ self.tmp.cleanup()
+
+ def test_save_hidden_store_preserves_life_arcs(self):
+ saved = hidden_server.save_hidden_store({
+ "entries": [],
+ "stories": [],
+ "lifeArcs": [{
+ "title": "Gym Journey",
+ "domain": "fitness",
+ "themes": ["consistency", "strength"],
+ "visibility": "private",
+ "chapters": [{
+ "title": "Starting Again",
+ "timeframe": "Summer 2026",
+ "purpose": "becoming someone who returns",
+ "storyDraft": "The first victory was returning.",
+ "moments": [{
+ "date": "2026-07-09",
+ "kind": "milestone",
+ "text": "First week back in the gym.",
+ "feeling": "steady",
+ "metric": "3 sessions",
+ "sourcePointer": "Obsidian: Gym",
+ }],
+ }],
+ }],
+ })
+
+ self.assertEqual(saved["lifeArcs"][0]["title"], "Gym Journey")
+ self.assertEqual(saved["lifeArcs"][0]["chapters"][0]["moments"][0]["metric"], "3 sessions")
+ reloaded = hidden_server.load_hidden_store()
+ self.assertEqual(reloaded["lifeArcs"][0]["themes"], ["consistency", "strength"])
+ self.assertEqual(reloaded["lifeArcs"][0]["chapters"][0]["storyDraft"], "The first victory was returning.")
+
+
+class PageRenderingTests(AuthoringServerTestCase):
def test_render_org_writes_metadata_and_body(self):
rendered = server.render_org(
{