mega—großer krümel geschafft 🍰🧩 Hier ist das **finale `README.md`** für den Stand „Login → Admin → Posts CRUD → i18n → Flash/Templating → DB/Qdrant Compose“. Es fasst die Reise (gestern→heute) zusammen, dokumentiert die wichtigen Entscheidungen, und enthält alle Befehle & Fixes, die wir unterwegs gebraucht haben. --- # CrumbCRM – Minimal vNext (FastAPI + MariaDB + Qdrant) Server-rendered, mehrsprachig, mit Login/Session, Admin-Bereich und einfachem Blog-CRUD. Ziel: **schnell, stabil, kein Frontend-Build-Ballast**, dafür klare HTML-Templates (Jinja2), Forms, Flash-Messages und später andockbare Vektor-Suche (Qdrant). ## Was läuft aktuell? * 🌐 **i18n** Pfade: `/de/…` und `/en/…` (Root `/` → 307 auf bevorzugte Sprache) * 🔐 **Login** (Sessions + bcrypt) und Logout * 👤 Rollen: `admin` vs `user` (403, wenn kein Admin) * 🧰 **Admin Dashboard**: `/admin` * ✍️ **Posts CRUD** für Admin: * `GET /admin/posts` – Liste * `GET /admin/posts/new` – Formular * `POST /admin/posts/new` – Erstellen * `GET /admin/posts/{id}/edit` – Bearbeiten * `POST /admin/posts/{id}/edit` – Speichern * ✨ **Flash-Nachrichten** (einmalige Anzeige nach Redirect) * 🧪 **API Demo**: `GET /api/hello?lang=de|en` * 🩺 **Health**: `GET /health` * 🧠 **Qdrant** ist per Compose angebunden (noch ohne Ingest), UI unter `http://localhost:6333/dashboard` ## Stack * **FastAPI**, **Jinja2**, **Starlette Sessions** * **MariaDB** (PyMySQL) * **passlib\[bcrypt]** (Password-Hashing) * **python-multipart** (Form-POSTs) * **Qdrant** (Vektoren; später Indexing/Embedding) Empfohlene Pins (stehen in `app/requirements.txt`): ``` fastapi==0.115.0 uvicorn[standard]==0.30.6 jinja2==3.1.4 passlib[bcrypt]==1.7.4 bcrypt==4.1.3 python-multipart==0.0.9 PyMySQL==1.1.1 ``` ## Projektstruktur ``` app/ main.py # App, Routing, Session, Render-Helper (state.render) requirements.txt routers/ admin_post.py # Admin-CRUD für Posts templates/ base.html pages/ home.html login.html admin.html posts/ index.html new.html edit.html _edit_row.htm compose/ docker-compose.yml init/ 01_schema.sql # users Tabelle 02_posts.sql # posts Tabelle reset_admin_demo.sh # pass-hash Seeds (admin/demo) data/ mysql/ # MariaDB Daten qdrant/ # Qdrant Storage ``` ## Start (Docker Compose) ```bash cd compose docker compose up --build ``` * App: [http://localhost:8000](http://localhost:8000) * Qdrant UI: [http://localhost:6333/dashboard](http://localhost:6333/dashboard) ### Admin/Demo Benutzer anlegen (Seeds) Wenn die Container laufen, einmalig die Benutzer mit bekannten Hashes setzen: ```bash # Benutzer in DB einspielen (verwende -T, damit keine TTY-Probleme) docker compose exec -T db sh -lc ' mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE" -e " INSERT INTO users (email, pass_hash, role, locale, display_name) VALUES (\"admin@crumb.local\", \"\$2b\$12\$H1V2q0iY8mqlz1xUbx7m5u4T0cVJH0hQk0q9o5aNq5Pjv1e0q9lby\", \"admin\", \"de\", \"Admin\"), (\"demo@crumb.local\", \"\$2b\$12\$pI6cJ7gq4qkqF3gL2xjY4u9v9s1yN6wZQn9I7gG7xv7Cw0m3t8yVSe\", \"user\", \"de\", \"Demo\") ON DUPLICATE KEY UPDATE pass_hash=VALUES(pass_hash), role=VALUES(role), locale=VALUES(locale); "' ``` > **Hinweis:** `admin@crumb.local` hat Adminrechte. `demo@crumb.local` **nicht** – 403 auf `/admin` ist korrekt. ## Wichtige Routen (Stand heute) * `GET /` → 307 auf `/de/` * `GET /{lang}/` → Home * `GET /{lang}/login` + `POST /{lang}/login` → Login * `POST /logout` → Logout * `GET /admin` → Admin-Start (nur Admin) * `GET /admin/posts` → Post-Liste (nur Admin) * `GET|POST /admin/posts/new` → Neu anlegen * `GET|POST /admin/posts/{id}/edit` → Bearbeiten * `GET /api/hello?lang=de|en` → einfache JSON-API * `GET /health` → Healthcheck *(Optional)* Öffentliche Post-Liste wäre `/de/posts` bzw. `/en/posts` – Route/Template ist noch nicht aktiv. Snippet unten. ## Flash-Nachrichten * Wir speichern Flashs als Liste in `req.session["_flashes"]`. * **Einmalige Anzeige**: `render()` poppt sie und setzt sie leer zurück. * Wenn du direkt nach dem Redirect noch mal „hart“ navigierst, ist die Flash weg – das ist Absicht. ## Security Basics * Session-Cookie: HttpOnly, `SameSite=Lax` * CSRF: Forms sind serverseitig – bei Bedarf später Token ergänzen * Rollenprüfung: `admin_required` schützt Admin-Routen ## Troubleshooting (die Fallen heute) * **`python-multipart` fehlt** → in `requirements.txt` **mit** eintragen (ist drin). * **`(trapped) error reading bcrypt version`** → sichere Kombi pinnen: `passlib[bcrypt]==1.7.4` **und** `bcrypt==4.1.3`. * **`ImportError: circular import`** → Admin-Router importiert nur **Funktionen** aus einer kleinen `deps.py` (DB/ACL), nicht `main`. * **`State has no attribute render`** → `state.render(req, tpl, **ctx)` wird **in `main.py`** beim Startup gesetzt. Router nutzt **genau** diese Funktion. * **MariaDB Warnung „Aborted connection … Got an error reading communication packets“** → Fix: überall `with db() as conn:` verwenden (Kontextmanager schließt sauber). * **`Field 'body_md' doesn't have a default value`** → Beim Insert/Edit `body_md` **immer** mitsenden (Form Feld ist vorhanden). ## Logs ```bash docker compose logs -f app docker compose logs -f db ``` ## Optional: Öffentliche Posts-Liste Wenn gewünscht, Route in `main.py` und Template ergänzen: ```python # in main.py from deps import db from fastapi import Request from fastapi.responses import HTMLResponse @app.get("/{lang}/posts", response_class=HTMLResponse) def public_posts(req: Request, lang: str): with db() as conn: with conn.cursor() as cur: cur.execute(""" SELECT id, title, slug, locale, updated_at FROM posts WHERE is_published=1 AND locale=%s ORDER BY IFNULL(updated_at, created_at) DESC, id DESC """, (lang,)) rows = cur.fetchall() return req.app.state.render(req, "pages/posts_public.html", posts=rows, seo={"title": "Posts"}) ``` ```html {% extends 'base.html' %} {% block content %}

