lib/dispatch/. Per-step Firestore state, resumable retries, cross-day rescue windows, cost tracking, audit logging. Sweeper runs every 3h.| Project | Firebase ID | GitHub Repo | Domain | Type | Status |
|---|---|---|---|---|---|
| Hub | epochblognet-hub |
ab-aidev/epoch-hub |
www.epochblog.net | Static HTML | live |
| Fashion Pub | epoch-fashion-pub |
ab-aidev/epoch-fashion-pub |
fashion.epochblog.net | SSG Static | live |
| Fashion Pipeline | fashion-epoch |
ab-aidev/epoch-fashion |
fashion-epoch.web.app | Backend | live |
| Traveler Pub | epoch-traveler-pub |
ab-aidev/epoch-traveler-pub |
traveler.epochblog.net | SSG Static | live |
| Traveler Pipeline | epoch-traveler |
ab-aidev/epoch-traveler |
epoch-traveler.web.app | Backend | live |
| Lab | epoch-lab |
ab-aidev/epoch-lab |
epoch-lab.web.app | Sandbox + Dashboard | live |
| Social | epoch-social-1697c |
ab-aidev/epoch-social |
(internal) | IG Publishing Pipeline | live |
| C-Dispatch | — | ab-aidev/epoch-dispatch |
— | Shared Library | live |
| C-Reel | fashion-epoch (vendored) |
ab-aidev/epoch-reel |
@epochblog · @epochblognet | Reels Pipeline (production) | live |
| Type | Name | Data | Purpose |
|---|---|---|---|
| CNAME | www | epochblognet-hub.web.app | Hub landing page |
| CNAME | fashion | epoch-fashion-pub.web.app | Fashion public site |
| CNAME | traveler | epoch-traveler-pub.web.app | Traveler public site |
| CNAME | lab | epoch-lab.web.app | Lab (internal) |
| Forward | epochblog.net | www.epochblog.net | Bare domain redirect |
C-Planner is the deterministic weekly agent that decides where S goes next. It runs once a week as
weeklyPlanner (Sun 10:00 UTC), scores 39 destinations against the current month and S's recent history,
picks a 3-city regional arc, and writes it to tripSchedule for the rest of the pipeline to pick up.
Zero LLM calls — pure scoring logic over a curated world file.
An arc = one complete trip abroad. It's the unit of planning. Each arc is decided once at the start of a home-active week, and contains exactly 3 cities picked together as a group. Once decided, nothing gets re-decided mid-trip — the planner fires every Sunday but silently skips while an arc is in progress. Cities in an arc share a region (not necessarily a country), so transit between them is short.
Lives at epoch-traveler/src/travel-world.json. Regions must match the transit table in
travel-agent.js. Each city is 7 days, each cluster is pre-paired via arcWith
so the scorer always returns a geographically coherent 3-city arc.
| # | Arc | Region | Cities | Best Months |
|---|---|---|---|---|
| 1 | Japan Alps | East Asia | Kanazawa · Takayama · Matsumoto | 4, 5, 10, 11 |
| 2 | Seto Inland Sea | East Asia | Naoshima · Onomichi · Hiroshima | 4, 5, 10, 11 |
| 3 | Hokkaido | East Asia | Hakodate · Otaru · Sapporo | 6, 7, 8, 9 |
| 4 | Korea | East Asia | Busan · Gyeongju · Jeonju | 4, 5, 9, 10, 11 |
| 5 | Taiwan | East Asia | Taipei · Tainan · Hualien | 11, 12, 1, 2, 3 |
| 6 | Portugal | Southern Europe | Lisbon · Porto · Évora | 3, 4, 5, 9, 10 |
| 7 | Andalusia | Southern Europe | Sevilla · Granada · Córdoba | 3, 4, 5, 10, 11 |
| 8 | Sicily | Southern Europe | Palermo · Catania · Siracusa | 4, 5, 9, 10 |
| 9 | Eastern France | Western Europe | Strasbourg · Dijon · Lyon | 5, 6, 9, 10 |
| 10 | Low Countries | Western Europe | Ghent · Antwerp · Utrecht | 5, 6, 7, 9 |
| 11 | Danube | Central Europe | Vienna · Bratislava · Budapest | 5, 6, 9, 10 |
| 12 | Silk Edge | Crossroads | Istanbul · Tbilisi · Yerevan | 4, 5, 9, 10 |
| 13 | Morocco | North Africa | Fez · Chefchaouen · Essaouira | 3, 4, 10, 11 |
On each run, C-Planner scores every city in the world file, picks the highest-scoring anchor,
then pulls its 2 companions from the arcWith list. Tie-breaking is a small random factor,
so picks feel organic rather than deterministic.
| Signal | Effect | Purpose |
|---|---|---|
| In-season month | +30 | Rotate through the calendar |
| Out-of-season month | −20 | Avoid bad weather |
| Never visited | +15 | Novelty bonus |
| Visited < 6 months ago | −100 | Hard block on recent repeats |
| Visited 6–12 months ago | −40 | Soft block, can still pick if starved |
| Visited > 12 months ago | +5 | Return visits allowed |
| Same region as last arc | −10 | Encourage regional variety |
| Random tiebreak | 0…10 | Prevent deterministic loops |
A "travel day" is a full day lost to airports, flights, jet lag, and settling in. S arrives in the evening of her last transit day and the trip begins the next morning.
Southern Europe · Western Europe · Central Europe · Scandinavia · Eastern Europe · North Africa · Crossroads · South America · Latin America
East Asia · Southeast Asia · South Asia · Oceania · Sub-Saharan Africa
| File | Role | Edit when… |
|---|---|---|
src/travel-agent.js |
Scoring + pickArc + scheduleArc + scheduleHome + planNext orchestrator | Tuning penalties, changing cycle rhythm, adding new regions to transit table |
src/travel-world.json |
Destination catalog (13 arcs, 39 cities) | Adding/removing cities, retuning bestMonths, growing the world past 12 months no-repeat |
src/world-firestore.js |
Character state (tripHistory, visitedCities, lastRegion) | Rare — only when onboarding a new character |
functions/index.js (weeklyPlanner) |
Cron wrapper around planNext() | Changing schedule day/time, adding secrets |
public/trips.html |
Manual override UI — seed trips, force arcs | Emergency manual scheduling |
| Issue | Impact | Fix |
|---|---|---|
| Arc ends on Sunday → next decision delayed by a week | Lead time shrinks to 1 day for that cycle; home-active posts may be clipped | Refactor planNext() to look ahead and plan the next arc while the current one is in progress |
lastRegion reads from tripHistory but older docs don't carry region metadata |
Region-variety penalty is a no-op until C-Planner trips flow through the system | Self-heals after a few arcs |
| No-repeat ceiling at ~12 months | After a year, 6–12mo penalty (−40) can still lose to novelty (+15) + random, so she may repeat a city | Grow world past 13 arcs, or tighten penalty to −200 |
| Off-season picks in Aug / Dec / Feb | Scorer picks "least bad" option when no arc is in-season (e.g. Morocco in August heat) | Add summer (Scandinavia, Baltics) and winter (SE Asia) arcs to the world file |
| Arc city order is by score, not geography | Within an arc, cities can end up in a non-optimal travel order (e.g. Évora → Porto → Lisbon) | Post-pick TSP/nearest-neighbor sort on the 3 chosen cities |
Two independent image pipelines feed the Traveler blog. Hero shots of S use
a two-stage process (background first, character composited into the background
second) because nothing else holds her face consistently. B-roll is text-to-image
only — no character — so it runs a different, cheaper model tuned for world
knowledge and text rendering. Every model is swappable via config flags in
src/traveler-config.js; no call sites need to change when we migrate.
| Pipeline | Stage | Model | Why | Config |
|---|---|---|---|---|
| Hero (S) | Stage 1 — background | gpt-image-1.5 high |
World knowledge, landmark accuracy, clean composition space for S to be inpainted into | blog-generate-v2.js |
| Stage 2 — composite S | fal-ai/flux-pro/v1.1-ultra (img2img or Flux Fill inpaint) |
Only model in testing that holds S's physical lock across eras and cities | TRAVELER_MODELS · id:304 |
|
| B-roll | Single-shot text-to-image | gpt-image-1.5 medium |
Best-in-class text rendering (menus, signs), strong world knowledge, ~$0.04/image, 17s gen time | BROLL_MODELS · id:401 |
Pre-rewrite, C-BRoll's system prompt instructed Flux to shoot an "empty world" with "no people, no landmarks" — which produced wet sidewalks and empty alleys, nothing a human would ever post. The new brief recasts C-BRoll as S holding her own phone, with a weighted archetype mix that mirrors what real travelers actually photograph:
| Archetype | Share | What it is |
|---|---|---|
| Food & Drink | 50% | Named dish from her own seat, plate or glass front-and-centre, menu/counter context in frame |
| View | 25% | Real named place she stopped to look at — the reason she raised her phone |
| Self-Presence | 15% | Her hand, foot, shadow, sleeve, or reflection — never her face, never a full body |
| Memento | 10% | Ticket, card, page, trinket she kept — held or resting on a surface |
S's entire hero-shot pipeline depends on Flux 1.1 Ultra holding her physical lock. If Black Forest Labs sunsets it, the migration path is:
2026-03-08
at a time when it shipped without multi-reference conditioning. A later release
added up to 10 reference images, which may fix character consistency.
Re-test before it's needed, not after Ultra is gone.
All swaps happen by flipping active: true in TRAVELER_MODELS
or BROLL_MODELS. No call-site edits. Each entry carries a startDate
and (for retired models) an endDate so the run log stays honest.
| Firebase Project | Collections | Storage Bucket |
|---|---|---|
| fashion-epoch | posts, weeklyBriefs, weeklyRetrospectives |
fashion-epoch.firebasestorage.app |
| epoch-traveler | travelerBlog/{tripId}/days, travelerWorld, tripSchedule, characters |
epoch-traveler.firebasestorage.app |
| epoch-lab | runs, imageTests (archived traveler data retained) |
epoch-lab.firebasestorage.app |
Pipelines are locked in a vault (API keys, service accounts, Firestore admin access). Public sites are just paper — pure static HTML with zero runtime dependencies. No Firebase SDK, no API calls, no secrets. SSG builds at deploy time, serves pre-rendered HTML.
✓ Hub — full CSP (script, style, img, connect, frame-ancestors)
✓ Fashion Pub — tight CSP, no connect-src needed
✓ Traveler Pub — tight CSP, no connect-src needed
✓ X-Frame-Options: DENY
✓ X-Content-Type-Options: nosniff
✓ Referrer-Policy: strict-origin-when-cross-origin
✓ Permissions-Policy: camera=(), microphone=(), geolocation=()
✓ All projects: owner-only read/write (rmbasualdo@gmail.com)
✓ Public sites never touch Firestore at runtime
✓ Traveler images: public read, owner-only write
✓ Everything else: owner-only
cd ~/AI-Projects/epoch-fashion # Full rebuild (all 953+ posts) node src/build-static.js --full node src/deploy-static.js # Incremental (after pipeline run) node src/build-static.js --incremental --post=DOC_ID node src/deploy-static.js # Feed only node src/build-static.js --feed-only
cd ~/AI-Projects/epoch-traveler # Full rebuild (all trips + days) node src/build-static.js --full node src/deploy-static.js # Incremental (single trip) node src/build-static.js --incremental --trip=TRIP_ID node src/deploy-static.js # Feed only node src/build-static.js --feed-only
cd ~/AI-Projects/epoch-hub firebase deploy --only hosting \ --project epochblognet-hub
cd ~/AI-Projects/epoch-traveler firebase deploy --only functions \ --project epoch-traveler # Set a secret firebase functions:secrets:set KEY_NAME \ --project epoch-traveler
travelerRoom/characters/roster/{id} to flat characters/{id} in epoch-traveler Firestore.weeklyPlanner Cloud Function was deployed Apr 6 but had never successfully run because travel-world.json didn't exist. Built 13 strict-3 arcs (39 cities) across 6 regions: East Asia (5), Southern Europe (3), Western Europe (2), Central Europe (1), Crossroads (1), North Africa (1). Full 12-month calendar coverage. Dry-run verified end-to-end; deployed to prod. First real fire: Sun Apr 12 10:00 UTC.src/broll-generate.js routes scenes to whichever BROLL_MODELS entry has active: true. Default is GPT Image 1.5 @ medium — chosen for world knowledge, text rendering (Japanese signage), and phone-camera-roll realism. Flux kept on the bench for fallback. Archetype-weighted shot mix: 50% food, 25% view, 15% self-presence, 10% memento./js/auth-gate.js. All 11 lab pages now load the same 3-script block (firebase-app + firebase-auth + auth-gate). Single owner email, single Firebase init, zero duplication. Auth gate locks body visibility before paint — no FOUC.scripts/backfill-day.js (full-pipeline replay + re-archive) and scripts/regen-broll-day.js (surgical B-roll fix). Root architectural issue: dailyBlog has no supervisor, no resumption, no alerting. See Roadmap below — this is the signal to build C-Director.functions/index.js but missed adding it to dailyBlog's secrets: [...] options array, so B-roll generation silently failed with "OPENAI_API_KEY missing" in production. Fixed + redeployed. Lesson: when a new provider is added, grep for every secrets: [ that calls the new module.src/blog-alert.js wraps dailyBlog in a try/catch and sends AB a dark-mode HTML email (via the existing GMAIL_USER / GMAIL_APP_PASSWORD secrets) on any throw. The email names the failing step (init → trip-loaded → weather → fashion → narrative → scenes → broll-extract → flux-images → broll-images → editor → layout → archive), the trip/city/day, the error + stack, and pre-filled backfill-day.js / regen-broll-day.js recovery commands. Interim band-aid — doesn't fix the all-or-nothing shape, just stops failures being silent until C-Director lands.saveTripJson() was writing to src/../trips/<id>.json unconditionally — fine locally, EROFS on Cloud Functions (everything outside /tmp is read-only). The failure was being swallowed by the per-trip catch, so dailyC0 on prod had never actually created a trip successfully since deploy; every trip AB has used was generated locally. Fix detects cloud via K_SERVICE || FUNCTION_TARGET and skips the write — Firestore is the canonical source. Local mirror still writes for dev runs.updatedAt within a 36h window. Any day regenerated out-of-schedule (backfill, manual re-trigger, C0 regen) silently broke the lookup. Switched to walking tripJson.mapValue.fields.days[] over the Firestore REST API and matching on isoDate === yesterday-in-America/Chicago. Gotcha discovered mid-fix: tripJson is stored as a nested map, not a JSON stringValue, so JSON.parse was the wrong instinct — walk the mapValue tree instead. Same fix applied to under-the-hood.js:fetchTravelerDay.dailyBlog freight train with a supervisor that
tracks step state in Firestore, resumes on failure, retries across days, and supports
human-review gates. A single step failure currently aborts the whole day with
nothing saved — exactly what happened to Osaka Day 6 Friday (Anthropic 401 at
C-Fashion wiped out the Sumiyoshi anchor day).
src/blog-alert.js
wraps dailyBlog in try/catch and sends a dark-mode HTML email on any throw,
naming the step that died, the trip/city/day context, the error + stack, and pre-filled
backfill-day.js / regen-broll-day.js recovery commands.
Uses the existing GMAIL_USER / GMAIL_APP_PASSWORD secrets already in dailyBlog's bind.
Does not fix the all-or-nothing shape — C-Director is still the real fix.
saveTripJson() was writing to
src/../trips/<id>.json unconditionally — EROFS on Cloud Functions
(read-only FS outside /tmp). The error was swallowed by the per-trip catch
so dailyC0 on prod had never actually created a trip since deploy; every trip used so
far was generated by running node src/blog-c0.js locally. Fix detects
cloud via K_SERVICE || FUNCTION_TARGET and skips the write — Firestore
is canonical. Local mirror still writes for dev runs.
scripts/backfill-day.js duplicates ~300 lines of dailyBlog's pipeline
body. Every time dailyBlog grows a new step, backfill-day must grow it too or drift
silently. Once C-Director exists, both scripts collapse into thin CLI wrappers:
director.runJob(graph, { forceDate: ... }) and
director.retryStep(jobId, 'brollImages'). Until then: keep the two
files mentally linked.
| Phase | Description | Date | Status |
|---|---|---|---|
| Phase 1 | Hub gets its own hangar | 2026-04-06 | complete |
| Phase 2 | Fashion public site (SSG) | 2026-04-06 | complete |
| Phase 3 | Extract traveler from lab + data migration | 2026-04-07 | complete |
| Phase 4 | Traveler public site (SSG) | 2026-04-07 | complete |
| Phase 5 | DNS, security headers, hub tile activation, cleanup | 2026-04-07 | complete |