← 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
A db/002_gallery_tiers.sqlA launchd/com.steve.ventura-gallery-3am.plistA scripts/build-virtual-gallery.shA scripts/generate-gallery-pieces.js
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" && cd /Users/stevestudio2/Projects/ventura-corridor && 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 →