docs: add logout button to login page
This commit is contained in:
238
scripts/build_docs.py
Normal file
238
scripts/build_docs.py
Normal file
@@ -0,0 +1,238 @@
|
||||
#!/opt/homebrew/bin/python3.11
|
||||
"""Build HTML documentation from module source files."""
|
||||
import os, markdown, re, glob, shutil
|
||||
|
||||
MODULES_DIR = os.path.join(os.path.dirname(__file__), "..", "docs_v1.0", "API_WORKSPACE", "modules")
|
||||
DOC_DIR = os.path.join(os.path.dirname(__file__), "..", "docs_v1.0", "doc")
|
||||
DOC_DEV_DIR = os.path.join(os.path.dirname(__file__), "..", "docs_v1.0", "doc_developer")
|
||||
|
||||
# User-facing modules (no developer content)
|
||||
USER_MODULES = {
|
||||
"01_auth", "02_health", "03_register", "04_lookup", "05_process",
|
||||
"06_search", "07_identity", "08_identity_agent", "08_media",
|
||||
"09_tmdb", "10_pipeline", "12_agent",
|
||||
}
|
||||
|
||||
|
||||
def md_to_html(md_text: str) -> str:
|
||||
"""Convert Markdown to HTML."""
|
||||
html = markdown.markdown(md_text, extensions=['fenced_code', 'tables', 'codehilite'])
|
||||
# Wrap tables
|
||||
html = re.sub(r'<table>', '<table class="table">', html)
|
||||
return html
|
||||
|
||||
def build_index(files, dev=False):
|
||||
"""Build index.html."""
|
||||
links = []
|
||||
for fname in sorted(files):
|
||||
name = os.path.splitext(fname)[0]
|
||||
label = MODULE_LABELS.get(name, name.replace("_", " ").title())
|
||||
if "|" in label:
|
||||
cn, en = label.split("|", 1)
|
||||
else:
|
||||
cn, en = label, ""
|
||||
html_name = fname.replace(".md", ".html")
|
||||
links.append(f'<tr onclick="window.location=\'{html_name}\'" style="cursor:pointer"><td class="cn">{cn}</td><td class="en">{en}</td></tr>')
|
||||
|
||||
title = "Momentry API 開發者文件" if dev else "Momentry API 文件"
|
||||
subtitle = "開發者專用" if dev else "API 參考手冊 — 登入後可瀏覽各模組文件"
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }}
|
||||
.container {{ max-width: 900px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }}
|
||||
h1 {{ font-size: 28px; margin-bottom: 8px; }}
|
||||
p.subtitle {{ color: #666; margin-bottom: 24px; }}
|
||||
table {{ width: 100%; border-collapse: collapse; }}
|
||||
tr {{ border-bottom: 1px solid #eee; }}
|
||||
tr:last-child {{ border: none; }}
|
||||
td {{ padding: 10px 0; }}
|
||||
td.cn {{ width: 140px; font-weight: 600; color: #333; }}
|
||||
td.en {{ color: #666; font-size: 14px; }}
|
||||
a {{ color: #0066cc; text-decoration: none; display: block; }}
|
||||
a:hover td {{ background: #f8f8f8; border-radius: 4px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{title}</h1>
|
||||
<p class="subtitle">{subtitle}</p>
|
||||
<table>{"".join(links)}</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
MODULE_LABELS = {
|
||||
"01_auth": "安全認證|Authentication",
|
||||
"02_health": "健康檢查|Health",
|
||||
"03_register": "檔案註冊|File Registration",
|
||||
"04_lookup": "檔案屬性查詢|File Lookup",
|
||||
"05_process": "處理流程|Processing",
|
||||
"06_search": "搜尋功能|Search",
|
||||
"07_identity": "身份識別|Identity",
|
||||
"08_identity_agent": "智能身份綁定|Smart Identity Binding",
|
||||
"08_media": "串流與截圖|Streaming & Thumbnails",
|
||||
"09_tmdb": "TMDb 整合|TMDb Integration",
|
||||
"10_pipeline": "生產線|Pipeline",
|
||||
"11_error_codes": "錯誤碼|Error Codes",
|
||||
"12_agent": "智慧代理|AI Agents",
|
||||
}
|
||||
|
||||
def build_html(md_text: str, title: str) -> str:
|
||||
"""Wrap MD content in HTML page."""
|
||||
content = md_to_html(md_text)
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{title} - Momentry API Docs</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }}
|
||||
.container {{ max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }}
|
||||
h1 {{ font-size: 24px; margin: 24px 0 12px; }}
|
||||
h2 {{ font-size: 20px; margin: 20px 0 10px; color: #222; }}
|
||||
h3 {{ font-size: 16px; margin: 16px 0 8px; color: #444; }}
|
||||
p {{ line-height: 1.6; margin: 8px 0; }}
|
||||
table {{ border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }}
|
||||
th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
|
||||
th {{ background: #f0f0f0; font-weight: 600; }}
|
||||
code {{ background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }}
|
||||
pre {{ background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }}
|
||||
pre code {{ background: none; padding: 0; }}
|
||||
a {{ color: #0066cc; }}
|
||||
.back {{ display: inline-block; margin-bottom: 20px; color: #666; }}
|
||||
.back:hover {{ color: #333; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
{content}
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
def login_page() -> str:
|
||||
return """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login - Momentry Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
.card { background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; width: 360px; }
|
||||
h1 { font-size: 24px; margin-bottom: 24px; text-align: center; }
|
||||
input { width: 100%; padding: 10px 12px; margin-bottom: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
||||
button { width: 100%; padding: 10px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
|
||||
button:hover { background: #0052a3; }
|
||||
.btn-logout { background: #888; margin-top: 8px; font-size: 13px; padding: 6px; }
|
||||
.btn-logout:hover { background: #666; }
|
||||
.error { color: #cc0000; font-size: 13px; margin-bottom: 12px; display: none; }
|
||||
.success { color: #006600; font-size: 13px; margin-bottom: 12px; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Momentry Docs</h1>
|
||||
<form id="loginForm">
|
||||
<input type="text" id="username" placeholder="Username" value="demo" required>
|
||||
<input type="password" id="password" placeholder="Password" value="demo" required>
|
||||
<div class="error" id="error">Invalid credentials</div>
|
||||
<button type="submit">Login</button>
|
||||
<button type="button" class="btn-logout" onclick="logout()">Logout (clear session)</button>
|
||||
<div class="success" id="logoutMsg">Session cleared</div>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('loginForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
const resp = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.href = '/doc/index.html';
|
||||
} else {
|
||||
document.getElementById('error').style.display = 'block';
|
||||
}
|
||||
};
|
||||
async function logout() {
|
||||
const resp = await fetch('/api/v1/auth/logout', { method: 'POST' });
|
||||
if (resp.ok) {
|
||||
document.getElementById('logoutMsg').style.display = 'block';
|
||||
document.getElementById('error').style.display = 'none';
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
def main():
|
||||
# Clean and recreate doc dirs
|
||||
for d in [DOC_DIR, DOC_DEV_DIR]:
|
||||
if os.path.exists(d):
|
||||
shutil.rmtree(d)
|
||||
os.makedirs(d)
|
||||
|
||||
md_files = sorted(glob.glob(os.path.join(MODULES_DIR, "*.md")))
|
||||
if not md_files:
|
||||
print(f"No MD files found in {MODULES_DIR}")
|
||||
return
|
||||
|
||||
user_html = []
|
||||
dev_html = []
|
||||
for md_path in md_files:
|
||||
with open(md_path) as f:
|
||||
md_text = f.read()
|
||||
fname = os.path.basename(md_path)
|
||||
stem = os.path.splitext(fname)[0]
|
||||
|
||||
# Skip template
|
||||
if stem == "_template":
|
||||
continue
|
||||
|
||||
# Skip error codes (developer-only)
|
||||
if stem == "11_error_codes":
|
||||
dev_only = True
|
||||
else:
|
||||
dev_only = stem not in USER_MODULES
|
||||
|
||||
title = stem.replace("_", " ").title()
|
||||
html = build_html(md_text, title)
|
||||
|
||||
if dev_only:
|
||||
out_path = os.path.join(DOC_DEV_DIR, fname.replace(".md", ".html"))
|
||||
with open(out_path, "w") as f:
|
||||
f.write(html)
|
||||
dev_html.append(fname)
|
||||
print(f" [dev] {fname}")
|
||||
else:
|
||||
out_path = os.path.join(DOC_DIR, fname.replace(".md", ".html"))
|
||||
with open(out_path, "w") as f:
|
||||
f.write(html)
|
||||
user_html.append(fname)
|
||||
print(f" [doc] {fname}")
|
||||
|
||||
# Build indexes + login page
|
||||
for d, files, label in [(DOC_DIR, user_html, "User"), (DOC_DEV_DIR, dev_html, "Dev")]:
|
||||
index = build_index(files)
|
||||
with open(os.path.join(d, "index.html"), "w") as f:
|
||||
f.write(index)
|
||||
with open(os.path.join(d, "login.html"), "w") as f:
|
||||
f.write(login_page())
|
||||
print(f" {label}: {len(files)} pages -> {d}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user