[object Object]

← back to Ventura Corridor

Virtual Blvd Gallery + 3-tier business store · 3am autonomous build

f95cfa2ee74f2402f9f19033fc0b9cb33c133a31 · 2026-05-12 11:20:15 -0700 · SteveStudio2

Per Steve via debate-team-fast: virtual art gallery aggregating imagined
art per Ventura Blvd business + tiered listing store. Runs autonomously
at 03:07 local tonight via launchd.

NEW Postgres schema (002_gallery_tiers.sql, applied):
- businesses extended: tier (default basic), stripe_customer_id,
  stripe_subscription_id, tier_expires_at
- gallery_pieces: one row per business, AI-generated art metadata
  (art_title, art_medium, art_year_est, art_description, color_palette
  JSONB, lon/lat, neighborhood, position_pct 0-100 east-to-west,
  generator='corridor-design-system' — never names vendor per Steve's
  'NEVER expose AI software' rule)
- tier_pricing: per category × tier (basic/featured/spotlight) with
  price_cents + display_name + marketing_copy JSONB
- tier_assets: business uploads (logo, photo, video_url, chef_bio,
  booking_url, bar_number)
- tier_leads: premium-tier lead-magnet captures

scripts/generate-gallery-pieces.js — qwen3:14b art generator:
- Priority queue: restaurants > cafes > salons > hotels > lawyers > etc.
- For each business: structured-JSON prompt → art_title, art_medium,
  art_year_est, art_description, 3-hex color_palette
- INSERT ON CONFLICT (business_id) DO NOTHING — idempotent
- Position mapping: lon -118.510 (Encino) → 0%, -118.380 (Studio City)
  → 100%, so the gallery scrolls east-to-west naturally
- Neighborhood derived from lon thresholds

scripts/build-virtual-gallery.sh — orchestrator:
- Applies schema migration (idempotent IF NOT EXISTS)
- Seeds tier_pricing (24+ rows, ON CONFLICT DO NOTHING)
- Runs the qwen3 generator for top 2000 businesses by category priority
- Logs everything to logs/build-virtual-gallery-<date>.log
- Writes final stats to logs/build-virtual-gallery-summary.txt

launchd/com.steve.ventura-gallery-3am.plist (installed to
~/Library/LaunchAgents/ + loaded):
- StartCalendarInterval: hour=3, minute=7 (avoid the :00 / :30 cron
  herd per cron best practice)
- PATH includes homebrew + postgresql@14 so psql + node + ollama all
  resolve
- StandardOut/Err logged to project logs/

