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; }
|
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."""
|
"""Render main UI HTML with optional user context."""
|
||||||
user_info = ""
|
user_info = ""
|
||||||
if username:
|
if username:
|
||||||
@@ -609,23 +609,52 @@ def render_ui_html(username: Optional[str] = None) -> str:
|
|||||||
</div>
|
</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"""
|
return f"""
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Art DAG L1 Server</title>
|
<title>Art DAG L1 Server</title>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{user_info}
|
{user_info}
|
||||||
<h1><a href="/ui" style="color: inherit;">Art DAG L1 Server</a></h1>
|
<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">
|
<div class="nav-tabs">
|
||||||
Refresh
|
<a href="/ui" class="{runs_active}">Runs</a>
|
||||||
</button>
|
<a href="/ui?tab=cache" class="{cache_active}">Cache</a>
|
||||||
<div id="runs" hx-get="/ui/runs" hx-trigger="load" hx-swap="innerHTML">
|
|
||||||
Loading...
|
|
||||||
</div>
|
</div>
|
||||||
|
{runs_content}
|
||||||
|
{cache_content}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
@@ -721,10 +750,10 @@ UI_REGISTER_HTML = """
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/ui", response_class=HTMLResponse)
|
@app.get("/ui", response_class=HTMLResponse)
|
||||||
async def ui_index(request: Request):
|
async def ui_index(request: Request, tab: str = "runs"):
|
||||||
"""Web UI for viewing runs."""
|
"""Web UI for viewing runs and cache."""
|
||||||
username = get_user_from_cookie(request)
|
username = get_user_from_cookie(request)
|
||||||
return render_ui_html(username)
|
return render_ui_html(username, tab)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/ui/login", response_class=HTMLResponse)
|
@app.get("/ui/login", response_class=HTMLResponse)
|
||||||
@@ -887,6 +916,79 @@ async def ui_runs(request: Request):
|
|||||||
return '\n'.join(html_parts)
|
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)
|
@app.get("/ui/detail/{run_id}", response_class=HTMLResponse)
|
||||||
async def ui_detail_page(run_id: str):
|
async def ui_detail_page(run_id: str):
|
||||||
"""Full detail page for a run."""
|
"""Full detail page for a run."""
|
||||||
|
|||||||
Reference in New Issue
Block a user