chore(pages): 🔧 Update build script for failed page deployment

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-30 08:50:21 -07:00
parent 4d40e70214
commit 8f011cf82e
6 changed files with 282 additions and 14 deletions

View file

@ -705,19 +705,48 @@ class SpriteRegistry:
limit: int = 50,
offset: int = 0,
) -> tuple[list[dict], int]:
"""Paginated variant query for the Theater UI.
"""Paginated variant query for browse/theater UIs.
mode='all' all completed variants with images
mode='review' only variants with rating >= 1 and not yet approved
mode='all' all completed variants with images
mode='completed' same as 'all'
mode='review' rating >= 1 and not yet approved
mode='processed' background-removed variants
mode='approved' approved variants
mode='installed' installed into game assets
mode='scored_<name>' passed a specific scorer's gate (e.g. 'scored_qwen3')
Returns (items, total_count).
"""
base_where = "v.job_status = 'completed' AND v.raw_path IS NOT NULL"
if mode == "review":
base_where += " AND v.rating >= 1 AND v.is_approved = 0"
extra_params: list = []
if mode in ("all", "completed"):
base_where = "v.job_status = 'completed' AND v.raw_path IS NOT NULL"
elif mode == "review":
base_where = (
"v.job_status = 'completed' AND v.raw_path IS NOT NULL"
" AND v.rating >= 1 AND v.is_approved = 0"
)
elif mode == "processed":
base_where = "v.processed_path IS NOT NULL"
elif mode == "approved":
base_where = "v.is_approved = 1"
elif mode == "installed":
base_where = "v.job_status = 'installed'"
elif mode.startswith("scored_"):
scorer = mode[len("scored_"):]
base_where = (
"EXISTS ("
" SELECT 1 FROM latest_scores ls"
" WHERE ls.variant_id = v.id AND ls.scorer_name = ? AND ls.gate_passed = 1"
")"
)
extra_params.append(scorer)
else:
base_where = "v.job_status = 'completed' AND v.raw_path IS NOT NULL"
total = self.conn.execute(
f"SELECT COUNT(*) FROM variants v WHERE {base_where}"
f"SELECT COUNT(*) FROM variants v WHERE {base_where}",
extra_params,
).fetchone()[0]
rows = self.conn.execute(
@ -743,7 +772,7 @@ class SpriteRegistry:
WHERE {base_where}
ORDER BY v.id DESC
LIMIT ? OFFSET ?""",
(limit, offset),
extra_params + [limit, offset],
).fetchall()
return [dict(r) for r in rows], total

View file

