| name | family-viewer-page |
| description | Build a mobile-first, accessibility-optimized viewer page for non-technical audiences to watch agent-based simulations without seeing admin details, secrets, or technical controls |
| source | auto-skill |
| extracted_at | 2026-05-30T16:19:44.775Z |
Family Viewer Page
When building agent-based simulations that non-technical family members want to watch, create a dedicated viewer page that hides all admin details, provider settings, API keys, and technical controls while keeping the story readable and engaging.
Core Principles
- Mobile-first — optimize for phone screens first, then desktop
- Large text — minimum 22px body, 32px headings on mobile
- Simple language — replace technical terms with plain English
- No secrets — never expose API keys, provider config, model names, or raw JSON
- Source boundary visible — always show that generated events are not scripture/source truth
Route Setup
@app.get("/mom")
@app.get("/viewer")
def serve_mom_viewer():
"""Serve the family viewer page — no admin details, no secrets."""
ui_path = PROJECT_ROOT / "frontend" / "mom.html"
return FileResponse(ui_path)
@app.post("/api/mom/tick")
def mom_tick():
"""Advance one tick for the family viewer. Does NOT change provider mode."""
event = _advance_one_tick()
return {"status": "ok", "event": event}
@app.post("/api/mom/reset")
def mom_reset():
"""Reset the simulation for the family viewer."""
return reset_simulation()
Page Structure
Header: Title + subtitle
Source Notice: "This is a Bible-inspired simulation. It is not scripture."
Visual Scene: Garden sky, ground, Adam/Eve markers, boundary tree, peace indicator, animals
Chapter Card: Title, plain-language summary, Bible source
What This Means: One-sentence explanation of current chapter
Read Aloud Text: Clean paragraph for reading out loud
Tick Counter: "Moment N — Day N"
Extra Large Text Toggle: Persisted in localStorage
Controls: Next Moment (primary), Start Watching/Pause, Auto Play/Pause, Reset
Garden Summary: Day, Time, Garden state, Weather, Harmony, Boundary
Adam Card: What Adam is thinking, What Adam does
Eve Card: What Eve is thinking, What Eve does
What Changed: Plain-language description of world changes
Recent Moments: Latest 3 events with "Show more" button
Footer: Source boundary reminder
Visual Scene
Add a CSS-based visual scene above the chapter card that shows the garden state:
<div class="visual-scene">
<div class="scene-sky">
<span class="time-icon">🌅</span>
</div>
<div class="scene-ground">
<div class="scene-marker" id="adam-marker">
<span class="icon">👤</span>
<span class="label">Adam</span>
<span class="action-text">—</span>
</div>
<div class="scene-marker" id="eve-marker">
<span class="icon">👤</span>
<span class="label">Eve</span>
<span class="action-text">—</span>
</div>
</div>
<div class="boundary-tree calm">
<span class="icon">🌳</span>
<span class="label">Boundary</span>
</div>
<div class="peace-indicator">✨</div>
<div class="scene-animals">🦁 🐑 🦅</div>
<div class="scene-chapter-label">The Garden Is Good</div>
</div>
Visual State Mapping
| World State | Visual |
|---|
| Time: morning | 🌅 + warm sky (#fff3e0) |
| Time: midday | ☀️ + blue sky (#e3f2fd) |
| Time: evening | 🌇 + pink sky (#fce4ec) |
| Time: night | 🌙 + dark sky (#1a237e) |
| Garden: pristine | 🌿 + lush green |
| Garden: tended | 🌱 + cared-for green |
| Garden: wild | 🌾 + yellow-green |
| Garden: damaged | 🍂 + brown |
| Harmony ≥ 0.9 | ✨ Perfect peace |
| Harmony ≥ 0.7 | 🕊️ Peaceful |
| Harmony ≥ 0.4 | 🌤️ Uneasy |
| Harmony < 0.4 | ⛈️ Troubled |
| Boundary respected | 🌳 calm glow animation |
| Boundary violated | 🌳 no glow |
Safe Visual Data Endpoint
Create /api/mom/visual-state that returns ONLY sanitized visual data:
@app.get("/api/mom/visual-state")
def mom_visual_state():
"""Return only sanitized visual state. No provider config, no secrets."""
return {
"tick": eden.tick,
"day": eden.day,
"chapter": {"title": chapter.title},
"garden": {"bg": "🌿", "label": "Lush and green", "color": "#2d7d32"},
"time": {"icon": "🌅", "label": "Morning", "sky": "#fff3e0"},
"harmony": {"icon": "✨", "label": "Perfect peace", "color": "#2d7d32"},
"boundary": {"icon": "🛡️", "label": "Respected", "color": "#2d7d32"},
"adam": {"display_text": clean_text(adam.current_action, "Adam"), "present": True},
"eve": {"display_text": clean_text(eve.current_action, "Eve"), "present": True},
"animals": eden.animals_present[:3],
"latest_event_label": event_label,
}
Animations
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes marker-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}
@keyframes calm-glow {
0%, 100% { filter: drop-shadow(0 0 2px rgba(45,125,50,0.3)); }
50% { filter: drop-shadow(0 0 8px rgba(45,125,50,0.6)); }
}
@media (prefers-reduced-motion: reduce) {
.scene-sky .time-icon, .peace-indicator, .boundary-tree.calm .icon {
animation: none;
}
}
Watch Mode
Add a "Start Watching" button that advances one moment every 15 seconds:
let watchMode = false, watchInterval = null;
function toggleWatch() {
const btn = document.getElementById('btn-watch');
if (watchMode) {
clearInterval(watchInterval);
watchMode = false;
btn.textContent = '👁 Start Watching';
} else {
watchMode = true;
btn.textContent = '⏸ Pause Watching';
watchInterval = setInterval(async () => {
try { await postJSON('/api/mom/tick'); await refresh(); } catch (e) {}
}, 15000);
}
}
Rules for Watch Mode:
- Advances one safe tick at a time (uses
/api/mom/tick, NOT batch endpoint)
- 15-second interval (slow enough to read, fast enough to stay engaged)
- Can be paused at any time
- Resets when story is reset
- Never exposes provider settings or secrets
Mobile-First CSS
body {
font-size: 22px;
line-height: 1.6;
overflow-x: hidden;
}
.card {
width: 94vw;
margin: 0 3vw 1.2rem;
padding: 1.2rem 4vw;
}
.btn {
width: 100%;
min-height: 64px;
font-size: 24px;
padding: 1rem 1.5rem;
}
.controls {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
@media (min-width: 701px) {
.container { max-width: 700px; margin: 0 auto; }
.card { width: auto; margin-left: 0; margin-right: 0; }
.controls { flex-direction: row; justify-content: center; }
.btn { width: auto; min-width: 200px; }
}
@media (max-width: 390px) {
:root { --body-size: 20px; --heading-size: 28px; }
}
Simplify Wording
| Technical | Family Viewer |
|---|
| "Tick" | "Moment" (primary), "Tick" (small) |
| "CANON_ANCHOR" | "Bible source" |
| "INTERPRETIVE_BRIDGE" | "Interpretation" |
| "SIMULATION_EVENT" | "Simulation" |
| "Source Notice" | "Notice: This is a Bible-inspired simulation. It is not scripture." |
| "Reset Simulation" | "Reset Story" |
Clean Agent Text
Remove mock/provider prefixes from display:
function cleanText(text) {
if (!text) return '—';
text = text.replace(/^\[mock:[^\]]*\]\s*/i, '');
text = text.replace(/^\[nim-dry-run:[^\]]*\]\s*/i, '');
text = text.replace(/^\[nvidia_nim_placeholder:[^\]]*\]\s*/i, '');
text = text.replace(/^\[live-[^:]*:[^\]]*\]\s*/i, '');
text = text.replace(/^Processing:.*?\.\.\.\s*/i, '');
return text.trim() || '—';
}
Recent Moments — Show 3 by Default
const displayCount = showAllMoments ? events.length : Math.min(3, events.length);
showMoreBtn.style.display = events.length > 3 ? 'block' : 'none';
showMoreBtn.textContent = showAllMoments ? 'Show fewer moments' : 'Show more moments';
Extra Large Text Toggle
Persist in localStorage for accessibility:
if (localStorage.getItem('genesis-xl-mode') === 'true') {
document.body.classList.add('xl-mode');
document.getElementById('xl-toggle').checked = true;
}
function toggleXL() {
const isXL = document.getElementById('xl-toggle').checked;
document.body.classList.toggle('xl-mode', isXL);
localStorage.setItem('genesis-xl-mode', isXL);
}
XL mode CSS:
body.xl-mode {
--body-size: 26px;
--heading-size: 40px;
--button-size: 28px;
--card-size: 26px;
}
LAN Access
For family members on the same home network:
- Server must bind to
0.0.0.0 (not localhost)
- Open Windows Firewall for the port:
netsh advfirewall firewall add rule name="Genesis Kernel Local Viewer 19734" dir=in action=allow protocol=TCP localport=19734 profile=private
- Family member opens:
http://<PC-LAN-IP>:19734/mom
Export for Offline Viewing
def export():
data_dir = PROJECT_ROOT / "data"
eden = EdenState()
if (data_dir / "eden_state.json").exists():
eden.load_state(data_dir / "eden_state.json")
event_log = EventLog(log_path=data_dir / "events.jsonl")
event_log.load_all()
return {
"export_version": "1.0",
"world_state": {...},
"events": event_log.events,
"summary": event_log.summary(),
"notice": "This is a Bible-inspired simulation. Generated events are not scripture.",
}
Security Checklist
Anti-Patterns
- ❌ Showing provider settings, model names, or API status on /mom
- ❌ Using technical jargon ("tick", "CANON_ANCHOR", "provider")
- ❌ Small text or cramped layout on mobile
- ❌ Horizontal scrolling on phone screens
- ❌ Exposing raw mock/provider prefixes in agent text
- ❌ Making mom scroll through long event logs
- ✅ Mobile-first CSS with 94vw cards
- ✅ Large touch targets (64px+ buttons)
- ✅ Clean text with prefix stripping
- ✅ Show 3 moments by default with "Show more" button
- ✅ Extra Large Text toggle persisted in localStorage
- ✅ Simple, plain-language labels