adding more things
All checks were successful
Build Authoring Service / build (push) Successful in 9s
All checks were successful
Build Authoring Service / build (push) Successful in 9s
This commit is contained in:
@@ -335,7 +335,7 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None
|
|||||||
return entry
|
return entry
|
||||||
|
|
||||||
|
|
||||||
def normalize_hidden_story(raw: dict[str, Any], entries: list[dict[str, Any]] | None = None, existing: dict[str, Any] | None = None) -> dict[str, Any]:
|
def normalize_hidden_story(raw: dict[str, Any], entries: list[dict[str, Any]] | None = None, existing: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
today = datetime.now().date().isoformat()
|
today = datetime.now().date().isoformat()
|
||||||
story = existing.copy() if existing else {}
|
story = existing.copy() if existing else {}
|
||||||
story.update(raw)
|
story.update(raw)
|
||||||
@@ -407,10 +407,81 @@ def normalize_hidden_story(raw: dict[str, Any], entries: list[dict[str, Any]] |
|
|||||||
"markers": markers,
|
"markers": markers,
|
||||||
"createdDate": str(story.get("createdDate") or today),
|
"createdDate": str(story.get("createdDate") or today),
|
||||||
"modifiedDate": today,
|
"modifiedDate": today,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def migrate_hidden_stories(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def normalize_life_arc(raw: dict[str, Any], entries: list[dict[str, Any]] | None = None, stories: list[dict[str, Any]] | None = None, existing: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
today = datetime.now().date().isoformat()
|
||||||
|
arc = existing.copy() if existing else {}
|
||||||
|
arc.update(raw)
|
||||||
|
title = str(normalize_character_text_refs(arc.get("title") or "")).strip()
|
||||||
|
if not title:
|
||||||
|
raise ValueError("Every life arc needs a title.")
|
||||||
|
arc_id = slugify(str(arc.get("id") or f"arc-{title}"))
|
||||||
|
if not arc_id.startswith("arc-"):
|
||||||
|
arc_id = f"arc-{arc_id}"
|
||||||
|
entry_ids = {entry["id"] for entry in entries or []}
|
||||||
|
story_ids = {story["id"] for story in stories or []}
|
||||||
|
chapters = []
|
||||||
|
for index, raw_chapter in enumerate(arc.get("chapters", [])):
|
||||||
|
if not isinstance(raw_chapter, dict):
|
||||||
|
continue
|
||||||
|
chapter_title = str(normalize_character_text_refs(raw_chapter.get("title") or f"Chapter {index + 1}")).strip()
|
||||||
|
chapter_id = slugify(str(raw_chapter.get("id") or f"{arc_id}-chapter-{index + 1}-{chapter_title}"))
|
||||||
|
if not chapter_id.startswith("chapter-"):
|
||||||
|
chapter_id = f"chapter-{chapter_id}"
|
||||||
|
moments = []
|
||||||
|
for moment_index, raw_moment in enumerate(raw_chapter.get("moments", [])):
|
||||||
|
if not isinstance(raw_moment, dict):
|
||||||
|
raw_moment = {"text": str(raw_moment)}
|
||||||
|
text = str(normalize_character_text_refs(raw_moment.get("text") or raw_moment.get("content") or "")).strip()
|
||||||
|
lesson = str(normalize_character_text_refs(raw_moment.get("lesson") or "")).strip()
|
||||||
|
if not text and not lesson:
|
||||||
|
continue
|
||||||
|
moment_id = slugify(str(raw_moment.get("id") or f"{chapter_id}-moment-{moment_index + 1}"))
|
||||||
|
if not moment_id.startswith("moment-"):
|
||||||
|
moment_id = f"moment-{moment_id}"
|
||||||
|
linked_entry_id = str(raw_moment.get("linkedEntryId") or "").strip()
|
||||||
|
linked_story_id = str(raw_moment.get("linkedStoryId") or "").strip()
|
||||||
|
moments.append({
|
||||||
|
"id": moment_id,
|
||||||
|
"date": str(raw_moment.get("date") or ""),
|
||||||
|
"kind": str(raw_moment.get("kind") or "moment"),
|
||||||
|
"text": text,
|
||||||
|
"feeling": str(normalize_character_text_refs(raw_moment.get("feeling") or "")).strip(),
|
||||||
|
"lesson": lesson,
|
||||||
|
"metric": str(raw_moment.get("metric") or ""),
|
||||||
|
"sourcePointer": str(raw_moment.get("sourcePointer") or ""),
|
||||||
|
"visibility": str(raw_moment.get("visibility") or "private"),
|
||||||
|
"linkedEntryId": linked_entry_id if linked_entry_id in entry_ids else "",
|
||||||
|
"linkedStoryId": linked_story_id if linked_story_id in story_ids else "",
|
||||||
|
})
|
||||||
|
chapters.append({
|
||||||
|
"id": chapter_id,
|
||||||
|
"title": chapter_title,
|
||||||
|
"timeframe": str(normalize_character_text_refs(raw_chapter.get("timeframe") or "")).strip(),
|
||||||
|
"tone": str(raw_chapter.get("tone") or arc.get("tone") or "warm"),
|
||||||
|
"purpose": str(normalize_character_text_refs(raw_chapter.get("purpose") or "")).strip(),
|
||||||
|
"storyDraft": str(normalize_character_text_refs(raw_chapter.get("storyDraft") or "")).replace("\r\n", "\n"),
|
||||||
|
"linkedStoryId": str(raw_chapter.get("linkedStoryId") or "") if str(raw_chapter.get("linkedStoryId") or "") in story_ids else "",
|
||||||
|
"moments": moments,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"id": arc_id,
|
||||||
|
"title": title,
|
||||||
|
"domain": str(normalize_character_text_refs(arc.get("domain") or "")).strip(),
|
||||||
|
"summary": str(normalize_character_text_refs(arc.get("summary") or "")).strip(),
|
||||||
|
"tone": str(arc.get("tone") or "warm"),
|
||||||
|
"themes": [str(normalize_character_text_refs(item)).strip() for item in arc.get("themes", []) if str(item).strip()],
|
||||||
|
"orgWebConnection": str(normalize_character_text_refs(arc.get("orgWebConnection") or "")).strip(),
|
||||||
|
"visibility": str(arc.get("visibility") or "private"),
|
||||||
|
"chapters": chapters,
|
||||||
|
"createdDate": str(arc.get("createdDate") or today),
|
||||||
|
"modifiedDate": today,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_hidden_stories(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
story_groups: dict[str, dict[str, Any]] = {}
|
story_groups: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
def add(group: str, entry: dict[str, Any], source: str) -> None:
|
def add(group: str, entry: dict[str, Any], source: str) -> None:
|
||||||
@@ -467,23 +538,27 @@ def migrate_hidden_stories(entries: list[dict[str, Any]]) -> list[dict[str, Any]
|
|||||||
|
|
||||||
def load_hidden_store() -> dict[str, Any]:
|
def load_hidden_store() -> dict[str, Any]:
|
||||||
migrated = False
|
migrated = False
|
||||||
if HIDDEN_CONTENT_JSON.exists():
|
if HIDDEN_CONTENT_JSON.exists():
|
||||||
data = json.loads(HIDDEN_CONTENT_JSON.read_text(encoding="utf-8"))
|
data = json.loads(HIDDEN_CONTENT_JSON.read_text(encoding="utf-8"))
|
||||||
entries = data.get("entries", [])
|
entries = data.get("entries", [])
|
||||||
stories = data.get("stories", [])
|
stories = data.get("stories", [])
|
||||||
else:
|
life_arcs = data.get("lifeArcs", [])
|
||||||
entries = migrate_hidden_entries_from_js()
|
else:
|
||||||
stories = []
|
entries = migrate_hidden_entries_from_js()
|
||||||
migrated = True
|
stories = []
|
||||||
data = {
|
life_arcs = []
|
||||||
"schemaVersion": 2,
|
migrated = True
|
||||||
|
data = {
|
||||||
|
"schemaVersion": 2,
|
||||||
"generatedFrom": "assets/scripts/features/hidden-details.js",
|
"generatedFrom": "assets/scripts/features/hidden-details.js",
|
||||||
"generatedAt": datetime.now().isoformat(timespec="seconds"),
|
"generatedAt": datetime.now().isoformat(timespec="seconds"),
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
"stories": stories,
|
"stories": stories,
|
||||||
}
|
"lifeArcs": life_arcs,
|
||||||
normalized = [normalize_hidden_entry(entry, entry) for entry in entries]
|
}
|
||||||
normalized_stories = [normalize_hidden_story(story, normalized, story) for story in stories]
|
normalized = [normalize_hidden_entry(entry, entry) for entry in entries]
|
||||||
|
normalized_stories = [normalize_hidden_story(story, normalized, story) for story in stories]
|
||||||
|
normalized_life_arcs = [normalize_life_arc(arc, normalized, normalized_stories, arc) for arc in life_arcs]
|
||||||
migrated_relationships = False
|
migrated_relationships = False
|
||||||
if not normalized_stories:
|
if not normalized_stories:
|
||||||
normalized_stories = migrate_hidden_stories(normalized)
|
normalized_stories = migrate_hidden_stories(normalized)
|
||||||
@@ -506,10 +581,11 @@ def load_hidden_store() -> dict[str, Any]:
|
|||||||
"storyMarkers": HIDDEN_STORY_MARKERS,
|
"storyMarkers": HIDDEN_STORY_MARKERS,
|
||||||
"discoveryStyles": HIDDEN_DISCOVERY_STYLES,
|
"discoveryStyles": HIDDEN_DISCOVERY_STYLES,
|
||||||
"layers": HIDDEN_LAYER_DEPTHS,
|
"layers": HIDDEN_LAYER_DEPTHS,
|
||||||
"entries": normalized,
|
"entries": normalized,
|
||||||
"stories": normalized_stories,
|
"stories": normalized_stories,
|
||||||
"recommendations": hidden_architecture_recommendations(),
|
"lifeArcs": normalized_life_arcs,
|
||||||
}
|
"recommendations": hidden_architecture_recommendations(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def validate_hidden_integrity(entries: list[dict[str, Any]], stories: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
def validate_hidden_integrity(entries: list[dict[str, Any]], stories: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
||||||
@@ -689,18 +765,26 @@ def replace_hidden_editable_block(source: str, entries: list[dict[str, Any]]) ->
|
|||||||
return source[:start] + generated_hidden_content_block(entries) + "\n" + source[end:]
|
return source[:start] + generated_hidden_content_block(entries) + "\n" + source[end:]
|
||||||
|
|
||||||
|
|
||||||
def save_hidden_store(payload: dict[str, Any]) -> dict[str, Any]:
|
def save_hidden_store(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
loaded = load_hidden_store()
|
loaded = load_hidden_store()
|
||||||
current = {entry["id"]: entry for entry in loaded["entries"]}
|
current = {entry["id"]: entry for entry in loaded["entries"]}
|
||||||
entries = [normalize_hidden_entry(entry, current.get(str(entry.get("id", "")))) for entry in payload.get("entries", [])]
|
entries = [normalize_hidden_entry(entry, current.get(str(entry.get("id", "")))) for entry in payload.get("entries", [])]
|
||||||
current_stories = {story["id"]: story for story in loaded.get("stories", [])}
|
current_stories = {story["id"]: story for story in loaded.get("stories", [])}
|
||||||
stories = [normalize_hidden_story(story, entries, current_stories.get(str(story.get("id", "")))) for story in payload.get("stories", loaded.get("stories", []))]
|
stories = [normalize_hidden_story(story, entries, current_stories.get(str(story.get("id", "")))) for story in payload.get("stories", loaded.get("stories", []))]
|
||||||
ids = [entry["id"] for entry in entries]
|
current_life_arcs = {arc["id"]: arc for arc in loaded.get("lifeArcs", [])}
|
||||||
if len(ids) != len(set(ids)):
|
life_arcs = [
|
||||||
raise ValueError("Entry ids must be unique.")
|
normalize_life_arc(arc, entries, stories, current_life_arcs.get(str(arc.get("id", ""))))
|
||||||
story_ids = [story["id"] for story in stories]
|
for arc in payload.get("lifeArcs", loaded.get("lifeArcs", []))
|
||||||
if len(story_ids) != len(set(story_ids)):
|
]
|
||||||
raise ValueError("Story ids must be unique.")
|
ids = [entry["id"] for entry in entries]
|
||||||
|
if len(ids) != len(set(ids)):
|
||||||
|
raise ValueError("Entry ids must be unique.")
|
||||||
|
story_ids = [story["id"] for story in stories]
|
||||||
|
if len(story_ids) != len(set(story_ids)):
|
||||||
|
raise ValueError("Story ids must be unique.")
|
||||||
|
life_arc_ids = [arc["id"] for arc in life_arcs]
|
||||||
|
if len(life_arc_ids) != len(set(life_arc_ids)):
|
||||||
|
raise ValueError("Life arc ids must be unique.")
|
||||||
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
backup_hidden_file(HIDDEN_DETAILS_JS, stamp)
|
backup_hidden_file(HIDDEN_DETAILS_JS, stamp)
|
||||||
backup_hidden_file(HIDDEN_CONTENT_JSON, stamp)
|
backup_hidden_file(HIDDEN_CONTENT_JSON, stamp)
|
||||||
@@ -709,10 +793,11 @@ def save_hidden_store(payload: dict[str, Any]) -> dict[str, Any]:
|
|||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"schemaVersion": 3,
|
"schemaVersion": 3,
|
||||||
"generatedAt": datetime.now().isoformat(timespec="seconds"),
|
"generatedAt": datetime.now().isoformat(timespec="seconds"),
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
"stories": stories,
|
"stories": stories,
|
||||||
},
|
"lifeArcs": life_arcs,
|
||||||
|
},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
indent=2,
|
indent=2,
|
||||||
) + "\n",
|
) + "\n",
|
||||||
|
|||||||
@@ -1522,7 +1522,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
font-family: Georgia, "Times New Roman", serif;
|
font-family: Georgia, "Times New Roman", serif;
|
||||||
}
|
}
|
||||||
.connection-list, .timeline-list, .story-list, .story-flow-list, .memory-pool { display: grid; gap: 8px; }
|
.connection-list, .timeline-list, .story-list, .story-flow-list, .memory-pool, .arc-list, .chapter-list, .moment-list { display: grid; gap: 8px; }
|
||||||
.connection {
|
.connection {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -1543,7 +1543,26 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(251, 241, 220, 0.075);
|
background: rgba(251, 241, 220, 0.075);
|
||||||
}
|
}
|
||||||
.story-card.active { border-color: var(--gold); background: rgba(211, 166, 77, 0.18); }
|
.story-card.active { border-color: var(--gold); background: rgba(211, 166, 77, 0.18); }
|
||||||
|
.arc-card, .chapter-card, .moment-card {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
height: auto;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 9px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(251, 241, 220, 0.06);
|
||||||
|
}
|
||||||
|
.arc-card.active, .chapter-card.active { border-color: var(--blue); background: rgba(117, 169, 189, 0.16); }
|
||||||
|
.moment-card {
|
||||||
|
border: 1px solid rgba(232, 202, 139, 0.14);
|
||||||
|
color: #efd9b2;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.moment-card strong, .arc-card strong, .chapter-card strong { overflow-wrap: anywhere; }
|
||||||
|
.arc-toolbar { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px; }
|
||||||
.story-cover {
|
.story-cover {
|
||||||
border: 1px solid rgba(232, 202, 139, 0.18);
|
border: 1px solid rgba(232, 202, 139, 0.18);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -1898,8 +1917,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<div id="drawerCharacters" class="pills" style="margin-top:10px"></div>
|
<div id="drawerCharacters" class="pills" style="margin-top:10px"></div>
|
||||||
<div id="preview" class="preview" style="margin-top:10px"></div>
|
<div id="preview" class="preview" style="margin-top:10px"></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel editor-panel story-editor-panel" id="storyEditorPanel">
|
<section class="panel editor-panel story-editor-panel" id="storyEditorPanel">
|
||||||
<h2>Story Builder</h2>
|
<h2>Story Builder</h2>
|
||||||
<div id="storyCover" class="story-cover"></div>
|
<div id="storyCover" class="story-cover"></div>
|
||||||
<div class="editor-grid" style="margin-top:10px">
|
<div class="editor-grid" style="margin-top:10px">
|
||||||
<label class="full">Story title<input id="storyTitle" /></label>
|
<label class="full">Story title<input id="storyTitle" /></label>
|
||||||
@@ -1929,10 +1948,57 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<button id="closeSectionBtn" type="button">Close</button>
|
<button id="closeSectionBtn" type="button">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 style="margin-top:14px">Memory pool</h3>
|
<h3 style="margin-top:14px">Memory pool</h3>
|
||||||
<div id="memoryPool" class="memory-pool"></div>
|
<div id="memoryPool" class="memory-pool"></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel editor-panel memory-editor-panel" id="memoryEditorPanel">
|
<section class="panel" id="arcStudioPanel">
|
||||||
|
<h2>Arc Studio</h2>
|
||||||
|
<div class="subtle">Shape long-running life arcs without turning this into a notes vault. Keep raw resources in Obsidian; store the moments that changed the story here.</div>
|
||||||
|
<div class="arc-toolbar" style="margin-top:10px">
|
||||||
|
<button id="newArcBtn" type="button">New arc</button>
|
||||||
|
<button id="newChapterBtn" type="button">New chapter</button>
|
||||||
|
<button id="deleteArcBtn" class="danger" type="button">Delete arc</button>
|
||||||
|
</div>
|
||||||
|
<div class="editor-grid" style="margin-top:10px">
|
||||||
|
<label class="full">Arc title<input id="arcTitle" placeholder="Gym Journey" /></label>
|
||||||
|
<label>Domain<input id="arcDomain" placeholder="fitness, discipline, faith" /></label>
|
||||||
|
<label>Visibility<select id="arcVisibility"><option value="private">Private</option><option value="hidden">Hidden org web</option><option value="public">Public fragment source</option></select></label>
|
||||||
|
<label>Tone<select id="arcTone"></select></label>
|
||||||
|
<label>Themes<input id="arcThemes" placeholder="consistency, strength, identity" /></label>
|
||||||
|
<label class="full">Summary<textarea id="arcSummary" placeholder="What this arc is really about."></textarea></label>
|
||||||
|
<label class="full">Org web connection<input id="arcOrgWeb" placeholder="optional hidden route, story idea, fragment source" /></label>
|
||||||
|
</div>
|
||||||
|
<h3 style="margin-top:14px">Life arcs</h3>
|
||||||
|
<div id="arcList" class="arc-list"></div>
|
||||||
|
<h3 style="margin-top:14px">Chapters</h3>
|
||||||
|
<div id="chapterList" class="chapter-list"></div>
|
||||||
|
<div class="editor-grid" style="margin-top:10px">
|
||||||
|
<label class="full">Chapter title<input id="chapterTitle" placeholder="Starting Again" /></label>
|
||||||
|
<label>Timeframe<input id="chapterTimeframe" placeholder="Summer 2026" /></label>
|
||||||
|
<label>Tone<select id="chapterTone"></select></label>
|
||||||
|
<label class="full">Narrative purpose<input id="chapterPurpose" placeholder="What changed in this phase?" /></label>
|
||||||
|
<label class="full">Story draft<textarea id="chapterStoryDraft" placeholder="Turn selected moments into prose here."></textarea></label>
|
||||||
|
</div>
|
||||||
|
<h3 style="margin-top:14px">Log moment</h3>
|
||||||
|
<div class="editor-grid">
|
||||||
|
<label>Date<input id="momentDate" type="date" /></label>
|
||||||
|
<label>Kind<select id="momentKind"><option value="moment">Moment</option><option value="milestone">Milestone</option><option value="lesson">Lesson</option><option value="quote">Quote</option><option value="feeling">Feeling</option><option value="metric">Metric</option></select></label>
|
||||||
|
<label class="full">What happened<textarea id="momentText" placeholder="Keep it compact: the scene, change, or realization."></textarea></label>
|
||||||
|
<label>Feeling<input id="momentFeeling" placeholder="steady, frustrated, proud" /></label>
|
||||||
|
<label>Metric<input id="momentMetric" placeholder="80kg bench, 4 sessions/week" /></label>
|
||||||
|
<label class="full">Lesson<input id="momentLesson" placeholder="What did this teach you?" /></label>
|
||||||
|
<label class="full">Obsidian/source pointer<input id="momentSource" placeholder="obsidian://... or Books/Atomic Habits" /></label>
|
||||||
|
<label>Moment visibility<select id="momentVisibility"><option value="private">Private</option><option value="hidden">Hidden org web</option><option value="public">Public fragment source</option></select></label>
|
||||||
|
<label>Link story<select id="momentStoryLink"></select></label>
|
||||||
|
<label class="full">Link memory<select id="momentEntryLink"></select></label>
|
||||||
|
</div>
|
||||||
|
<div class="actions" style="margin-top:10px">
|
||||||
|
<button id="addMomentBtn" type="button">Add moment</button>
|
||||||
|
<button id="deleteChapterBtn" class="danger" type="button">Delete chapter</button>
|
||||||
|
</div>
|
||||||
|
<div id="momentList" class="moment-list" style="margin-top:10px"></div>
|
||||||
|
</section>
|
||||||
|
<section class="panel editor-panel memory-editor-panel" id="memoryEditorPanel">
|
||||||
<h2>Memory editor</h2>
|
<h2>Memory editor</h2>
|
||||||
<div class="editor-grid">
|
<div class="editor-grid">
|
||||||
<label class="full">Name<input id="title" /></label>
|
<label class="full">Name<input id="title" /></label>
|
||||||
@@ -2017,9 +2083,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
const draftKey = "hiddenNarrativeObservatoryDraft:v1";
|
const draftKey = "hiddenNarrativeObservatoryDraft:v1";
|
||||||
const WORLD = { width: 4600, height: 3300, cx: 2300, cy: 1650 };
|
const WORLD = { width: 4600, height: 3300, cx: 2300, cy: 1650 };
|
||||||
const SCREEN = { width: 1000, height: 760 };
|
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 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 $ = (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 toneColors = { warm: "#d3a64d", funny: "#7fb089", nostalgic: "#d06b78", wise: "#75a9bd", strange: "#a58ac9", soft: "#e6bd8c", hopeful: "#9fcf9f", protective: "#d98f71", melancholy: "#8fa7c6" };
|
||||||
const appearanceLabels = {
|
const appearanceLabels = {
|
||||||
@@ -2052,17 +2120,27 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
function selectedOptions(id) {
|
function selectedOptions(id) {
|
||||||
return Array.from($(id).selectedOptions || []).map((option) => option.value).filter(Boolean);
|
return Array.from($(id).selectedOptions || []).map((option) => option.value).filter(Boolean);
|
||||||
}
|
}
|
||||||
function storyById(id = state.selectedStoryId) {
|
function storyById(id = state.selectedStoryId) {
|
||||||
return state.stories.find((story) => story.id === id);
|
return state.stories.find((story) => story.id === id);
|
||||||
}
|
}
|
||||||
function storiesForEntry(entryId) {
|
function arcById(id = state.selectedArcId) {
|
||||||
return state.stories.filter((story) => (story.nodes || []).includes(entryId));
|
return state.lifeArcs.find((arc) => arc.id === id);
|
||||||
}
|
}
|
||||||
function storyItems(story) {
|
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 (!story) return [];
|
||||||
if (Array.isArray(story.items) && story.items.length) return story.items;
|
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) {
|
function entryById(id) {
|
||||||
return state.entries.find((entry) => entry.id === id);
|
return state.entries.find((entry) => entry.id === id);
|
||||||
}
|
}
|
||||||
@@ -2248,20 +2326,24 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
window.clearTimeout(state.renderTimer);
|
window.clearTimeout(state.renderTimer);
|
||||||
state.renderTimer = window.setTimeout(render, 90);
|
state.renderTimer = window.setTimeout(render, 90);
|
||||||
}
|
}
|
||||||
function snapshot() {
|
function snapshot() {
|
||||||
state.undo.push(JSON.stringify({ entries: state.entries, stories: state.stories, selectedStoryId: state.selectedStoryId }));
|
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.undo = state.undo.slice(-60);
|
||||||
state.redo = [];
|
state.redo = [];
|
||||||
}
|
}
|
||||||
function restore(serialized) {
|
function restore(serialized) {
|
||||||
const data = JSON.parse(serialized);
|
const data = JSON.parse(serialized);
|
||||||
state.entries = data.entries || data || [];
|
state.entries = data.entries || data || [];
|
||||||
state.stories = data.stories || state.stories || [];
|
state.stories = data.stories || state.stories || [];
|
||||||
state.selectedStoryId = data.selectedStoryId || state.selectedStoryId || state.stories[0]?.id || "";
|
state.lifeArcs = data.lifeArcs || state.lifeArcs || [];
|
||||||
rememberDraft();
|
state.selectedStoryId = data.selectedStoryId || state.selectedStoryId || state.stories[0]?.id || "";
|
||||||
render();
|
state.selectedArcId = data.selectedArcId || state.selectedArcId || state.lifeArcs[0]?.id || "";
|
||||||
selectNode(state.selectedId || state.entries[0]?.id);
|
state.selectedChapterId = data.selectedChapterId || state.selectedChapterId || arcById()?.chapters?.[0]?.id || "";
|
||||||
fillStoryForm(storyById());
|
rememberDraft();
|
||||||
|
render();
|
||||||
|
selectNode(state.selectedId || state.entries[0]?.id);
|
||||||
|
fillStoryForm(storyById());
|
||||||
|
fillArcForm(arcById());
|
||||||
}
|
}
|
||||||
function filterState() {
|
function filterState() {
|
||||||
return {
|
return {
|
||||||
@@ -2325,10 +2407,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
applyExploration(previous);
|
applyExploration(previous);
|
||||||
setStatus("one exploration step undone.");
|
setStatus("one exploration step undone.");
|
||||||
}
|
}
|
||||||
function rememberDraft() {
|
function rememberDraft() {
|
||||||
localStorage.setItem(draftKey, JSON.stringify({ entries: state.entries, stories: state.stories, selectedId: state.selectedId, selectedStoryId: state.selectedStoryId, savedAt: Date.now() }));
|
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.";
|
$("autosave").textContent = "local draft kept warm.";
|
||||||
}
|
}
|
||||||
function depth(entry) {
|
function depth(entry) {
|
||||||
const value = String(entry.familyLayer || "").match(/[0-5]/);
|
const value = String(entry.familyLayer || "").match(/[0-5]/);
|
||||||
if (value) return Number(value[0]);
|
if (value) return Number(value[0]);
|
||||||
@@ -2417,16 +2499,219 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
markers: selectedOptions("storyMarkers"),
|
markers: selectedOptions("storyMarkers"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function applyStoryFormToState() {
|
function applyStoryFormToState() {
|
||||||
if (!state.selectedStoryId) return;
|
if (!state.selectedStoryId) return;
|
||||||
const index = state.stories.findIndex((story) => story.id === state.selectedStoryId);
|
const index = state.stories.findIndex((story) => story.id === state.selectedStoryId);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
state.stories[index] = storyFromForm();
|
state.stories[index] = storyFromForm();
|
||||||
state.selectedStoryId = state.stories[index].id;
|
state.selectedStoryId = state.stories[index].id;
|
||||||
rememberDraft();
|
rememberDraft();
|
||||||
renderStoryBuilder();
|
renderStoryBuilder();
|
||||||
render();
|
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 = "<span class='subtle'>Create a chapter before logging moments.</span>";
|
||||||
|
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) => `<button class="arc-card ${item.id === state.selectedArcId ? "active" : ""}" type="button" data-arc="${html(item.id)}"><strong>${html(item.title)}</strong><span class="subtle">${html(item.domain || "life arc")} / ${arcMomentCount(item)} moments / ${html(item.visibility || "private")}</span></button>`).join("")
|
||||||
|
: "<span class='subtle'>No life arcs yet.</span>";
|
||||||
|
$("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) => `<button class="chapter-card ${item.id === state.selectedChapterId ? "active" : ""}" type="button" data-chapter="${html(item.id)}"><strong>${html(item.title)}</strong><span class="subtle">${html(item.timeframe || "open timeframe")} / ${(item.moments || []).length} moments</span></button>`).join("")
|
||||||
|
: "<span class='subtle'>No chapters in this arc yet.</span>";
|
||||||
|
$("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) => `<div class="moment-card"><strong>${html(moment.date || moment.kind || "moment")}</strong><span>${html(moment.text || moment.lesson)}</span><span class="subtle">${[moment.feeling, moment.metric, moment.sourcePointer, moment.visibility].filter(Boolean).map(html).join(" / ")}</span><button type="button" data-delete-moment="${html(moment.id)}">Delete</button></div>`).join("")
|
||||||
|
: "<span class='subtle'>Log compact moments here, then turn selected material into the chapter story draft.</span>";
|
||||||
|
$("momentList").querySelectorAll("[data-delete-moment]").forEach((button) => button.addEventListener("click", () => deleteMoment(button.dataset.deleteMoment)));
|
||||||
|
}
|
||||||
function fillStoryForm(story) {
|
function fillStoryForm(story) {
|
||||||
if (!story) {
|
if (!story) {
|
||||||
$("storyCover").innerHTML = "<span class='subtle'>Create a story to shape a route through the observatory.</span>";
|
$("storyCover").innerHTML = "<span class='subtle'>Create a story to shape a route through the observatory.</span>";
|
||||||
@@ -3902,9 +4187,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
const opt = (value, label = value) => `<option value="${html(value)}">${html(label || "All")}</option>`;
|
const opt = (value, label = value) => `<option value="${html(value)}">${html(label || "All")}</option>`;
|
||||||
$("type").innerHTML = state.meta.types.map((value) => opt(value)).join("");
|
$("type").innerHTML = state.meta.types.map((value) => opt(value)).join("");
|
||||||
$("contentClass").innerHTML = (state.meta.contentClasses || ["fragment","memory"]).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("");
|
$("tone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
|
||||||
$("storyTone").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("");
|
$("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("");
|
$("storyMarkers").innerHTML = (state.meta.storyMarkers || []).map((value) => opt(value)).join("");
|
||||||
$("rarity").innerHTML = state.meta.rarities.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("");
|
$("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"""<!doctype html>
|
|||||||
const territory = config.observatory?.territory || "memory territory";
|
const territory = config.observatory?.territory || "memory territory";
|
||||||
return `<button class="connection character-row" type="button" data-character="${html(character)}" style="--character-glow:${html(characterGlow(character))}"><span>${characterImg(character, "medium")}</span><span><strong>${html(characterLabel(character))}</strong><br><span class="subtle">${html(territory)}</span></span></button>`;
|
return `<button class="connection character-row" type="button" data-character="${html(character)}" style="--character-glow:${html(characterGlow(character))}"><span>${characterImg(character, "medium")}</span><span><strong>${html(characterLabel(character))}</strong><br><span class="subtle">${html(territory)}</span></span></button>`;
|
||||||
}).join("");
|
}).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}`);
|
snapshotExploration(`character territory: ${button.dataset.character}`);
|
||||||
$("characterFilter").value = button.dataset.character;
|
$("characterFilter").value = button.dataset.character;
|
||||||
state.mode = "character";
|
state.mode = "character";
|
||||||
@@ -3928,22 +4215,34 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
$("modebar").querySelectorAll("button").forEach((item) => item.classList.toggle("active", item.dataset.mode === state.mode));
|
$("modebar").querySelectorAll("button").forEach((item) => item.classList.toggle("active", item.dataset.mode === state.mode));
|
||||||
render();
|
render();
|
||||||
window.setTimeout(() => fitCameraTo(boundsForEntries(visibleScope().entries), true), 0);
|
window.setTimeout(() => fitCameraTo(boundsForEntries(visibleScope().entries), true), 0);
|
||||||
}));
|
}));
|
||||||
renderStoryBuilder();
|
refreshArcLinkOptions();
|
||||||
}
|
renderStoryBuilder();
|
||||||
async function save() {
|
renderArcStudio();
|
||||||
applyFormToState();
|
}
|
||||||
applyStoryFormToState();
|
function refreshArcLinkOptions() {
|
||||||
setStatus("Saving the constellation.");
|
const opt = (value, label = value) => `<option value="${html(value)}">${html(label || "None")}</option>`;
|
||||||
try {
|
$("momentStoryLink").innerHTML = opt("", "No story link") + state.stories.map((story) => opt(story.id, story.title)).join("");
|
||||||
const data = await api("/api/hidden", { method: "POST", body: JSON.stringify({ entries: state.entries, stories: state.stories }) });
|
$("momentEntryLink").innerHTML = opt("", "No memory link") + state.entries.map((entry) => opt(entry.id, entry.title)).join("");
|
||||||
state.entries = data.entries;
|
}
|
||||||
state.stories = data.stories || [];
|
async function save() {
|
||||||
state.meta = data;
|
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);
|
localStorage.removeItem(draftKey);
|
||||||
setStatus(data.queuedBuild ? "constellation stored safely. asset republish queued." : (data.message || "constellation stored safely."));
|
setStatus(data.queuedBuild ? "constellation stored safely. asset republish queued." : (data.message || "constellation stored safely."));
|
||||||
renderValidation();
|
renderValidation();
|
||||||
render();
|
refreshArcLinkOptions();
|
||||||
|
renderArcStudio();
|
||||||
|
render();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus(err.message);
|
setStatus(err.message);
|
||||||
}
|
}
|
||||||
@@ -3952,21 +4251,28 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
try {
|
try {
|
||||||
const data = await api("/api/hidden");
|
const data = await api("/api/hidden");
|
||||||
state.meta = data;
|
state.meta = data;
|
||||||
state.entries = data.entries || [];
|
state.entries = data.entries || [];
|
||||||
state.stories = data.stories || [];
|
state.stories = data.stories || [];
|
||||||
const draft = JSON.parse(localStorage.getItem(draftKey) || "null");
|
state.lifeArcs = data.lifeArcs || [];
|
||||||
if (draft?.entries?.length && confirm("A local constellation draft exists. Restore it?")) {
|
const draft = JSON.parse(localStorage.getItem(draftKey) || "null");
|
||||||
state.entries = draft.entries;
|
if ((draft?.entries?.length || draft?.stories?.length || draft?.lifeArcs?.length) && confirm("A local constellation draft exists. Restore it?")) {
|
||||||
state.stories = draft.stories || state.stories;
|
state.entries = draft.entries;
|
||||||
state.selectedId = draft.selectedId || "";
|
state.stories = draft.stories || state.stories;
|
||||||
state.selectedStoryId = draft.selectedStoryId || "";
|
state.lifeArcs = draft.lifeArcs || state.lifeArcs;
|
||||||
}
|
state.selectedId = draft.selectedId || "";
|
||||||
|
state.selectedStoryId = draft.selectedStoryId || "";
|
||||||
|
state.selectedArcId = draft.selectedArcId || "";
|
||||||
|
state.selectedChapterId = draft.selectedChapterId || "";
|
||||||
|
}
|
||||||
populateControls();
|
populateControls();
|
||||||
renderValidation();
|
renderValidation();
|
||||||
state.selectedStoryId = state.selectedStoryId || state.stories[0]?.id || "";
|
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.");
|
state.selectedArcId = state.selectedArcId || state.lifeArcs[0]?.id || "";
|
||||||
selectNode(state.selectedId || state.entries[0]?.id);
|
state.selectedChapterId = state.selectedChapterId || arcById()?.chapters?.[0]?.id || "";
|
||||||
fillStoryForm(storyById());
|
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);
|
fitCameraTo({ minX: 250, minY: 220, maxX: WORLD.width - 250, maxY: WORLD.height - 220 }, false);
|
||||||
render();
|
render();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -4068,10 +4374,18 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
applyTypeDefaults();
|
applyTypeDefaults();
|
||||||
applyFormToState();
|
applyFormToState();
|
||||||
});
|
});
|
||||||
storyFields.forEach((id) => {
|
storyFields.forEach((id) => {
|
||||||
$(id).addEventListener("input", applyStoryFormToState);
|
$(id).addEventListener("input", applyStoryFormToState);
|
||||||
$(id).addEventListener("change", () => { snapshot(); 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;
|
let filterSnapshotTimer = 0;
|
||||||
["search","layerFilter","characterFilter","toneFilter","rarityFilter"].forEach((id) => {
|
["search","layerFilter","characterFilter","toneFilter","rarityFilter"].forEach((id) => {
|
||||||
$(id).addEventListener("focus", () => {
|
$(id).addEventListener("focus", () => {
|
||||||
@@ -4092,7 +4406,12 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
$("newBtn").onclick = () => newEntry();
|
$("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);
|
$("storyFromSelectionBtn").onclick = () => newStory(state.selectedId);
|
||||||
$("addSectionBtn").onclick = addStorySection;
|
$("addSectionBtn").onclick = addStorySection;
|
||||||
$("saveSectionBtn").onclick = saveStorySection;
|
$("saveSectionBtn").onclick = saveStorySection;
|
||||||
@@ -4113,8 +4432,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
$("readerNextBtn").onclick = () => stepStoryReader(1);
|
$("readerNextBtn").onclick = () => stepStoryReader(1);
|
||||||
$("duplicateBtn").onclick = duplicateEntry;
|
$("duplicateBtn").onclick = duplicateEntry;
|
||||||
$("deleteBtn").onclick = deleteEntry;
|
$("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()); };
|
$("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, selectedStoryId: state.selectedStoryId })); restore(state.redo.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", () => {
|
$("modebar").querySelectorAll("button").forEach((button) => button.addEventListener("click", () => {
|
||||||
snapshotExploration(`switch to ${button.dataset.mode}`);
|
snapshotExploration(`switch to ${button.dataset.mode}`);
|
||||||
state.mode = button.dataset.mode;
|
state.mode = button.dataset.mode;
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ from unittest import mock
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
import authoring_service.build as build_server
|
import authoring_service.build as build_server
|
||||||
import authoring_service.config as config_server
|
import authoring_service.config as config_server
|
||||||
import authoring_service.content as server
|
import authoring_service.content as server
|
||||||
import authoring_service.utils as utils_server
|
import authoring_service.hidden as hidden_server
|
||||||
|
import authoring_service.utils as utils_server
|
||||||
|
|
||||||
|
|
||||||
class AuthoringServerTestCase(unittest.TestCase):
|
class AuthoringServerTestCase(unittest.TestCase):
|
||||||
@@ -123,12 +124,79 @@ class UtilityTests(AuthoringServerTestCase):
|
|||||||
|
|
||||||
filename, payload, page_path = server.parse_upload_form(content_type, body)
|
filename, payload, page_path = server.parse_upload_form(content_type, body)
|
||||||
|
|
||||||
self.assertEqual(filename, "photo.png")
|
self.assertEqual(filename, "photo.png")
|
||||||
self.assertEqual(payload, b"image bytes")
|
self.assertEqual(payload, b"image bytes")
|
||||||
self.assertEqual(page_path, "lima/index.md")
|
self.assertEqual(page_path, "lima/index.md")
|
||||||
|
|
||||||
|
|
||||||
class PageRenderingTests(AuthoringServerTestCase):
|
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):
|
def test_render_org_writes_metadata_and_body(self):
|
||||||
rendered = server.render_org(
|
rendered = server.render_org(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user