Compare commits

...

3 Commits

Author SHA1 Message Date
giles
17b92c77ef Fix async function call for load_plan_for_run_with_fallback
Use await directly instead of asyncio.to_thread() since the
function is already async.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 11:31:27 +00:00
giles
64ef9396d6 Add /help routes to display README documentation
Provides /help index and /help/{doc_name} routes to view
L1 server and Common library READMEs in the web UI.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 11:30:36 +00:00
giles
255d44fbf6 Remove redundant documentation UI routes
/docs now correctly points to FastAPI's Swagger API docs.
README files can be viewed directly in the git repository.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 11:21:16 +00:00

215
server.py
View File

@@ -612,7 +612,7 @@ def render_home_html(actor_id: Optional[str] = None) -> str:
<a href="/recipes" class="px-4 py-2 bg-dark-500 hover:bg-dark-600 rounded-md text-blue-400 hover:text-blue-300 font-medium transition-colors">Recipes</a>
<a href="/media" class="px-4 py-2 bg-dark-500 hover:bg-dark-600 rounded-md text-blue-400 hover:text-blue-300 font-medium transition-colors">Media</a>
<a href="/storage" class="px-4 py-2 bg-dark-500 hover:bg-dark-600 rounded-md text-blue-400 hover:text-blue-300 font-medium transition-colors">Storage</a>
<a href="/docs" class="px-4 py-2 bg-dark-500 hover:bg-dark-600 rounded-md text-blue-400 hover:text-blue-300 font-medium transition-colors">API Docs</a>
<a href="/docs" class="px-4 py-2 bg-dark-500 hover:bg-dark-600 rounded-md text-gray-400 hover:text-gray-300 font-medium transition-colors">API</a>
{user_section}
</nav>
@@ -1437,7 +1437,7 @@ async def run_plan_node_detail(run_id: str, step_id: str, request: Request):
return HTMLResponse(f'<p class="text-red-400">Run not found</p>', status_code=404)
# Load plan data (with fallback to generate from recipe)
plan_data = await asyncio.to_thread(load_plan_for_run_with_fallback, run)
plan_data = await load_plan_for_run_with_fallback(run)
if not plan_data:
return HTMLResponse('<p class="text-gray-400">Plan not found</p>')
@@ -1584,7 +1584,7 @@ async def run_plan_visualization(run_id: str, request: Request, node: Optional[s
return HTMLResponse(render_page("Access Denied", content, ctx.actor_id, active_tab="runs"), status_code=403)
# Load plan data (with fallback to generate from recipe)
plan_data = await asyncio.to_thread(load_plan_for_run_with_fallback, run)
plan_data = await load_plan_for_run_with_fallback(run)
# Build sub-navigation tabs
tabs_html = render_run_sub_tabs(run_id, active="plan")
@@ -6444,187 +6444,84 @@ async def download_client():
# ============================================================================
# Documentation Routes
# Help / Documentation Routes
# ============================================================================
# Documentation paths
DOCS_DIR = Path(__file__).parent
COMMON_DOCS_DIR = Path(__file__).parent.parent / "common"
DOCS_MAP = {
"l1": DOCS_DIR / "README.md",
"common": COMMON_DOCS_DIR / "README.md",
"l1": ("L1 Server (Celery)", DOCS_DIR / "README.md"),
"common": ("Common Library", COMMON_DOCS_DIR / "README.md"),
}
def render_markdown(content: str) -> str:
"""Convert markdown to HTML with basic styling."""
import re
# Escape HTML first
content = content.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
# Code blocks (``` ... ```)
def code_block_replace(match):
lang = match.group(1) or ""
code = match.group(2)
return f'<pre class="bg-gray-800 p-4 rounded-lg overflow-x-auto text-sm"><code class="language-{lang}">{code}</code></pre>'
content = re.sub(r'```(\w*)\n(.*?)```', code_block_replace, content, flags=re.DOTALL)
# Inline code
content = re.sub(r'`([^`]+)`', r'<code class="bg-gray-700 px-1 rounded text-sm">\1</code>', content)
# Headers
content = re.sub(r'^### (.+)$', r'<h3 class="text-lg font-semibold text-white mt-6 mb-2">\1</h3>', content, flags=re.MULTILINE)
content = re.sub(r'^## (.+)$', r'<h2 class="text-xl font-bold text-white mt-8 mb-3 border-b border-gray-700 pb-2">\1</h2>', content, flags=re.MULTILINE)
content = re.sub(r'^# (.+)$', r'<h1 class="text-2xl font-bold text-white mb-4">\1</h1>', content, flags=re.MULTILINE)
# Bold and italic
content = re.sub(r'\*\*([^*]+)\*\*', r'<strong class="font-semibold">\1</strong>', content)
content = re.sub(r'\*([^*]+)\*', r'<em>\1</em>', content)
# Links
content = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="\2" class="text-blue-400 hover:underline">\1</a>', content)
# Tables
def table_replace(match):
lines = match.group(0).strip().split('\n')
if len(lines) < 2:
return match.group(0)
header = lines[0]
rows = lines[2:] if len(lines) > 2 else []
header_cells = [cell.strip() for cell in header.split('|')[1:-1]]
header_html = ''.join(f'<th class="px-4 py-2 text-left border-b border-gray-600">{cell}</th>' for cell in header_cells)
rows_html = ''
for row in rows:
cells = [cell.strip() for cell in row.split('|')[1:-1]]
cells_html = ''.join(f'<td class="px-4 py-2 border-b border-gray-700">{cell}</td>' for cell in cells)
rows_html += f'<tr class="hover:bg-gray-700">{cells_html}</tr>'
return f'<table class="w-full text-sm mb-4"><thead><tr class="bg-gray-700">{header_html}</tr></thead><tbody>{rows_html}</tbody></table>'
content = re.sub(r'(\|[^\n]+\|\n)+', table_replace, content)
# Bullet points
content = re.sub(r'^- (.+)$', r'<li class="ml-4 list-disc">\1</li>', content, flags=re.MULTILINE)
content = re.sub(r'(<li[^>]*>.*</li>\n?)+', r'<ul class="mb-4">\g<0></ul>', content)
# Paragraphs (lines not starting with < or whitespace)
lines = content.split('\n')
result = []
in_paragraph = False
for line in lines:
stripped = line.strip()
if not stripped:
if in_paragraph:
result.append('</p>')
in_paragraph = False
result.append('')
elif stripped.startswith('<'):
if in_paragraph:
result.append('</p>')
in_paragraph = False
result.append(line)
else:
if not in_paragraph:
result.append('<p class="mb-4 text-gray-300">')
in_paragraph = True
result.append(line)
if in_paragraph:
result.append('</p>')
content = '\n'.join(result)
return content
@app.get("/docs", response_class=HTMLResponse)
async def docs_index(request: Request):
@app.get("/help", response_class=HTMLResponse)
async def help_index(request: Request):
"""Documentation index page."""
user = await get_optional_user(request)
username = user.username if user else None
html = f"""<!DOCTYPE html>
<html class="dark">
<head>
<title>Documentation - Art DAG L1</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config = {{ darkMode: 'class' }}</script>
</head>
<body class="bg-gray-900 text-gray-100 min-h-screen">
<nav class="bg-gray-800 border-b border-gray-700 px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-xl font-bold text-white">Art DAG L1</a>
<div class="flex items-center gap-4">
<a href="/runs" class="text-gray-300 hover:text-white">Runs</a>
<a href="/recipes" class="text-gray-300 hover:text-white">Recipes</a>
<a href="/media" class="text-gray-300 hover:text-white">Media</a>
<a href="/docs" class="text-white font-semibold">Docs</a>
</div>
</div>
</nav>
<main class="max-w-4xl mx-auto p-8">
<h1 class="text-3xl font-bold mb-8">Documentation</h1>
# Build doc links
doc_links = ""
for key, (title, path) in DOCS_MAP.items():
if path.exists():
doc_links += f'''
<a href="/help/{key}" class="block p-6 bg-dark-600 rounded-lg hover:bg-dark-500 transition-colors">
<h2 class="text-xl font-semibold text-white mb-2">{title}</h2>
<p class="text-gray-400">View documentation</p>
</a>
'''
content = f'''
<div class="max-w-4xl mx-auto">
<h1 class="text-2xl font-bold text-white mb-6">Documentation</h1>
<div class="grid gap-4">
<a href="/docs/l1" class="block p-6 bg-gray-800 rounded-lg hover:bg-gray-700 transition">
<h2 class="text-xl font-semibold text-white mb-2">L1 Server (Celery)</h2>
<p class="text-gray-400">Distributed rendering server with Celery workers, IPFS integration, and 3-phase execution.</p>
</a>
<a href="/docs/common" class="block p-6 bg-gray-800 rounded-lg hover:bg-gray-700 transition">
<h2 class="text-xl font-semibold text-white mb-2">Common Library</h2>
<p class="text-gray-400">Shared components: Jinja2 templates, middleware, content negotiation, and utilities.</p>
</a>
{doc_links}
</div>
</main>
</body>
</html>"""
return HTMLResponse(html)
</div>
'''
return HTMLResponse(render_page("Help", content, username))
@app.get("/docs/{doc_name}", response_class=HTMLResponse)
async def docs_page(doc_name: str, request: Request):
"""Render a markdown documentation file as HTML."""
@app.get("/help/{doc_name}", response_class=HTMLResponse)
async def help_page(doc_name: str, request: Request):
"""Render a README as HTML."""
if doc_name not in DOCS_MAP:
raise HTTPException(404, f"Documentation '{doc_name}' not found")
doc_path = DOCS_MAP[doc_name]
title, doc_path = DOCS_MAP[doc_name]
if not doc_path.exists():
raise HTTPException(404, f"Documentation file not found: {doc_path}")
raise HTTPException(404, f"Documentation file not found")
content = doc_path.read_text()
html_content = render_markdown(content)
user = await get_optional_user(request)
username = user.username if user else None
html = f"""<!DOCTYPE html>
<html class="dark">
<head>
<title>{doc_name.upper()} - Art DAG Documentation</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config = {{ darkMode: 'class' }}</script>
</head>
<body class="bg-gray-900 text-gray-100 min-h-screen">
<nav class="bg-gray-800 border-b border-gray-700 px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-xl font-bold text-white">Art DAG L1</a>
<div class="flex items-center gap-4">
<a href="/runs" class="text-gray-300 hover:text-white">Runs</a>
<a href="/recipes" class="text-gray-300 hover:text-white">Recipes</a>
<a href="/media" class="text-gray-300 hover:text-white">Media</a>
<a href="/docs" class="text-white font-semibold">Docs</a>
</div>
</div>
</nav>
<main class="max-w-4xl mx-auto p-8">
# Read and render markdown
import markdown
md_content = doc_path.read_text()
html_content = markdown.markdown(md_content, extensions=['tables', 'fenced_code'])
content = f'''
<div class="max-w-4xl mx-auto">
<div class="mb-4">
<a href="/docs" class="text-blue-400 hover:underline">&larr; Back to Documentation</a>
<a href="/help" class="text-blue-400 hover:underline">&larr; Back to Help</a>
</div>
<article class="prose prose-invert max-w-none">
<div class="prose prose-invert max-w-none
prose-headings:text-white prose-headings:border-b prose-headings:border-dark-500 prose-headings:pb-2
prose-h1:text-2xl prose-h1:mt-0
prose-h2:text-xl prose-h2:mt-6
prose-a:text-blue-400 hover:prose-a:text-blue-300
prose-pre:bg-dark-600 prose-pre:border prose-pre:border-dark-500
prose-code:bg-dark-600 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-green-300
prose-table:border-collapse
prose-th:bg-dark-600 prose-th:border prose-th:border-dark-500 prose-th:px-4 prose-th:py-2
prose-td:border prose-td:border-dark-500 prose-td:px-4 prose-td:py-2">
{html_content}
</article>
</main>
</body>
</html>"""
return HTMLResponse(html)
</div>
</div>
'''
return HTMLResponse(render_page(title, content, username))
# ============================================================================