FLUX DOCUMENTATION SYSTEM Layer 11 — STARTER KIT | starter-kit-configuration flux.dantesisofo.com/wiki/starter-kit-configuration/
Implement this page first. Everything else depends on the config module, the naming function, and the two JSON schemas defined here.
config.py)One module reads .env (via python-dotenv) and exposes constants. No
identifier may be hardcoded anywhere else.
| Env var | Meaning | Example |
|---|---|---|
FLUX_BUCKET |
S3 bucket name | your-cityflux-site |
FLUX_CF_DIST |
CloudFront distribution ID | E0000000000000 |
FLUX_REGION |
AWS region | us-east-1 |
FLUX_BASE_URL |
public base URL | https://your-domain |
FLUX_AUTHOR |
photographer/display name | Your Name |
COLLECTION |
hub membership tag | boston-in-flux |
CORRIDOR_TARGET |
coverage goal (int) | 50 |
ROUTE_COLOR |
route accent hex | #b84000 |
TILE_URL |
Leaflet tile template | CARTO light_all |
PORTAL_USER / PORTAL_PASS |
local portal auth | — |
ANTHROPIC_API_KEY |
optional AI about-text | blank = template |
Derived constants: HUB_KEY="hub/index.html", EMBED_KEY="hub/embed/index.html",
LISTING_KEY="catalog/index.html", CATALOG_KEY="data/catalog.json",
HUB_URL="/hub/", EMBED_URL="/hub/embed/", ROUTE_MAX_POINTS=120.
One street name → every identifier. Deterministic, pure. Implement exactly:
STREET_TYPES = { # normalize street-type tokens to canonical abbreviations
"avenue":"AVE","ave":"AVE","av":"AVE","street":"STREET","st":"STREET","str":"STREET",
"boulevard":"BLVD","blvd":"BLVD","road":"RD","rd":"RD","drive":"DR","dr":"DR",
"lane":"LN","ln":"LN","place":"PL","pl":"PL","court":"CT","ct":"CT",
"highway":"HWY","hwy":"HWY","parkway":"PKWY","pkwy":"PKWY","pike":"PIKE",
"terrace":"TER","ter":"TER",
}
def canonical(street_name: str) -> dict:
words = re.sub(r"[^A-Za-z0-9 ]+", " ", street_name or "")
words = re.sub(r"\s+", " ", words).strip()
if not words:
return {"title":"","slug":"","folder":"","export_name":"","collections":[COLLECTION]}
toks = [STREET_TYPES.get(w.lower(), w.upper()) for w in words.split(" ")]
title = "_".join(toks) + "_IN_FLUX" # e.g. PASSYUNK_AVE_IN_FLUX
slug = slugify(" ".join(toks) + " in flux") # e.g. passyunk-ave-in-flux
return {"title":title, "slug":slug, "folder":slug,
"export_name":f"{slug}-project.zip", "collections":[COLLECTION]}
def slugify(text): # lower, non-alnum -> '-', collapse, trim, cap length
return re.sub(r"[^a-z0-9]+","-",text.lower()).strip("-")[:60].strip("-")
Contract tests: canonical("Passyunk Ave").title == "PASSYUNK_AVE_IN_FLUX";
canonical("Passyunk Avenue").slug == "passyunk-ave-in-flux";
canonical("Broad Street").title == "BROAD_STREET_IN_FLUX".
catalog.json (THE INDEX / SOURCE OF TRUTH)Lives at s3://YOUR_BUCKET/data/catalog.json, mirrored locally. The hub, embed,
and listing render only from this file.
{
"schema_version": "1.1",
"last_updated": "2026-06-09T12:00:00Z",
"reserved_slugs": ["passyunk-ave-in-flux", ...], // tombstones; never reuse
"walks": [ WalkRecord, ... ]
}
// WalkRecord:
{
"walk_id": "WALK_001", // sequential, frozen
"slug": "passyunk-ave-in-flux", // frozen once published
"title": "PASSYUNK_AVE_IN_FLUX",
"photographer_name": "Your Name",
"location_text": "Passyunk Ave, Your City",
"date_range": "2026-06-06",
"total_photos": 118,
"geotagged_count": 116,
"distance_km": 9.0, // CANONICAL unit = km
"duration_minutes": 154,
"duration_formatted": "2h 34m",
"cover_image": "passyunk-ave-in-flux/large/<photo_id>.jpg",
"public_url": "/passyunk-ave-in-flux/",
"published_at": "2026-06-06T18:00:00Z",
"collections": ["boston-in-flux"], // hub membership (array)
"route": [[39.913,-75.242], ...], // downsampled <= ROUTE_MAX_POINTS
"bbox": [minLat, minLon, maxLat, maxLon]
}
Rules: slug and walk_id are immutable after first publish; removed slugs
stay in reserved_slugs. route/bbox are an index projection of the full route
(the full route stays in the draft) — storing them here lets the hub render with
zero per-project fetches.
submission.json (THE DRAFT)One per project, in the draft run folder. The Worker writes it; the Publisher reads it. This is the draft — there is no separate draft model.
{
"schema_version": "1.0",
"slug": "passyunk-ave-in-flux",
"status": "new|uploaded|generating|draft_ready|publishing|published|error",
"created_at": "...", "updated_at": "...",
"submitter": { "name": "Your Name", "email": "" },
"walk": {
"title": "PASSYUNK_AVE_IN_FLUX",
"location_text": "Passyunk Ave, Your City",
"collections": ["boston-in-flux"] // explicit hub membership
},
"photos": [ Photo, ... ],
"stats": Stats,
"about_generated": "A walk through ...",
"about_source": "ai|template",
"contact_sheet": "contact_sheet.png",
"zine": { "selection": ["<photo_id>", ...] } // optional final 36; else null
}
// Photo:
{
"id": "<sha256[:16]>", "seq": "001",
"original_filename": "R0010001.JPG",
"timestamp": "2026:06:06 11:52:17", "timestamp_unix": 1781000000,
"lat": 39.913, "lon": -75.242, "address": "Passyunk Avenue, ...",
"make": "...", "model": "...", "lens": "...", "focal": "18mm",
"aperture": "f/8.0", "shutter": "...", "iso": "ISO 200",
"width": 3504, "height": 2336, "file_size": 4500000,
"geotagged": true,
"large": "large/<id>.jpg", "thumb": "thumbs/<id>.jpg"
}
// Stats:
{
"total_photos": 118, "geotagged_count": 116, "geotagged_pct": 98,
"duration_minutes": 154, "duration_formatted": "2h 34m",
"date_range": "2026-06-06", "start_time": "...", "end_time": "...",
"distance_km": 9.0,
"route": [[lat,lon], ...], // FULL route (not downsampled)
"first_photo_id": "...", "last_photo_id": "..."
}
photo.id = sha256(file)[:16] (dedup key + derivative filename). seq is assigned
after chronological sort by timestamp_unix.
cityflux/
config.py # §1
naming.py # §2 canonical()
worker.py # ARCHITECTURE
publisher.py # DEPLOYMENT
deploy.py # aws s3 sync wrapper (full-site)
portal/app.py # optional Flask studio
zine/ # client-side PDF + vendored leaflet/qrcode/jspdf
data/catalog.json # the index (seed: empty walks[])
_drafts/<slug>/ # per-project: originals/ large/ thumbs/ submission.json contact_sheet.png
.env .env.example .gitignore requirements.txt
_drafts/, .env, data/catalog.json (except an empty seed) are gitignored.
config raises a clear error if FLUX_BUCKET/FLUX_CF_DIST/FLUX_BASE_URL are unset.canonical() passes the §2 contract tests.catalog.json (one WalkRecord) validates against §3 and
the hub builder renders it.Next: SETUP.
FLUX_WIKI_v2.0 — flux.dantesisofo.com/wiki/starter-kit-configuration/