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:
122
server.py
122
server.py
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user