feat: add cache browsing UI with tabs for runs and cache

- Add tabbed navigation between Runs and Cache views
- Add /ui/cache-list endpoint to list cached files with thumbnails
- Show media type, file size, and preview for each cached item
- Sort cache items by modification time (newest first)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-07 17:41:55 +00:00
parent 618e3b1e04
commit 0b50f5ea95

122
server.py
View File

@@ -592,7 +592,7 @@ UI_CSS = """
code { background: #222; padding: 2px 6px; border-radius: 4px; }
"""
def render_ui_html(username: Optional[str] = None) -> str:
def render_ui_html(username: Optional[str] = None, tab: str = "runs") -> str:
"""Render main UI HTML with optional user context."""
user_info = ""
if username:
@@ -609,23 +609,52 @@ def render_ui_html(username: Optional[str] = None) -> str:
</div>
'''
runs_active = "active" if tab == "runs" else ""
cache_active = "active" if tab == "cache" else ""
runs_content = ""
cache_content = ""
if tab == "runs":
runs_content = '''
<div id="content" hx-get="/ui/runs" hx-trigger="load" hx-swap="innerHTML">
Loading...
</div>
'''
else:
cache_content = '''
<div id="content" hx-get="/ui/cache-list" hx-trigger="load" hx-swap="innerHTML">
Loading...
</div>
'''
return f"""
<!DOCTYPE html>
<html>
<head>
<title>Art DAG L1 Server</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>{UI_CSS}</style>
<style>{UI_CSS}
.nav-tabs {{ margin-bottom: 20px; }}
.nav-tabs a {{
padding: 10px 20px;
margin-right: 8px;
background: #333;
border-radius: 4px 4px 0 0;
color: #888;
}}
.nav-tabs a.active {{ background: #1a1a1a; color: #fff; }}
.nav-tabs a:hover {{ color: #fff; }}
</style>
</head>
<body>
{user_info}
<h1><a href="/ui" style="color: inherit;">Art DAG L1 Server</a></h1>
<button class="refresh-btn" hx-get="/ui/runs" hx-target="#runs" hx-swap="innerHTML">
Refresh
</button>
<div id="runs" hx-get="/ui/runs" hx-trigger="load" hx-swap="innerHTML">
Loading...
<div class="nav-tabs">
<a href="/ui" class="{runs_active}">Runs</a>
<a href="/ui?tab=cache" class="{cache_active}">Cache</a>
</div>
{runs_content}
{cache_content}
</body>
</html>
"""
@@ -721,10 +750,10 @@ UI_REGISTER_HTML = """
@app.get("/ui", response_class=HTMLResponse)
async def ui_index(request: Request):
"""Web UI for viewing runs."""
async def ui_index(request: Request, tab: str = "runs"):
"""Web UI for viewing runs and cache."""
username = get_user_from_cookie(request)
return render_ui_html(username)
return render_ui_html(username, tab)
@app.get("/ui/login", response_class=HTMLResponse)
@@ -887,6 +916,79 @@ async def ui_runs(request: Request):
return '\n'.join(html_parts)
@app.get("/ui/cache-list", response_class=HTMLResponse)
async def ui_cache_list(request: Request):
"""HTMX partial: list of cached items."""
current_user = get_user_from_cookie(request)
# Get all cached files
cache_items = []
if CACHE_DIR.exists():
for f in CACHE_DIR.iterdir():
if f.is_file() and not f.name.endswith('.provenance.json'):
stat = f.stat()
cache_items.append({
"hash": f.name,
"size": stat.st_size,
"mtime": stat.st_mtime
})
# Sort by modification time (newest first)
cache_items.sort(key=lambda x: x["mtime"], reverse=True)
if not cache_items:
return '<p class="no-runs">Cache is empty.</p>'
html_parts = ['<div class="runs">']
for item in cache_items[:50]: # Limit to 50 items
content_hash = item["hash"]
cache_path = CACHE_DIR / content_hash
media_type = detect_media_type(cache_path)
# Format size
size = item["size"]
if size > 1024*1024:
size_str = f"{size/(1024*1024):.1f} MB"
elif size > 1024:
size_str = f"{size/1024:.1f} KB"
else:
size_str = f"{size} bytes"
html_parts.append(f'''
<a href="/ui/cache/{content_hash}" class="run-link">
<div class="run">
<div class="run-header">
<div>
<span class="run-recipe">{media_type}</span>
<span class="run-id">{content_hash}</span>
</div>
<span class="status completed">{size_str}</span>
</div>
<div class="media-row">
<div class="media-box" style="max-width: 300px;">
<div class="media-container">
''')
if media_type == "video":
html_parts.append(f'<video src="/cache/{content_hash}" controls muted loop style="max-height:150px;"></video>')
elif media_type == "image":
html_parts.append(f'<img src="/cache/{content_hash}" alt="{content_hash[:16]}" style="max-height:150px;">')
else:
html_parts.append(f'<p>Unknown file type</p>')
html_parts.append('''
</div>
</div>
</div>
</div>
</a>
''')
html_parts.append('</div>')
return '\n'.join(html_parts)
@app.get("/ui/detail/{run_id}", response_class=HTMLResponse)
async def ui_detail_page(run_id: str):
"""Full detail page for a run."""