Add documentation routes and update README

- Update README with comprehensive documentation covering ActivityPub,
  OpenTimestamps anchoring, L1 integration, and all API endpoints
- Add /docs routes to serve markdown documentation as styled HTML
- Include common library documentation in web interface

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
giles
2026-01-11 09:58:36 +00:00
parent d1e9287829
commit 5730cd0f22
2 changed files with 498 additions and 174 deletions

184
server.py
View File

@@ -3682,6 +3682,190 @@ async def download_client():
)
# ============================================================================
# Documentation Routes
# ============================================================================
# Documentation paths
L2_DOCS_DIR = Path(__file__).parent
COMMON_DOCS_DIR = Path(__file__).parent.parent / "common"
L2_DOCS_MAP = {
"l2": L2_DOCS_DIR / "README.md",
"common": 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):
"""Documentation index page."""
user = await get_optional_user(request)
html = f"""<!DOCTYPE html>
<html class="dark">
<head>
<title>Documentation - Art DAG L2</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 L2</a>
<div class="flex items-center gap-4">
<a href="/assets" class="text-gray-300 hover:text-white">Assets</a>
<a href="/activities" class="text-gray-300 hover:text-white">Activities</a>
<a href="/anchors/ui" class="text-gray-300 hover:text-white">Anchors</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>
<div class="grid gap-4">
<a href="/docs/l2" class="block p-6 bg-gray-800 rounded-lg hover:bg-gray-700 transition">
<h2 class="text-xl font-semibold text-white mb-2">L2 Server (ActivityPub)</h2>
<p class="text-gray-400">Ownership registry, ActivityPub federation, and OpenTimestamps anchoring.</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>
</div>
</main>
</body>
</html>"""
return HTMLResponse(html)
@app.get("/docs/{doc_name}", response_class=HTMLResponse)
async def docs_page(doc_name: str, request: Request):
"""Render a markdown documentation file as HTML."""
if doc_name not in L2_DOCS_MAP:
raise HTTPException(404, f"Documentation '{doc_name}' not found")
doc_path = L2_DOCS_MAP[doc_name]
if not doc_path.exists():
raise HTTPException(404, f"Documentation file not found: {doc_path}")
content = doc_path.read_text()
html_content = render_markdown(content)
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 L2</a>
<div class="flex items-center gap-4">
<a href="/assets" class="text-gray-300 hover:text-white">Assets</a>
<a href="/activities" class="text-gray-300 hover:text-white">Activities</a>
<a href="/anchors/ui" class="text-gray-300 hover:text-white">Anchors</a>
<a href="/docs" class="text-white font-semibold">Docs</a>
</div>
</div>
</nav>
<main class="max-w-4xl mx-auto p-8">
<div class="mb-4">
<a href="/docs" class="text-blue-400 hover:underline">&larr; Back to Documentation</a>
</div>
<article class="prose prose-invert max-w-none">
{html_content}
</article>
</main>
</body>
</html>"""
return HTMLResponse(html)
if __name__ == "__main__":
import uvicorn
uvicorn.run("server:app", host="0.0.0.0", port=8200, workers=4)