efihub/docs/superpowers/plans/2026-04-27-landing-page.md

371 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Landing Page & Shared Layout Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a shared Jinja2/Tailwind CSS layout and a module-card landing page for the University Process Hub.
**Architecture:** FastAPI serves HTML via `Jinja2Templates`. `base.html` defines the red navbar (`#e2001a`), a `{% block content %}` slot, and a footer. `index.html` extends it with a hero section, responsive 3-column module-card grid, and an info strip. Module metadata is a static list in `main.py` — no DB access on the landing page.
**Tech Stack:** FastAPI, Jinja2, Tailwind CSS Play CDN (no build step), pytest, httpx
---
### Task 1: Add dependencies and update .gitignore
**Files:**
- Modify: `requirements.txt`
- Modify: `.gitignore`
- [ ] **Step 1: Update `requirements.txt`**
```
fastapi
uvicorn[standard]
pydantic-settings
sqlalchemy
jinja2
pytest
httpx
```
- [ ] **Step 2: Add `.superpowers/` to `.gitignore`**
Append to the `# Project specific` section:
```
.superpowers/
```
- [ ] **Step 3: Install new dependencies**
```bash
.venv/bin/pip install -r requirements.txt
```
Expected: `jinja2`, `pytest`, `httpx` installed without error.
- [ ] **Step 4: Commit**
```bash
git add requirements.txt .gitignore
git commit -m "chore: add jinja2 and test dependencies"
```
---
### Task 2: Create base layout template
**Files:**
- Create: `app/templates/base.html`
- Create: `app/static/.gitkeep`
- [ ] **Step 1: Create directories**
```bash
mkdir -p app/templates app/static
touch app/static/.gitkeep
```
- [ ] **Step 2: Write `app/templates/base.html`**
```html
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}EFI Hub{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: { 'efi': '#e2001a' }
}
}
}
</script>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col">
<nav class="bg-efi h-14 flex items-center px-8 shrink-0">
<a href="/" class="text-white font-bold text-lg tracking-tight mr-8">
EFI<span class="opacity-70 font-normal">Hub</span>
</a>
{% for item in nav_items %}
<a href="{{ item.url }}"
class="text-white/85 hover:text-white text-sm px-4 h-14 flex items-center border-b-2
{% if item.active %}border-white text-white{% else %}border-transparent{% endif %}">
{{ item.label }}
</a>
{% endfor %}
<div class="ml-auto">
<button class="text-white text-sm px-4 py-1.5 rounded border border-white/40 bg-white/15 hover:bg-white/25">
Anmelden
</button>
</div>
</nav>
<main class="flex-1">
{% block content %}{% endblock %}
</main>
<footer class="bg-gray-100 border-t border-gray-300 px-8 py-3.5 flex items-center justify-between text-xs text-gray-400 shrink-0">
<span class="font-semibold text-gray-500">EFI Hub</span>
<span>Technische Hochschule Nürnberg Georg Simon Ohm · Fakultät EFI</span>
<span>v{{ app_version }}</span>
</footer>
</body>
</html>
```
- [ ] **Step 3: Commit**
```bash
git add app/templates/base.html app/static/.gitkeep
git commit -m "feat: add base layout template with red navbar and footer"
```
---
### Task 3: Create landing page template
**Files:**
- Create: `app/templates/index.html`
- [ ] **Step 1: Write `app/templates/index.html`**
```html
{% extends "base.html" %}
{% block title %}University Process Hub{% endblock %}
{% block content %}
<div class="bg-white border-b border-gray-200 px-8 py-10">
<p class="text-xs font-semibold uppercase tracking-widest text-efi mb-2">
Fakultät EFI · TH Nürnberg
</p>
<h1 class="text-2xl font-bold text-gray-900 mb-2">University Process Hub</h1>
<p class="text-sm text-gray-500 max-w-xl leading-relaxed">
Modulares Informationssystem für Fakultätsprozesse.
Startpunkt für studentische Arbeiten und Schnittstelle für interne Dienste.
</p>
</div>
<div class="px-8 py-7">
<p class="text-xs font-semibold uppercase tracking-widest text-gray-400 mb-4">
Verfügbare Module
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{% for module in modules %}
<div class="bg-white rounded-md border border-gray-200 shadow-sm p-5 relative overflow-hidden
hover:shadow-md hover:border-efi transition-all duration-150 cursor-pointer">
<div class="absolute top-0 left-0 right-0 h-0.5 bg-efi"></div>
<div class="text-2xl mb-2">{{ module.icon }}</div>
<div class="font-semibold text-gray-900 text-sm mb-1">{{ module.name }}</div>
<div class="text-xs text-gray-500 leading-relaxed mb-3">{{ module.description }}</div>
{% if module.status == "active" %}
<span class="text-xs font-semibold px-2 py-0.5 rounded-full bg-green-50 text-green-700 border border-green-200">Aktiv</span>
{% else %}
<span class="text-xs font-semibold px-2 py-0.5 rounded-full bg-gray-100 text-gray-500 border border-gray-200">Geplant</span>
{% endif %}
</div>
{% endfor %}
<div class="bg-white rounded-md border border-dashed border-gray-300 p-5">
<div class="text-2xl mb-2 opacity-30"></div>
<div class="font-semibold text-gray-300 text-sm mb-1">Neues Modul</div>
<div class="text-xs text-gray-300 leading-relaxed">Erweiterbar durch studentische Arbeiten.</div>
</div>
</div>
</div>
<div class="bg-white border-t border-gray-200 px-8 py-4 flex flex-wrap items-center gap-8">
<span class="text-xs text-gray-500">API: <strong class="text-gray-800">FastAPI</strong></span>
<span class="text-xs text-gray-500">Datenbank: <strong class="text-gray-800">{{ db_mode }}</strong></span>
<span class="text-xs text-gray-500">WebSocket: <strong class="text-gray-800">aktiv</strong></span>
<div class="ml-auto text-xs font-medium text-efi">● System online</div>
</div>
{% endblock %}
```
- [ ] **Step 2: Commit**
```bash
git add app/templates/index.html
git commit -m "feat: add landing page template with module grid and info strip"
```
---
### Task 4: Wire templates into FastAPI and write tests
**Files:**
- Modify: `app/main.py`
- Create: `tests/__init__.py`
- Create: `tests/test_landing.py`
- [ ] **Step 1: Write the failing tests**
```bash
mkdir -p tests && touch tests/__init__.py
```
Schreibe `tests/test_landing.py`:
```python
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_landing_returns_html():
response = client.get("/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
def test_landing_contains_title():
response = client.get("/")
assert "University Process Hub" in response.text
def test_landing_contains_rss_module():
response = client.get("/")
assert "RSS-Feed Server" in response.text
def test_landing_navbar_links_present():
response = client.get("/")
assert "Übersicht" in response.text
assert "RSS-Feeds" in response.text
def test_landing_info_strip_shows_db_mode():
response = client.get("/")
assert "SQLite" in response.text or "MariaDB" in response.text
```
- [ ] **Step 2: Run tests to confirm they fail**
```bash
.venv/bin/pytest tests/test_landing.py -v
```
Expected: FAIL — `GET /` returns JSON, not HTML.
- [ ] **Step 3: Replace `app/main.py` with template-serving version**
```python
from fastapi import FastAPI, Request, WebSocket
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from app.core.config import get_settings
settings = get_settings()
app = FastAPI(
title="University Process Hub",
root_path=settings.APP_PREFIX,
)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
templates = Jinja2Templates(directory="app/templates")
MODULES = [
{
"icon": "📡",
"name": "RSS-Feed Server",
"description": "Aggregiert und verteilt Neuigkeiten der Fakultät als standardkonformen RSS 2.0 Feed.",
"status": "active",
},
{
"icon": "📅",
"name": "Kalender",
"description": "Veranstaltungen, Prüfungstermine und Fristen im iCal-Format.",
"status": "planned",
},
{
"icon": "🔔",
"name": "Benachrichtigungen",
"description": "Push-Nachrichten und WebSocket-basierte Echtzeit-Alerts für Studierende.",
"status": "planned",
},
{
"icon": "📊",
"name": "Auslastung",
"description": "Raum- und Ressourcenauslastung der Fakultät in Echtzeit.",
"status": "planned",
},
{
"icon": "📚",
"name": "Lehrveranstaltungen",
"description": "Stundenplan-API und Kursinformationen aus dem Campus-System.",
"status": "planned",
},
]
NAV_ITEMS = [
{"label": "Übersicht", "url": "/", "active": True},
{"label": "RSS-Feeds", "url": "/rss", "active": False},
{"label": "Kalender", "url": "/kalender", "active": False},
{"label": "Docs", "url": "/docs", "active": False},
]
def _db_mode() -> str:
url = settings.DATABASE_URL
return "SQLite (dev)" if url.startswith("sqlite") else "MariaDB (prod)"
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
return templates.TemplateResponse(
request,
"index.html",
{
"nav_items": NAV_ITEMS,
"modules": MODULES,
"db_mode": _db_mode(),
"app_version": "0.1.0",
},
)
@app.websocket("/ws/hello/{name}")
async def websocket_hello(websocket: WebSocket, name: str):
await websocket.accept()
await websocket.send_text(f"Hello, {name}!")
await websocket.close()
```
- [ ] **Step 4: Run tests to confirm they pass**
```bash
.venv/bin/pytest tests/test_landing.py -v
```
Expected: All 5 tests PASS.
- [ ] **Step 5: Verify in browser**
```bash
.venv/bin/uvicorn app.main:app --reload
```
Öffne `http://localhost:8000` — Navbar rot, Hero-Bereich, 5 Modul-Karten + Placeholder-Karte, Info-Strip, Footer.
- [ ] **Step 6: Commit**
```bash
git add app/main.py tests/
git commit -m "feat: serve landing page via Jinja2 with module grid"
```