@ -8,6 +8,7 @@ import SpriteTheaterPage from './pages/SpriteTheaterPage'
import TerrainGridPage from './pages/TerrainGridPage'
import VariantPage from './pages/VariantPage'
import { SpriteCoveragePage } from './pages/SpriteCoveragePage'
import { VariantBrowsePage } from './pages/VariantBrowsePage'
import { SpriteStream } from './SpriteStream'
import { colors } from './pages/theme'
@ -85,6 +86,7 @@ export function App(): ReactElement {
<Route path="/variant/:id" element={<VariantPage />} />
<Route path="/coverage" element={<SpriteCoveragePage />} />
<Route path="/terrain-grid" element={<TerrainGridPage />} />
<Route path="/browse" element={<VariantBrowsePage />} />
<Route path="/sprite/*" element={<SpritePage />} />
</Routes>
</div>

View file

@ -270,3 +270,11 @@ export async function fetchPipeline(): Promise<PipelineState> {
export async function fetchTerrainGrid(elevation: TerrainElevation): Promise<TerrainGrid> {
return request<TerrainGrid>(buildUrl('/terrain-grid', { elevation }))
}
export async function fetchVariantBrowse(params: {
stage: string
limit?: number
offset?: number
}): Promise<{ items: RecentVariant[]; total: number }> {
return request<{ items: RecentVariant[]; total: number }>(buildUrl('/variants/browse', params))
}

View file

@ -85,6 +85,7 @@ interface FlowStage {
count: number // done
queued: number // waiting at this step (stacked on top)
color: string
stage: string // browse URL stage param
}
function PipelineFlow({
@ -99,12 +100,14 @@ function PipelineFlow({
const stages: FlowStage[] = [
{
label: 'Completed',
stage: 'completed',
count: funnel.total_completed,
queued: Math.max(0, totalPlanned - funnel.total_completed),
color: colors.muted,
},
{
label: 'Processed',
stage: 'processed',
count: funnel.total_processed,
queued: Math.max(0, funnel.total_completed - funnel.total_processed),
color: '#3b82f6',
@ -113,6 +116,7 @@ function PipelineFlow({
.filter((s) => funnel.scoring[s] !== undefined)
.map((s, i) => ({
label: s.charAt(0).toUpperCase() + s.slice(1),
stage: `scored_${s}`,
count: funnel.scoring[s].passed,
queued: i === 0
? funnel.unscored
@ -121,12 +125,14 @@ function PipelineFlow({
}))),
{
label: 'Approved',
stage: 'approved',
count: funnel.approved,
queued: Math.max(0, (funnel.scoring[SCORER_ORDER[SCORER_ORDER.length - 1]]?.passed ?? 0) - funnel.approved),
color: '#22c55e',
},
{
label: 'Installed',
stage: 'installed',
count: funnel.installed,
queued: Math.max(0, funnel.approved - funnel.installed),
color: '#a855f7',
@ -192,12 +198,18 @@ function PipelineFlow({
/>
)}
</div>
{/* done count */}
<FlashNumber
value={stage.count}
color={stage.color}
style={{ color: stage.color, fontWeight: 700, fontSize: 16, lineHeight: 1 }}
/>
{/* done count — links to browse page */}
<Link
to={`/browse?stage=${stage.stage}`}
style={{ textDecoration: 'none', lineHeight: 1 }}
title={`Browse ${stage.label} variants`}
>
<FlashNumber
value={stage.count}
color={stage.color}
style={{ color: stage.color, fontWeight: 700, fontSize: 16, lineHeight: 1 }}
/>
</Link>
{/* queued count */}
{stage.queued > 0 && (
<span style={{ color: stage.color, fontWeight: 500, fontSize: 11, opacity: 0.5, lineHeight: 1, marginTop: -4 }}>

View file

@ -0,0 +1,207 @@
import { useEffect, useState, useCallback, type CSSProperties, type ReactNode } from 'react'
import { useSearchParams, useNavigate, Link } from 'react-router-dom'
import { fetchVariantBrowse, bestImageUrl } from '../api'
import type { RecentVariant } from '../types'
import { colors } from './theme'
const PAGE_SIZE = 120
const STAGE_LABELS: Record<string, string> = {
completed: 'Completed',
processed: 'Processed',
approved: 'Approved',
installed: 'Installed',
scored_qwen3: 'Scored — Qwen3',
scored_haiku: 'Scored — Haiku',
scored_sonnet: 'Scored — Sonnet',
scored_opus: 'Scored — Opus',
}
function VariantTile({ variant }: { variant: RecentVariant }): ReactNode {
const navigate = useNavigate()
const imgSrc = bestImageUrl(variant.processed_path, variant.raw_path)
const [hovered, setHovered] = useState(false)
return (
<div
onClick={(): void => navigate(`/variant/${variant.variant_id}`)}
onMouseEnter={(): void => setHovered(true)}
onMouseLeave={(): void => setHovered(false)}
style={{
position: 'relative',
aspectRatio: '1',
background: colors.surface,
borderRadius: 8,
border: `1px solid ${hovered ? colors.highlight : colors.accent}`,
overflow: 'hidden',
cursor: 'pointer',
transition: 'border-color 0.12s',
}}
>
{imgSrc ? (
<img
src={imgSrc}
alt={variant.sprite_id}
style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }}
loading="lazy"
/>
) : (
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: colors.muted, fontSize: 11 }}>No image</span>
</div>
)}
{/* overlay on hover */}
{hovered && (
<div
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: '6px 8px',
background: 'rgba(0,0,0,0.75)',
fontSize: 10,
color: colors.muted,
lineHeight: 1.4,
}}
>
<div style={{ color: colors.text, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{variant.sprite_id.split('/').pop()}
</div>
{variant.rating !== null && (
<div style={{ color: variant.rating >= 70 ? '#10b981' : variant.rating >= 50 ? '#f59e0b' : '#ef4444' }}>
{variant.rating.toFixed(0)}
</div>
)}
</div>
)}
{/* approved badge */}
{variant.is_approved && (
<div
style={{
position: 'absolute',
top: 4,
right: 4,
width: 8,
height: 8,
borderRadius: '50%',
background: '#10b981',
}}
/>
)}
</div>
)
}
export function VariantBrowsePage(): ReactNode {
const [searchParams, setSearchParams] = useSearchParams()
const stage = searchParams.get('stage') ?? 'completed'
const page = parseInt(searchParams.get('page') ?? '0', 10)
const [items, setItems] = useState<RecentVariant[]>([])
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const load = useCallback(async () => {
setLoading(true)
setError(null)
try {
const result = await fetchVariantBrowse({ stage, limit: PAGE_SIZE, offset: page * PAGE_SIZE })
setItems(result.items)
setTotal(result.total)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
}, [stage, page])
useEffect(() => { void load() }, [load])
const totalPages = Math.ceil(total / PAGE_SIZE)
const navBtn: CSSProperties = {
background: colors.surface,
border: `1px solid ${colors.accent}`,
borderRadius: 6,
color: colors.text,
fontSize: 13,
fontWeight: 600,
padding: '6px 16px',
cursor: 'pointer',
}
const stageLabel = STAGE_LABELS[stage] ?? stage
return (
<div style={{ padding: '20px 24px', maxWidth: 1600, margin: '0 auto' }}>
{/* header */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
<Link
to="/"
style={{ color: colors.muted, fontSize: 13, textDecoration: 'none' }}
>
Dashboard
</Link>
<span style={{ color: colors.accent }}></span>
<h1 style={{ margin: 0, color: colors.text, fontSize: 18, fontWeight: 700 }}>
{stageLabel}
</h1>
<span style={{ color: colors.muted, fontSize: 14 }}>
{loading ? '…' : `${total.toLocaleString()} variants`}
</span>
</div>
{error && (
<div style={{ color: '#f87171', background: '#4a1a1a', border: '1px solid #f8717144', borderRadius: 8, padding: '10px 16px', marginBottom: 16 }}>
{error}
</div>
)}
{/* grid */}
{!loading && items.length === 0 && !error && (
<div style={{ color: colors.muted, fontSize: 14, textAlign: 'center', padding: '60px 0' }}>
No variants in this stage.
</div>
)}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
gap: 8,
}}
>
{items.map((v) => (
<VariantTile key={v.variant_id} variant={v} />
))}
</div>
{/* pagination */}
{totalPages > 1 && (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 12, marginTop: 24 }}>
<button
style={{ ...navBtn, opacity: page === 0 ? 0.4 : 1 }}
disabled={page === 0}
onClick={(): void => setSearchParams({ stage, page: String(page - 1) })}
>
Prev
</button>
<span style={{ color: colors.muted, fontSize: 13 }}>
Page {page + 1} of {totalPages}
</span>
<button
style={{ ...navBtn, opacity: page >= totalPages - 1 ? 0.4 : 1 }}
disabled={page >= totalPages - 1}
onClick={(): void => setSearchParams({ stage, page: String(page + 1) })}
>
Next
</button>
</div>
)}
</div>
)
}

View file

@ -307,6 +307,16 @@ def create_app(
items, total = registry.query_variants(mode=mode, limit=limit, offset=offset)
return {"items": items, "total": total}
@app.get("/api/variants/browse")
def browse_variants(
stage: Annotated[str, Query(pattern=r"^(all|completed|review|processed|approved|installed|scored_[a-z0-9_]+)$")] = "completed",
limit: Annotated[int, Query(ge=1, le=500)] = 120,
offset: Annotated[int, Query(ge=0)] = 0,
) -> dict:
"""Paginated variant browse by funnel stage — used by the dashboard funnel click-throughs."""
items, total = registry.query_variants(mode=stage, limit=limit, offset=offset)
return {"items": items, "total": total}
@app.get("/api/stream/variants")
async def stream_variants() -> StreamingResponse:
async def event_generator():