← WIKI INDEX

FLUX DOCUMENTATION SYSTEM Layer 11 — STARTER KIT | starter-kit-configuration flux.dantesisofo.com/wiki/starter-kit-configuration/

STARTER KIT — CONFIGURATION & CONTRACTS

Implement this page first. Everything else depends on the config module, the naming function, and the two JSON schemas defined here.


1. CONFIG MODULE (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.


2. CANONICAL NAMING (THE NAMING AUTHORITY)

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".


3. SCHEMA — 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.


4. SCHEMA — 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.


5. FILE / DIRECTORY LAYOUT (LOCAL)

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.


6. ACCEPTANCE CHECKS

Next: SETUP.


FLUX_WIKI_v2.0 — flux.dantesisofo.com/wiki/starter-kit-configuration/