Smoke test result: 1 business generated end-to-end in ~10 seconds.
Sample: 'Crimson Blooms, No. 12' · mural · Studio City · pos 88.78%
· palette [#9E1B32, #F06292, #F5F5F5].

Reversible: TRUNCATE gallery_pieces RESTART IDENTITY rolls back the
entire art set. tier_pricing/tier_assets/tier_leads safe to drop too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit f95cfa2ee74f2402f9f19033fc0b9cb33c133a31
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 11:20:15 2026 -0700

    Virtual Blvd Gallery + 3-tier business store · 3am autonomous build
    
    Per Steve via debate-team-fast: virtual art gallery aggregating imagined
    art per Ventura Blvd business + tiered listing store. Runs autonomously
    at 03:07 local tonight via launchd.
    
    NEW Postgres schema (002_gallery_tiers.sql, applied):
    - businesses extended: tier (default basic), stripe_customer_id,
      stripe_subscription_id, tier_expires_at
    - gallery_pieces: one row per business, AI-generated art metadata
      (art_title, art_medium, art_year_est, art_description, color_palette
      JSONB, lon/lat, neighborhood, position_pct 0-100 east-to-west,
      generator='corridor-design-system' — never names vendor per Steve's
      'NEVER expose AI software' rule)
    - tier_pricing: per category × tier (basic/featured/spotlight) with
      price_cents + display_name + marketing_copy JSONB
    - tier_assets: business uploads (logo, photo, video_url, chef_bio,
      booking_url, bar_number)
    - tier_leads: premium-tier lead-magnet captures
    
    scripts/generate-gallery-pieces.js — qwen3:14b art generator:
    - Priority queue: restaurants > cafes > salons > hotels > lawyers > etc.
    - For each business: structured-JSON prompt → art_title, art_medium,
      art_year_est, art_description, 3-hex color_palette
    - INSERT ON CONFLICT (business_id) DO NOTHING — idempotent
    - Position mapping: lon -118.510 (Encino) → 0%, -118.380 (Studio City)
      → 100%, so the gallery scrolls east-to-west naturally
    - Neighborhood derived from lon thresholds
    
    scripts/build-virtual-gallery.sh — orchestrator:
    - Applies schema migration (idempotent IF NOT EXISTS)
    - Seeds tier_pricing (24+ rows, ON CONFLICT DO NOTHING)
    - Runs the qwen3 generator for top 2000 businesses by category priority
    - Logs everything to logs/build-virtual-gallery-<date>.log
    - Writes final stats to logs/build-virtual-gallery-summary.txt
    
    launchd/com.steve.ventura-gallery-3am.plist (installed to
    ~/Library/LaunchAgents/ + loaded):
    - StartCalendarInterval: hour=3, minute=7 (avoid the :00 / :30 cron
      herd per cron best practice)
    - PATH includes homebrew + postgresql@14 so psql + node + ollama all
      resolve
    - StandardOut/Err logged to project logs/
    
    Smoke test result: 1 business generated end-to-end in ~10 seconds.
    Sample: 'Crimson Blooms, No. 12' · mural · Studio City · pos 88.78%
    · palette [#9E1B32, #F06292, #F5F5F5].
    
    Reversible: TRUNCATE gallery_pieces RESTART IDENTITY rolls back the
    entire art set. tier_pricing/tier_assets/tier_leads safe to drop too.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 db/002_gallery_tiers.sql                    |  67 ++++++++++++
 launchd/com.steve.ventura-gallery-3am.plist |  33 ++++++
 scripts/build-virtual-gallery.sh            | 110 +++++++++++++++++++
 scripts/generate-gallery-pieces.js          | 163 ++++++++++++++++++++++++++++
 4 files changed, 373 insertions(+)

diff --git a/db/002_gallery_tiers.sql b/db/002_gallery_tiers.sql
new file mode 100644
index 0000000..3b52456
--- /dev/null
+++ b/db/002_gallery_tiers.sql
@@ -0,0 +1,67 @@
+-- 002_gallery_tiers.sql — The Blvd Gallery + tiered business store.
+-- Designed by debate-team-fast 2026-05-12. Per-business AI-generated art
+-- metadata + 3-tier subscription ladder per category.
+
+-- Tier columns on businesses (basic free; featured/premium paid)
+ALTER TABLE businesses ADD COLUMN IF NOT EXISTS tier VARCHAR(20) DEFAULT 'basic';
+ALTER TABLE businesses ADD COLUMN IF NOT EXISTS stripe_customer_id VARCHAR(64);
+ALTER TABLE businesses ADD COLUMN IF NOT EXISTS stripe_subscription_id VARCHAR(64);
+ALTER TABLE businesses ADD COLUMN IF NOT EXISTS tier_expires_at TIMESTAMPTZ;
+
+CREATE INDEX IF NOT EXISTS businesses_tier ON businesses(tier) WHERE tier != 'basic';
+
+-- Gallery pieces (one per business, generated overnight)
+CREATE TABLE IF NOT EXISTS gallery_pieces (
+  id            SERIAL PRIMARY KEY,
+  business_id   INT REFERENCES businesses(id) ON DELETE CASCADE,
+  art_title     TEXT NOT NULL,
+  art_medium    VARCHAR(60),          -- mural, print, sculpture, neon, ceramic, etc.
+  art_year_est  INT,
+  art_description TEXT,
+  color_palette JSONB,                -- ["#c84b31","#f5e6c8","#2d4a3e"]
+  image_url     TEXT,
+  image_source  VARCHAR(20),          -- 'ai_generated', 'user_upload', 'placeholder'
+  lon           DECIMAL(10,7),
+  lat           DECIMAL(10,7),
+  neighborhood  TEXT,                 -- Studio City | Sherman Oaks | Encino
+  position_pct  NUMERIC(5,2),         -- 0.00 (east, Encino) → 100.00 (west, Studio City)
+  generator     VARCHAR(40) DEFAULT 'corridor-design-system',  -- user-visible: never vendor name
+  created_at    TIMESTAMPTZ DEFAULT NOW(),
+  UNIQUE (business_id)
+);
+
+CREATE INDEX IF NOT EXISTS gallery_pos        ON gallery_pieces(position_pct);
+CREATE INDEX IF NOT EXISTS gallery_neighbor   ON gallery_pieces(neighborhood);
+CREATE INDEX IF NOT EXISTS gallery_business   ON gallery_pieces(business_id);
+
+-- Tier pricing (per category × tier)
+CREATE TABLE IF NOT EXISTS tier_pricing (
+  category        VARCHAR(80) NOT NULL,
+  tier            VARCHAR(20) NOT NULL,
+  display_name    TEXT NOT NULL,
+  price_cents     INT NOT NULL,
+  stripe_price_id VARCHAR(64),
+  marketing_copy  JSONB,              -- { title, subtitle, bullets[], cta }
+  created_at      TIMESTAMPTZ DEFAULT NOW(),
+  PRIMARY KEY (category, tier)
+);
+
+-- Per-business uploaded / paid-tier assets
+CREATE TABLE IF NOT EXISTS tier_assets (
+  id            SERIAL PRIMARY KEY,
+  business_id   INT NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
+  asset_type    VARCHAR(40),          -- logo, photo, video_url, chef_bio, bar_number, booking_url
+  asset_value   TEXT,
+  sort_order    INT DEFAULT 0,
+  created_at    TIMESTAMPTZ DEFAULT NOW()
+);
+CREATE INDEX IF NOT EXISTS tier_assets_business ON tier_assets(business_id);
+
+-- Lead capture (premium tier feature for law firms / salons)
+CREATE TABLE IF NOT EXISTS tier_leads (
+  id            SERIAL PRIMARY KEY,
+  business_id   INT REFERENCES businesses(id),
+  lead_data     JSONB,
+  emailed_at    TIMESTAMPTZ,
+  created_at    TIMESTAMPTZ DEFAULT NOW()
+);
diff --git a/launchd/com.steve.ventura-gallery-3am.plist b/launchd/com.steve.ventura-gallery-3am.plist
new file mode 100644
index 0000000..88ea12e
--- /dev/null
+++ b/launchd/com.steve.ventura-gallery-3am.plist
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>Label</key>
+  <string>com.steve.ventura-gallery-3am</string>
+
+  <key>ProgramArguments</key>
+  <array>
+    <string>/bin/bash</string>
+    <string>-c</string>
+    <string>export PATH="/opt/homebrew/bin:/opt/homebrew/opt/postgresql@14/bin:/usr/local/bin:/usr/bin:/bin:$PATH" &amp;&amp; cd /Users/stevestudio2/Projects/ventura-corridor &amp;&amp; bash scripts/build-virtual-gallery.sh</string>
+  </array>
+
+  <key>StartCalendarInterval</key>
+  <dict>
+    <key>Hour</key>
+    <integer>3</integer>
+    <key>Minute</key>
+    <integer>7</integer>
+  </dict>
+
+  <key>StandardOutPath</key>
+  <string>/Users/stevestudio2/Projects/ventura-corridor/logs/launchd-gallery.out</string>
+  <key>StandardErrorPath</key>
+  <string>/Users/stevestudio2/Projects/ventura-corridor/logs/launchd-gallery.err</string>
+
+  <key>RunAtLoad</key>
+  <false/>
+  <key>AbandonProcessGroup</key>
+  <false/>
+</dict>
+</plist>
diff --git a/scripts/build-virtual-gallery.sh b/scripts/build-virtual-gallery.sh
new file mode 100755
index 0000000..95aad90
--- /dev/null
+++ b/scripts/build-virtual-gallery.sh
@@ -0,0 +1,110 @@
+#!/bin/bash
+# build-virtual-gallery.sh — autonomous 3am build for The Blvd Gallery.
+# Per Steve's overnight rules: local Ollama only (qwen3:14b), no Claude API,
+# no email/DNS/destructive ops, every step reversible.
+#
+# What it does:
+#   1. Apply 002_gallery_tiers.sql migration (idempotent — IF NOT EXISTS)
+#   2. Seed tier_pricing table with consensus prices (idempotent — ON CONFLICT)
+#   3. Generate art metadata via qwen3:14b for top 2000 businesses by category
+#      priority (restaurants > salons > hotels > cafes > law firms > clinics
+#      > shops > rest). Batched, one INSERT per business. Each is independent
+#      and idempotent (UNIQUE on business_id).
+#   4. Write a run log to logs/build-virtual-gallery-<date>.log
+#   5. Final stats line written to logs/build-virtual-gallery-summary.txt
+#
+# Reversible: every INSERT is keyed on business_id. To roll back:
+#   psql -d ventura_corridor -c "TRUNCATE gallery_pieces RESTART IDENTITY;"
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+ROOT="$(pwd)"
+LOG_DIR="$ROOT/logs"
+mkdir -p "$LOG_DIR"
+DATE=$(date +%Y-%m-%d_%H%M)
+LOG="$LOG_DIR/build-virtual-gallery-$DATE.log"
+
+exec > >(tee -a "$LOG") 2>&1
+
+echo "════════════════════════════════════════════════════════════════"
+echo "  The Blvd Gallery · autonomous build"
+echo "  start: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
+echo "════════════════════════════════════════════════════════════════"
+
+# ── 1. Schema migration ─────────────────────────────────────────────
+echo
+echo "── 1. Applying schema migration ──"
+psql -d ventura_corridor -f db/002_gallery_tiers.sql | tail -20
+
+# ── 2. Seed tier_pricing ────────────────────────────────────────────
+echo
+echo "── 2. Seeding tier_pricing ──"
+psql -d ventura_corridor <<'SQL'
+INSERT INTO tier_pricing (category, tier, display_name, price_cents, marketing_copy) VALUES
+  -- Restaurants: Free / Featured $19 / Chef-Spotlight $59
+  ('amenity: restaurant',  'basic',     'Basic listing',         0,
+   '{"title":"Free listing","subtitle":"OSM-sourced name, address, phone, hours","bullets":["Always free","Auto-updated from OpenStreetMap"],"cta":"Already live"}'),
+  ('amenity: restaurant',  'featured',  'Featured Restaurant',   1900,
+   '{"title":"Featured · $19/mo","subtitle":"Logo + 5 photos + boosted sort","bullets":["Custom logo","Up to 5 dining-room photos","Boosted in browse + search","Cuisine + dietary tags","Featured ribbon"],"cta":"Light up the listing"}'),
+  ('amenity: restaurant',  'spotlight', 'Chef-Spotlight',        5900,
+   '{"title":"Chef-Spotlight · $59/mo","subtitle":"Chef bio + video + featured dish","bullets":["Everything in Featured","Chef bio + headshot","60-second chef video","Featured-dish callout w/ price","Weekly nearby-user email digest","Priority placement"],"cta":"Be the corridor''s named chef"}'),
+
+  -- Law firms: Free / Verified $29 / Lead-Magnet $79
+  ('office: lawyer',       'basic',     'Basic listing',         0,
+   '{"title":"Free listing","subtitle":"Name, address, phone","bullets":["Always free"],"cta":"Already live"}'),
+  ('office: lawyer',       'verified',  'Practice-Verified',     2900,
+   '{"title":"Practice-Verified · $29/mo","subtitle":"5 practice areas + State Bar # + Verified badge","bullets":["Up to 5 practice areas with descriptions","State Bar number displayed","Consultation request form","Attorney headshot","Verified badge"],"cta":"Get verified"}'),
+  ('office: lawyer',       'spotlight', 'Lead-Magnet',           7900,
+   '{"title":"Lead-Magnet · $79/mo","subtitle":"Intake form + priority placement","bullets":["Everything in Verified","AI-powered intake form (case pre-screening)","Weekly lead digest delivered","Priority placement in browse + search","Map-pin highlight"],"cta":"Open the lead pipe"}'),
+
+  -- Salons / Hairdresser: Free / Gallery $15 / Book-Online $39
+  ('shop: hairdresser',    'basic',     'Basic listing',         0,    NULL),
+  ('shop: hairdresser',    'featured',  'Gallery · $15/mo',      1500, NULL),
+  ('shop: hairdresser',    'spotlight', 'Book-Online · $39/mo',  3900, NULL),
+  ('shop: beauty',         'basic',     'Basic listing',         0,    NULL),
+  ('shop: beauty',         'featured',  'Gallery · $15/mo',      1500, NULL),
+  ('shop: beauty',         'spotlight', 'Book-Online · $39/mo',  3900, NULL),
+
+  -- Cafes: Free / Featured $15 / Local-Favorite $39
+  ('amenity: cafe',        'basic',     'Basic',                  0, NULL),
+  ('amenity: cafe',        'featured',  'Featured · $15/mo',   1500, NULL),
+  ('amenity: cafe',        'spotlight', 'Local-Favorite · $39/mo', 3900, NULL),
+
+  -- Hotels: Free / Featured $39 / Concierge $99
+  ('tourism: hotel',       'basic',     'Basic',                  0, NULL),
+  ('tourism: hotel',       'featured',  'Featured · $39/mo',   3900, NULL),
+  ('tourism: hotel',       'spotlight', 'Concierge · $99/mo',  9900, NULL),
+
+  -- Dentists/clinics: Free / Verified $29 / Premium $79
+  ('amenity: dentist',     'basic',     'Basic',                  0, NULL),
+  ('amenity: dentist',     'featured',  'Verified · $29/mo',   2900, NULL),
+  ('amenity: dentist',     'spotlight', 'Premium · $79/mo',    7900, NULL),
+  ('amenity: clinic',      'basic',     'Basic',                  0, NULL),
+  ('amenity: clinic',      'featured',  'Verified · $29/mo',   2900, NULL),
+  ('amenity: clinic',      'spotlight', 'Premium · $79/mo',    7900, NULL)
+ON CONFLICT (category, tier) DO NOTHING;
+SQL
+echo "tier_pricing seeded — $(psql -d ventura_corridor -tA -c 'SELECT COUNT(*) FROM tier_pricing') rows"
+
+# ── 3. Generate gallery pieces via qwen3:14b ────────────────────────
+echo
+echo "── 3. Generating gallery_pieces via local qwen3:14b ──"
+node scripts/generate-gallery-pieces.js --limit 2000 2>&1
+
+# ── 4. Final summary ────────────────────────────────────────────────
+echo
+echo "── Summary ──"
+SUMMARY=$(psql -d ventura_corridor -tA <<'SQL'
+SELECT 'gallery_pieces:    ' || COUNT(*) FROM gallery_pieces;
+SELECT 'tier_pricing:      ' || COUNT(*) FROM tier_pricing;
+SELECT 'businesses (tier!=basic): ' || COUNT(*) FROM businesses WHERE tier != 'basic';
+SQL
+)
+echo "$SUMMARY"
+echo "$SUMMARY" > "$LOG_DIR/build-virtual-gallery-summary.txt"
+
+echo
+echo "════════════════════════════════════════════════════════════════"
+echo "  Build complete: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
+echo "  Log: $LOG"
+echo "════════════════════════════════════════════════════════════════"
diff --git a/scripts/generate-gallery-pieces.js b/scripts/generate-gallery-pieces.js
new file mode 100644
index 0000000..dced859
--- /dev/null
+++ b/scripts/generate-gallery-pieces.js
@@ -0,0 +1,163 @@
+#!/usr/bin/env node
+// generate-gallery-pieces.js — for each business, generate AI art metadata via
+// local qwen3:14b. INSERT into gallery_pieces idempotent on business_id.
+// Designed to run autonomously at 3am (no Claude, no paid APIs).
+//
+// Category priority (highest first): restaurants, salons, hotels, cafes,
+// law firms, clinics, dentists, then the rest. Stops at --limit.
+
+const { Client } = require('pg');
+
+const args = process.argv.slice(2);
+const arg = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i+1] : d; };
+const LIMIT = parseInt(arg('--limit', '2000'), 10);
+
+const PRIORITY = [
+  'amenity: restaurant',
+  'amenity: fast food',
+  'amenity: cafe',
+  'shop: hairdresser',
+  'shop: beauty',
+  'tourism: hotel',
+  'office: lawyer',
+  'amenity: dentist',
+  'amenity: clinic',
+  'shop: clothes',
+  'shop: supermarket',
+  'shop: department store',
+  'office: company',
+];
+
+// Ventura Blvd runs east-west. Approximate lon bounds (rough):
+//   East end (Encino):       lon ≈ -118.510
+//   West end (Studio City):  lon ≈ -118.380
+const LON_EAST = -118.510;
+const LON_WEST = -118.380;
+
+function neighborhoodFor(lon) {
+  if (lon == null) return 'Sherman Oaks';
+  if (lon <= -118.470) return 'Encino';
+  if (lon <= -118.430) return 'Sherman Oaks';
+  return 'Studio City';
+}
+function positionPctFor(lon) {
+  if (lon == null) return null;
+  const pct = ((lon - LON_EAST) / (LON_WEST - LON_EAST)) * 100;
+  return Math.max(0, Math.min(100, +pct.toFixed(2)));
+}
+
+async function qwen3(prompt) {
+  const r = await fetch('http://127.0.0.1:11434/api/chat', {
+    method: 'POST', headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      model: 'qwen3:14b',
+      stream: false, think: false,
+      format: 'json',
+      options: { temperature: 0.85, num_predict: 400 },
+      messages: [
+        { role: 'system', content: `/no_think
+You are an art curator imagining the artwork visible inside or outside a Ventura Boulevard business. Each business gets ONE imagined piece of art — a mural, framed print, sculpture, neon sign, ceramic, installation, woven hanging, photograph, sign-painting, or whatever fits the business's vibe. The art doesn't have to literally exist — you're imagining what art SHOULD be there, in keeping with the business's character, neighborhood, and category.
+
+Output STRICT JSON only:
+{
+  "art_title": "<3-6 word evocative title — '[Series Name], No. <N>' format welcome — e.g. 'The Tide Pool Series, No. 3'>",
+  "art_medium": "<one of: mural, framed-print, sculpture, neon, ceramic, photograph, sign-painting, woven, installation, mosaic, oil, watercolor>",
+  "art_year_est": <integer year between 1965 and 2025>,
+  "art_description": "<2-3 sentence prose description of the imagined piece, where it lives in the space, what it depicts. Evocative, in the voice of an art critic.>",
+  "color_palette": ["<#hex>","<#hex>","<#hex>"]
+}
+
+No <think> blocks. Just the JSON.` },
+        { role: 'user', content: prompt },
+      ],
+    }),
+  });
+  if (!r.ok) throw new Error('ollama http ' + r.status);
+  const j = await r.json();
+  const raw = (j.message?.content || '').replace(/<think>[\s\S]*?<\/think>/g, '').trim();
+  try { return JSON.parse(raw); }
+  catch { const m = raw.match(/\{[\s\S]*\}/); return m ? JSON.parse(m[0]) : null; }
+}
+
+async function generateFor(b) {
+  const lon = b.lon != null ? Number(b.lon) : null;
+  const lat = b.lat != null ? Number(b.lat) : null;
+  const neighborhood = neighborhoodFor(lon);
+  const positionPct = positionPctFor(lon);
+
+  const prompt = `Business: ${b.name}
+Category: ${b.category}
+Address: ${b.address || 'Ventura Blvd, ' + neighborhood}
+Neighborhood: ${neighborhood}
+
+Imagine the single most striking piece of art visible to a visitor or passerby of this business. Generate the JSON.`;
+
+  const piece = await qwen3(prompt);
+  if (!piece || !piece.art_title) return null;
+
+  return {
+    business_id: b.id,
+    art_title: String(piece.art_title).slice(0, 200),
+    art_medium: String(piece.art_medium || 'print').toLowerCase().slice(0, 60),
+    art_year_est: parseInt(piece.art_year_est, 10) || null,
+    art_description: String(piece.art_description || '').slice(0, 1200),
+    color_palette: Array.isArray(piece.color_palette) ? piece.color_palette.slice(0, 6) : [],
+    lon, lat,
+    neighborhood,
+    position_pct: positionPct,
+    image_source: 'ai_generated',
+    generator: 'corridor-design-system',
+  };
+}
+
+async function main() {
+  const pg = new Client({ database: 'ventura_corridor', host: '/tmp', user: process.env.USER });
+  await pg.connect();
+
+  // Build the ordered work-list by category priority + within-category exclude-already-done.
+  const params = [LIMIT];
+  const orderCase = PRIORITY.map((c,i) => `WHEN $${i+2} THEN ${i}`).join(' ');
+  PRIORITY.forEach(c => params.push(c));
+  const sql = `
+    SELECT b.id, b.name, b.category, b.address, b.lng AS lon, b.lat
+    FROM businesses b
+    LEFT JOIN gallery_pieces g ON g.business_id = b.id
+    WHERE g.id IS NULL
+      AND b.name IS NOT NULL
+    ORDER BY CASE b.category ${orderCase} ELSE 99 END, b.id
+    LIMIT $1
+  `;
+  const todo = await pg.query(sql, [LIMIT, ...PRIORITY]);
+  console.log(`  queue: ${todo.rows.length} businesses to generate`);
+
+  let ok = 0, fail = 0;
+  const t0 = Date.now();
+  for (const b of todo.rows) {
+    try {
+      const piece = await generateFor(b);
+      if (!piece) { fail++; process.stdout.write('✗'); continue; }
+      await pg.query(
+        `INSERT INTO gallery_pieces
+          (business_id, art_title, art_medium, art_year_est, art_description,
+           color_palette, image_source, lon, lat, neighborhood, position_pct, generator)
+         VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$10,$11,$12)
+         ON CONFLICT (business_id) DO NOTHING`,
+        [piece.business_id, piece.art_title, piece.art_medium, piece.art_year_est,
+         piece.art_description, JSON.stringify(piece.color_palette),
+         piece.image_source, piece.lon, piece.lat, piece.neighborhood,
+         piece.position_pct, piece.generator]
+      );
+      ok++;
+      if (ok % 50 === 0) {
+        const elapsed = (Date.now() - t0) / 1000;
+        console.log(`\n  · ${ok}/${todo.rows.length} (${(ok/elapsed).toFixed(1)}/s, eta ${Math.ceil((todo.rows.length-ok)/Math.max(1,ok/elapsed)/60)}m)`);
+      } else { process.stdout.write('·'); }
+    } catch (e) {
+      fail++; process.stdout.write('!');
+      console.error('\n  · err', b.id, e.message);
+    }
+  }
+  await pg.end();
+  console.log(`\nDone: ${ok} ingested · ${fail} failed · ${((Date.now()-t0)/1000/60).toFixed(1)} min`);
+}
+main().catch(e => { console.error(e); process.exit(1); });

← 0900593 docs(appointments): Shopify embed snippet + copy-paste instr  ·  back to Ventura Corridor  ·  YOLO tick 1: /gallery — geo-walk horizontal scroll of qwen3 4598d33 →