feat: HTML mirror pages — readable project dashboards in browser
GET /projects/{name}/mirror.html serves a styled HTML page rendered
from the mirror markdown. Clean typography, responsive, dark mode
support, mobile-friendly. Open from phone or desktop:
http://dalidou:8100/projects/p04-gigabit/mirror.html
http://dalidou:8100/projects/p05-interferometer/mirror.html
http://dalidou:8100/projects/p06-polisher/mirror.html
Uses the markdown library for md→html conversion. Added to
requirements.txt. The JSON endpoint (/mirror) still exists for
programmatic access.
Source of truth remains the AtoCore database. The HTML page is a
derived view with a clear disclaimer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
import atocore.config as _config
|
||||
@@ -1075,6 +1076,72 @@ def api_create_relationship(req: RelationshipCreateRequest) -> dict:
|
||||
}
|
||||
|
||||
|
||||
@router.get("/projects/{project_name}/mirror.html", response_class=HTMLResponse)
|
||||
def api_project_mirror_html(project_name: str) -> HTMLResponse:
|
||||
"""Serve a readable HTML project overview page.
|
||||
|
||||
Open in a browser for a clean, styled project dashboard derived
|
||||
from AtoCore's structured data. Source of truth is the database —
|
||||
this page is a derived view.
|
||||
"""
|
||||
from atocore.projects.registry import resolve_project_name as _resolve
|
||||
|
||||
canonical = _resolve(project_name)
|
||||
try:
|
||||
md_content = generate_project_overview(canonical)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Mirror generation failed: {e}")
|
||||
|
||||
import markdown
|
||||
|
||||
html_body = markdown.markdown(md_content, extensions=["tables", "fenced_code"])
|
||||
html = _MIRROR_HTML_TEMPLATE.replace("{{title}}", f"{canonical} — AtoCore Mirror")
|
||||
html = html.replace("{{body}}", html_body)
|
||||
return HTMLResponse(content=html)
|
||||
|
||||
|
||||
_MIRROR_HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{title}}</title>
|
||||
<style>
|
||||
:root { --bg: #fafafa; --text: #1a1a2e; --accent: #2563eb; --border: #e2e8f0; --card: #fff; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --bg: #0f172a; --text: #e2e8f0; --accent: #60a5fa; --border: #334155; --card: #1e293b; }
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.7; color: var(--text); background: var(--bg);
|
||||
max-width: 800px; margin: 0 auto; padding: 2rem 1.5rem;
|
||||
}
|
||||
h1 { font-size: 1.8rem; margin-bottom: 0.5rem; color: var(--accent); }
|
||||
h2 { font-size: 1.4rem; margin-top: 2.5rem; margin-bottom: 0.8rem; padding-bottom: 0.3rem; border-bottom: 2px solid var(--border); }
|
||||
h3 { font-size: 1.15rem; margin-top: 1.5rem; margin-bottom: 0.5rem; }
|
||||
p { margin-bottom: 0.8rem; }
|
||||
ul { margin-left: 1.5rem; margin-bottom: 1rem; }
|
||||
li { margin-bottom: 0.4rem; }
|
||||
li ul { margin-top: 0.3rem; }
|
||||
strong { color: var(--accent); font-weight: 600; }
|
||||
em { opacity: 0.7; font-size: 0.9em; }
|
||||
blockquote {
|
||||
background: var(--card); border-left: 4px solid var(--accent);
|
||||
padding: 0.8rem 1.2rem; margin: 1rem 0; border-radius: 0 8px 8px 0;
|
||||
}
|
||||
hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
||||
code { background: var(--card); padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{body}}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
@router.get("/projects/{project_name}/mirror")
|
||||
def api_project_mirror(project_name: str) -> dict:
|
||||
"""Generate a human-readable project overview from structured data.
|
||||
|
||||
Reference in New Issue
Block a user