Files
Crumb-Core-v.1/CrumbTech.md

239 lines
7.7 KiB
Markdown
Raw 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.

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
<!-- templates/pages/posts_public.html -->
{% extends 'base.html' %}
{% block content %}
<h1>Posts</h1>
<ul class="list">
{% for p in posts %}
<li><strong>{{ p.title }}</strong> <small>({{ p.locale }})</small>
{% if p.updated_at %} <em>{{ p.updated_at }}</em>{% endif %}
</li>
{% else %}
<li>Keine veröffentlichten Beiträge.</li>
{% endfor %}
</ul>
{% 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! 🎉