370 lines
13 KiB
Python
370 lines
13 KiB
Python
"""
|
|
Album - Documentation system.
|
|
"""
|
|
|
|
import os
|
|
import httpx
|
|
from pathlib import Path
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
app = FastAPI(title="Album", version="0.1.0")
|
|
|
|
BASE_DIR = Path(__file__).parent.resolve()
|
|
BOOK_DIR = BASE_DIR / "book"
|
|
STATIC_DIR = BASE_DIR / "static"
|
|
|
|
# Create static directory if it doesn't exist
|
|
STATIC_DIR.mkdir(exist_ok=True)
|
|
|
|
templates = Jinja2Templates(directory=str(BASE_DIR))
|
|
|
|
# Serve static files
|
|
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
|
|
# Pawprint URL for data fetching
|
|
PAWPRINT_URL = os.getenv("PAWPRINT_URL", "http://localhost:12000")
|
|
|
|
|
|
def get_data():
|
|
"""Fetch data from pawprint hub."""
|
|
try:
|
|
resp = httpx.get(f"{PAWPRINT_URL}/api/data/album", timeout=5.0)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
except Exception as e:
|
|
print(f"Failed to fetch data from pawprint: {e}")
|
|
return {"templates": [], "larders": [], "books": []}
|
|
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
return {"status": "ok", "service": "album"}
|
|
|
|
|
|
@app.get("/")
|
|
def index(request: Request):
|
|
data = get_data()
|
|
return templates.TemplateResponse("index.html", {
|
|
"request": request,
|
|
"pawprint_url": os.getenv("PAWPRINT_EXTERNAL_URL", PAWPRINT_URL),
|
|
**data,
|
|
})
|
|
|
|
|
|
@app.get("/api/data")
|
|
def api_data():
|
|
"""API endpoint for frontend data (proxied from pawprint)."""
|
|
return get_data()
|
|
|
|
|
|
# --- Book: Feature Flow (HTML presentations) ---
|
|
@app.get("/book/feature-flow/", response_class=HTMLResponse)
|
|
@app.get("/book/feature-flow", response_class=HTMLResponse)
|
|
def feature_flow_index():
|
|
"""Redirect to English presentation by default"""
|
|
return """<!DOCTYPE html>
|
|
<html><head><meta charset="UTF-8"><title>Feature Flow</title>
|
|
<style>
|
|
body { font-family: system-ui; background: #0f172a; color: #e2e8f0; min-height: 100vh;
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
|
h1 { font-size: 2rem; margin-bottom: 2rem; }
|
|
.links { display: flex; gap: 1rem; }
|
|
a { background: #334155; color: #e2e8f0; padding: 1rem 2rem; border-radius: 8px;
|
|
text-decoration: none; font-size: 1.1rem; }
|
|
a:hover { background: #475569; }
|
|
</style></head>
|
|
<body>
|
|
<h1>Feature Flow - Standardization Pipeline</h1>
|
|
<div class="links">
|
|
<a href="/book/feature-flow/en">English</a>
|
|
<a href="/book/feature-flow/es">Español</a>
|
|
</div>
|
|
</body></html>"""
|
|
|
|
|
|
@app.get("/book/feature-flow/en", response_class=HTMLResponse)
|
|
def feature_flow_en():
|
|
html_file = BOOK_DIR / "feature-flow" / "index-en.html"
|
|
return HTMLResponse(html_file.read_text())
|
|
|
|
|
|
@app.get("/book/feature-flow/es", response_class=HTMLResponse)
|
|
def feature_flow_es():
|
|
html_file = BOOK_DIR / "feature-flow" / "index-es.html"
|
|
return HTMLResponse(html_file.read_text())
|
|
|
|
|
|
# --- Book: Feature Form Samples (templated book) ---
|
|
@app.get("/book/feature-form-samples/", response_class=HTMLResponse)
|
|
@app.get("/book/feature-form-samples", response_class=HTMLResponse)
|
|
def feature_form_samples_index(request: Request):
|
|
"""Templated book landing page"""
|
|
data = get_data()
|
|
book = next((b for b in data.get("books", []) if b["slug"] == "feature-form-samples"), None)
|
|
if not book:
|
|
return HTMLResponse("<h1>Book not found</h1>", status_code=404)
|
|
return templates.TemplateResponse("book-template.html", {
|
|
"request": request,
|
|
"book": book,
|
|
})
|
|
|
|
|
|
@app.get("/book/feature-form-samples/template/", response_class=HTMLResponse)
|
|
@app.get("/book/feature-form-samples/template", response_class=HTMLResponse)
|
|
def feature_form_samples_template():
|
|
"""View the template - styled like actual feature forms"""
|
|
html = """<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Feature Form Template · Album</title>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
background: #f8fafc;
|
|
color: #1e293b;
|
|
line-height: 1.6;
|
|
}
|
|
.container { max-width: 800px; margin: 0 auto; padding: 2rem 1rem; }
|
|
header { margin-bottom: 1.5rem; }
|
|
.breadcrumb { font-size: 0.9rem; color: #64748b; margin-bottom: 0.5rem; }
|
|
.breadcrumb a { color: #15803d; text-decoration: none; }
|
|
.breadcrumb a:hover { text-decoration: underline; }
|
|
h1 { font-size: 1.5rem; color: #15803d; }
|
|
.meta { display: flex; gap: 0.5rem; margin-top: 0.5rem; font-size: 0.8rem; }
|
|
.meta span { background: #f1f5f9; padding: 0.2rem 0.5rem; border-radius: 4px; color: #64748b; }
|
|
.meta .template { background: #dbeafe; color: #1d4ed8; }
|
|
|
|
.form-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
}
|
|
.form-header {
|
|
background: linear-gradient(135deg, #15803d, #22c55e);
|
|
color: white;
|
|
padding: 1rem 1.5rem;
|
|
}
|
|
.form-header h2 { font-size: 1.1rem; font-weight: 600; }
|
|
.form-body { padding: 1.5rem; }
|
|
|
|
.field { margin-bottom: 1.25rem; }
|
|
.field:last-child { margin-bottom: 0; }
|
|
.field-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: #64748b;
|
|
margin-bottom: 0.35rem;
|
|
}
|
|
.field-value {
|
|
background: #f8fafc;
|
|
border: 2px dashed #cbd5e1;
|
|
border-radius: 6px;
|
|
padding: 0.75rem;
|
|
font-size: 0.95rem;
|
|
color: #94a3b8;
|
|
font-style: italic;
|
|
min-height: 2.5rem;
|
|
}
|
|
.field-value.multiline { min-height: 4rem; }
|
|
|
|
.field-steps .field-value { padding-left: 0.5rem; }
|
|
.field-steps ol { list-style: none; padding-left: 0; margin: 0; }
|
|
.field-steps li {
|
|
position: relative;
|
|
padding-left: 2rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.field-steps li::before {
|
|
content: attr(data-step);
|
|
position: absolute;
|
|
left: 0;
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
background: #cbd5e1;
|
|
color: white;
|
|
border-radius: 50%;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.field-problems .field-value { background: #fef2f2; border-color: #fecaca; }
|
|
.field-special .field-value { background: #fffbeb; border-color: #fde68a; }
|
|
.field-technical .field-value { background: #f0fdf4; border-color: #bbf7d0; font-family: monospace; font-size: 0.85rem; }
|
|
|
|
footer {
|
|
margin-top: 2rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid #e2e8f0;
|
|
font-size: 0.85rem;
|
|
}
|
|
footer a { color: #15803d; text-decoration: none; }
|
|
footer a:hover { text-decoration: underline; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<div class="breadcrumb">
|
|
<a href="/">Album</a> / <a href="/book/feature-form-samples/">Feature Form Samples</a> / Template
|
|
</div>
|
|
<h1>Feature Form Template</h1>
|
|
<div class="meta">
|
|
<span class="template">Template</span>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="form-card">
|
|
<div class="form-header">
|
|
<h2>[Nombre del Flujo]</h2>
|
|
</div>
|
|
<div class="form-body">
|
|
<div class="field">
|
|
<label class="field-label">Tipo de Usuario</label>
|
|
<div class="field-value">[Dueno de mascota / Veterinario / Admin]</div>
|
|
</div>
|
|
<div class="field">
|
|
<label class="field-label">Punto de Entrada</label>
|
|
<div class="field-value">[Que pagina/boton/link]</div>
|
|
</div>
|
|
<div class="field">
|
|
<label class="field-label">Objetivo del Usuario</label>
|
|
<div class="field-value">[Objetivo en una oracion]</div>
|
|
</div>
|
|
<div class="field field-steps">
|
|
<label class="field-label">Pasos</label>
|
|
<div class="field-value multiline">
|
|
<ol>
|
|
<li data-step="1">[Primera cosa que hace el usuario]</li>
|
|
<li data-step="2">[Segunda cosa que hace el usuario]</li>
|
|
<li data-step="3">[etc.]</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label class="field-label">Resultado Esperado</label>
|
|
<div class="field-value">[Resultado esperado cuando todo funciona]</div>
|
|
</div>
|
|
<div class="field field-problems">
|
|
<label class="field-label">Problemas Comunes</label>
|
|
<div class="field-value multiline">[Problema 1]<br>[Problema 2]</div>
|
|
</div>
|
|
<div class="field field-special">
|
|
<label class="field-label">Casos Especiales</label>
|
|
<div class="field-value multiline">[Caso especial 1]<br>[Caso especial 2]</div>
|
|
</div>
|
|
<div class="field">
|
|
<label class="field-label">Flujos Relacionados</label>
|
|
<div class="field-value">[Otros flujos que se conectan con este]</div>
|
|
</div>
|
|
<div class="field field-technical">
|
|
<label class="field-label">Notas Tecnicas</label>
|
|
<div class="field-value">[Notas para el equipo de desarrollo]</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>
|
|
<a href="/book/feature-form-samples/">← Feature Form Samples</a>
|
|
</footer>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
return HTMLResponse(html)
|
|
|
|
|
|
@app.get("/book/feature-form-samples/larder/", response_class=HTMLResponse)
|
|
@app.get("/book/feature-form-samples/larder", response_class=HTMLResponse)
|
|
def feature_form_samples_larder():
|
|
"""Browse the larder (actual data)"""
|
|
html_file = BOOK_DIR / "feature-form-samples" / "index.html"
|
|
if html_file.exists():
|
|
return HTMLResponse(html_file.read_text())
|
|
return HTMLResponse("<h1>Larder index not found</h1>", status_code=404)
|
|
|
|
|
|
@app.get("/book/feature-form-samples/larder/{user_type}/{filename}", response_class=HTMLResponse)
|
|
def feature_form_samples_detail(request: Request, user_type: str, filename: str):
|
|
"""View a specific feature form"""
|
|
# Look in the larder subfolder (feature-form)
|
|
larder_dir = BOOK_DIR / "feature-form-samples" / "feature-form"
|
|
file_path = larder_dir / user_type / filename
|
|
if not file_path.exists():
|
|
return HTMLResponse("<h1>Not found</h1>", status_code=404)
|
|
|
|
content = file_path.read_text()
|
|
detail_file = BOOK_DIR / "feature-form-samples" / "detail.html"
|
|
return templates.TemplateResponse(str(detail_file.relative_to(BASE_DIR)), {
|
|
"request": request,
|
|
"user_type": user_type,
|
|
"filename": filename,
|
|
"content": content,
|
|
})
|
|
|
|
|
|
# --- Book: Gherkin Samples ---
|
|
@app.get("/book/gherkin-samples/", response_class=HTMLResponse)
|
|
@app.get("/book/gherkin-samples", response_class=HTMLResponse)
|
|
@app.get("/book/gherkin/", response_class=HTMLResponse) # Alias
|
|
@app.get("/book/gherkin", response_class=HTMLResponse) # Alias
|
|
def gherkin_samples_index():
|
|
"""Browse gherkin samples"""
|
|
html_file = BOOK_DIR / "gherkin-samples" / "index.html"
|
|
return HTMLResponse(html_file.read_text())
|
|
|
|
|
|
@app.get("/book/gherkin-samples/{lang}/{user_type}/{filename}", response_class=HTMLResponse)
|
|
@app.get("/book/gherkin/{lang}/{user_type}/{filename}", response_class=HTMLResponse) # Alias
|
|
def gherkin_samples_detail(request: Request, lang: str, user_type: str, filename: str):
|
|
"""View a specific gherkin file"""
|
|
file_path = BOOK_DIR / "gherkin-samples" / lang / user_type / filename
|
|
if not file_path.exists() or not file_path.suffix == ".feature":
|
|
return HTMLResponse("<h1>Not found</h1>", status_code=404)
|
|
|
|
content = file_path.read_text()
|
|
detail_file = BOOK_DIR / "gherkin-samples" / "detail.html"
|
|
return templates.TemplateResponse(str(detail_file.relative_to(BASE_DIR)), {
|
|
"request": request,
|
|
"lang": lang,
|
|
"user_type": user_type,
|
|
"filename": filename,
|
|
"content": content,
|
|
})
|
|
|
|
|
|
# --- Book: Architecture Model (static site) ---
|
|
app.mount("/book/arch-model", StaticFiles(directory=str(BOOK_DIR / "arch-model"), html=True), name="arch-model")
|
|
|
|
|
|
# --- Book: Drive Index ---
|
|
@app.get("/book/drive-index/", response_class=HTMLResponse)
|
|
@app.get("/book/drive-index", response_class=HTMLResponse)
|
|
def drive_index():
|
|
"""Browse drive index"""
|
|
html_file = BOOK_DIR / "drive-index" / "index.html"
|
|
return HTMLResponse(html_file.read_text())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(
|
|
"main:app",
|
|
host="0.0.0.0",
|
|
port=int(os.getenv("PORT", "12002")),
|
|
reload=os.getenv("DEV", "").lower() in ("1", "true"),
|
|
)
|