From 0b50f5ea954d369fb9deb074ab309519caf0dfce Mon Sep 17 00:00:00 2001 From: gilesb Date: Wed, 7 Jan 2026 17:41:55 +0000 Subject: [PATCH] feat: add cache browsing UI with tabs for runs and cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- server.py | 122 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 10 deletions(-) diff --git a/server.py b/server.py index 3be7ff7..d49225c 100644 --- a/server.py +++ b/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: ''' + runs_active = "active" if tab == "runs" else "" + cache_active = "active" if tab == "cache" else "" + + runs_content = "" + cache_content = "" + if tab == "runs": + runs_content = ''' +
+ Loading... +
+ ''' + else: + cache_content = ''' +
+ Loading... +
+ ''' + return f""" Art DAG L1 Server - + {user_info}

Art DAG L1 Server

- -
- Loading... + + {runs_content} + {cache_content} """ @@ -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 '

Cache is empty.

' + + html_parts = ['
'] + + 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''' + +
+
+
+ {media_type} + {content_hash} +
+ {size_str} +
+
+
+
+ ''') + + if media_type == "video": + html_parts.append(f'') + elif media_type == "image": + html_parts.append(f'{content_hash[:16]}') + else: + html_parts.append(f'

Unknown file type

') + + html_parts.append(''' +
+
+
+
+
+ ''') + + html_parts.append('
') + 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."""