Compare commits

5 Commits

Author SHA1 Message Date
19a63069b2 Improve mobile navigation and responsive layout
All checks were successful
Build Authoring Service / build (push) Successful in 9s
2026-07-23 23:08:27 +01:00
1a49838b58 updates
All checks were successful
Build Authoring Service / build (push) Successful in 8s
2026-07-14 12:38:20 +01:00
2f0009df56 adding more things
All checks were successful
Build Authoring Service / build (push) Successful in 9s
2026-07-10 10:28:47 +01:00
8329c00fcc testing
All checks were successful
Build Authoring Service / build (push) Successful in 8s
2026-07-09 17:06:10 +01:00
4dd619d02c Merge pull request 'Improvements' (#1) from zq/work/constellation-fix into main
All checks were successful
Build Authoring Service / build (push) Successful in 9s
Reviewed-on: #1
2026-07-09 16:36:26 +01:00
5 changed files with 1039 additions and 192 deletions

View File

@@ -6,7 +6,7 @@ The hidden ecosystem has one friendly source of truth:
The live site still consumes generated constants in:
- `assets/scripts/hidden-details.js` in `org_web`
- `assets/scripts/features/hidden-details.js` in `org_web`
## Canonical Content

View File

@@ -48,7 +48,7 @@ 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_DETAILS_JS = ROOT / "assets" / "scripts" / "features" / "hidden-details.js"
HIDDEN_CONTENT_JSON = ROOT / "assets" / "content" / "hidden-details.json"
HIDDEN_BACKUP_DIR = Path(os.environ.get("AUTHOR_HIDDEN_BACKUP_DIR", APP_ROOT / "backups" / "hidden-details")).expanduser().resolve()
EXCLUDED_CONTENT_DIR_NAMES = {

View File

@@ -335,7 +335,7 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None
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()
story = existing.copy() if existing else {}
story.update(raw)
@@ -407,10 +407,81 @@ def normalize_hidden_story(raw: dict[str, Any], entries: list[dict[str, Any]] |
"markers": markers,
"createdDate": str(story.get("createdDate") or 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]] = {}
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]:
migrated = False
if HIDDEN_CONTENT_JSON.exists():
data = json.loads(HIDDEN_CONTENT_JSON.read_text(encoding="utf-8"))
entries = data.get("entries", [])
stories = data.get("stories", [])
else:
entries = migrate_hidden_entries_from_js()
stories = []
migrated = True
data = {
"schemaVersion": 2,
"generatedFrom": "assets/scripts/hidden-details.js",
"generatedAt": datetime.now().isoformat(timespec="seconds"),
"entries": entries,
"stories": stories,
}
normalized = [normalize_hidden_entry(entry, entry) for entry in entries]
normalized_stories = [normalize_hidden_story(story, normalized, story) for story in stories]
if HIDDEN_CONTENT_JSON.exists():
data = json.loads(HIDDEN_CONTENT_JSON.read_text(encoding="utf-8"))
entries = data.get("entries", [])
stories = data.get("stories", [])
life_arcs = data.get("lifeArcs", [])
else:
entries = migrate_hidden_entries_from_js()
stories = []
life_arcs = []
migrated = True
data = {
"schemaVersion": 2,
"generatedFrom": "assets/scripts/features/hidden-details.js",
"generatedAt": datetime.now().isoformat(timespec="seconds"),
"entries": entries,
"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_life_arcs = [normalize_life_arc(arc, normalized, normalized_stories, arc) for arc in life_arcs]
migrated_relationships = False
if not normalized_stories:
normalized_stories = migrate_hidden_stories(normalized)
@@ -506,10 +581,11 @@ def load_hidden_store() -> dict[str, Any]:
"storyMarkers": HIDDEN_STORY_MARKERS,
"discoveryStyles": HIDDEN_DISCOVERY_STYLES,
"layers": HIDDEN_LAYER_DEPTHS,
"entries": normalized,
"stories": normalized_stories,
"recommendations": hidden_architecture_recommendations(),
}
"entries": normalized,
"stories": normalized_stories,
"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]:
@@ -554,7 +630,7 @@ def validate_hidden_integrity(entries: list[dict[str, Any]], stories: list[dict[
def hidden_architecture_recommendations() -> dict[str, Any]:
return {
"storage": "Use assets/content/hidden-details.json as the friendly source of truth for reusable memories, first-class stories, and story-only sections, then regenerate the editable constants in assets/scripts/hidden-details.js.",
"storage": "Use assets/content/hidden-details.json as the friendly source of truth for reusable memories, first-class stories, and story-only sections, then regenerate the editable constants in assets/scripts/features/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, resolve conflicts by entry id and story id rather than by whole-file ownership.",
@@ -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:]
def save_hidden_store(payload: dict[str, Any]) -> dict[str, Any]:
loaded = load_hidden_store()
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", [])]
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", []))]
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.")
def save_hidden_store(payload: dict[str, Any]) -> dict[str, Any]:
loaded = load_hidden_store()
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", [])]
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", []))]
current_life_arcs = {arc["id"]: arc for arc in loaded.get("lifeArcs", [])}
life_arcs = [
normalize_life_arc(arc, entries, stories, current_life_arcs.get(str(arc.get("id", ""))))
for arc in payload.get("lifeArcs", loaded.get("lifeArcs", []))
]
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")
backup_hidden_file(HIDDEN_DETAILS_JS, 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(
{
"schemaVersion": 3,
"generatedAt": datetime.now().isoformat(timespec="seconds"),
"entries": entries,
"stories": stories,
},
"generatedAt": datetime.now().isoformat(timespec="seconds"),
"entries": entries,
"stories": stories,
"lifeArcs": life_arcs,
},
ensure_ascii=False,
indent=2,
) + "\n",

File diff suppressed because it is too large Load Diff

View File

@@ -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(
{