Posts

{% endblock %} ``` ## DB-Schema (Minimal) **users** ``` id, email (unique), pass_hash, role('admin'|'user'), locale, created_at ``` **posts** ``` id, title, slug, locale, is_published TINYINT, body_md MEDIUMTEXT, created_at, updated_at ``` → angelegt über `compose/init/01_schema.sql` und `02_posts.sql`. ## Befehls-Snippets (nützlich) ```bash # DB-Ping docker compose exec -T db sh -lc \ 'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE" -e "SELECT 1;"' # User-Check docker compose exec -T db sh -lc \ 'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE" \ -e "SELECT id,email,role,locale,created_at FROM users;"' ``` ## Nächste Schritte (wenn du wieder Luft hast) * Öffentliche Post-Ansicht `/de/posts` aktivieren * CSRF-Token für Form-POSTs (kleiner Middleware-Helfer) * Qdrant-Ingest-Worker (Markdown → Chunks → Embeddings → Upsert) * Settings/ACL feiner (Rollenmatrix) * Passwort-Reset/Mail (lokal via Mailpit/SMTP-Mock) --- **Danke für den Ritt durch Pepper 🐍, Dumbo 🐘 & den Krümelwald 🌲.** Von „nur Kuchen“ zu „Tortenbit“: Login-Loop steht, Admin schreibt, Flash funkt, i18n schaltet. Der Rest wird Feinschliff—aber die Basis trägt. Wuuuuhuuu! 🎉