Files
app-hejyou/knowledge/directus_struktur.md
2026-04-23 22:06:12 +02:00

376 lines
14 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.
# Directus Datenbankstruktur — Language App
**Domain:** https://db.hejyou.com
**Stand:** 2026-04-23
**Eigene Collections:** 10 (inkl. Junction `questions_words`)
**Frontend:** React 19 + Vite, kein Router-Package (State-basiertes Routing)
---
## App-Konzept (Kurzfassung)
Social-Media-Feed-Style-Sprachlern-App mit Fokus auf "Language Parenting" (Kontext & Immersion statt Grammatikdrill).
**Datenfluss:**
1. Ein Bild wird in `pictures` hochgeladen
2. KI-Analyse erkennt Objekte → werden in `objects` gespeichert (mit bbox, confidence, Label-Wort)
3. Zum Bild werden `questions` generiert (z. B. "Kannst du die Möwe fliegen sehen?")
4. Jedes Wort der Frage wird in `words` gespeichert und per M2M (`questions_words`) verknüpft
**Lern-Flow in der App:**
- User scrollt durch Feed-Kacheln
- Kacheln existieren **nicht als DB-Einträge**, sondern werden dynamisch aus `words` / `questions` generiert
- Kachel-Typen: `listen`, `speak`, `write`, `multiple_choice`, `image_match`, `sentence_fill`
- 1 EP pro gelöster Kachel
- Erst alle Wörter einer Frage auf Ziellevel → dann Satzfrage freischalten
- Danach gemischter Feed
---
## Sprachrichtung
- **Zielsprachen:** Alle drei gegenseitig (DE/EN/SE × DE/EN/SE)
- Gespeichert in `directus_users.language_native` + `language_target` (M2O → `language_options`)
- Zusätzlich `learning_pairs` pro aktive Sprachrichtung (mit eigenem Level + Punkten)
---
## App-Architektur (Frontend)
### Navigation
4 Seiten, über `BottomNav` (Tabs) gesteuerter `useState`-Router in `App.jsx`:
| Tab | Seite | Status |
|-----|-------|--------|
| Feed | `pages/Feed.jsx` | Implementiert, Directus-Daten |
| Game | `pages/Game.jsx` | Placeholder |
| Pro | `pages/Pro.jsx` | Placeholder |
| Profil | `pages/Profil.jsx` | Implementiert, Directus-Daten |
`App.jsx` zeigt `<AuthScreen />` wenn `!user.username || !user.language_native || !user.language_target`.
### Auth-Flow (2 Schritte)
```
RegisterStep1 (E-Mail + Passwort)
→ registerUser() + login() → Token in localStorage (hejyou_token)
→ onSuccess(userId, token) → RegisterStep2
RegisterStep2 (Username + Muttersprache + Zielsprache)
→ checkUsername() → createProfile()
→ POST /items/users_language
→ PATCH /users/:id (username, language_native, language_target)
→ PATCH /items/users_language/:id (user-Verknüpfung)
→ POST /items/learning_pairs
→ setUser() → AuthScreen zeigt Erfolgsscreen
AuthContext überwacht Token, ruft getMe() beim Start
```
Token-Key im localStorage: `hejyou_token`
### Frontend API-Funktionen (`src/api/directus.js`)
| Funktion | Endpoint | Zweck |
|----------|----------|-------|
| `login(email, pw)` | POST `/auth/login` | Token holen |
| `getMe(token)` | GET `/users/me` | Basisfelder für Auth-Check |
| `getProfilData(token)` | GET `/users/me` (erweitert) | Profil inkl. username-Objekt, points_total, streak_days |
| `registerUser(email, pw)` | POST `/users` | Neuen Directus-User anlegen |
| `checkUsername(name)` | GET `/items/users_language` (filter) | Verfügbarkeit prüfen |
| `createProfile(...)` | 4 PATCH/POST-Calls | Vollständiges Onboarding |
| `getActiveLearningPair(profileId)` | GET `/items/learning_pairs` (filter active) | Aktives Lernpaar des Nutzers |
| `getWords(limit)` | GET `/items/words` | Wörter für Feed-Karten |
| `langById(id)` | lokal | LANGUAGE_OPTIONS-Lookup per UUID |
### LANGUAGE_OPTIONS (Frontend-Konstante)
```js
// Felder pro Sprache:
{ id, label, flag, suffix, speech }
```
| Sprache | suffix | speech |
|---------|--------|--------|
| Deutsch | `de` | `de-DE` |
| Englisch | `en` | `en-US` |
| Schwedisch | `se` | `sv-SE` |
`suffix` wird genutzt um DB-Felder dynamisch zu bauen: `title_${suffix}` (z.B. `title_de`).
`speech` wird an die Web Speech API (SpeechRecognition) übergeben.
### Feed-Karten-Generierung
`buildCards(words, fromLang, toLang)` erzeugt pro Wort bis zu 3 Kacheln:
| Kachel-Typ | Komponente | Bedingung |
|------------|-----------|-----------|
| `text` | `NewWordTextCard` | immer |
| `voice` | `NewWordVoiceCard` | immer |
| `letter` | `LetterOrderCard` | `word.length >= 4` |
Felder aus DB: `word = words.title_${to.suffix}`, `translation = words.title_${from.suffix}`
Zusätzlich werden Demo-Karten (`DEMO_EXTRA`) an den echten Feed angehängt, solange Bilder/Audio noch nicht in der DB vorhanden sind (ImagePickCard, AudioQuizCard, ImageQuizCard).
> **Noch nicht implementiert:** Nach Kartenabschluss wird aktuell **kein** `user_progress`-Eintrag angelegt und `learning_pairs.points` wird nicht aktualisiert. Das ist der nächste Schritt.
---
## Collections im Überblick
| Collection | Zweck |
|---|---|
| `categories` | Kategorien für Wörter/Objekte (Farben, Zuhause, Tiere, …) |
| `language_options` | Verfügbare Sprachen (DE/EN/SE) |
| `learning_pairs` | Aktive Sprachrichtungen pro Nutzer, mit Level & Punkten |
| `objects` | Von KI erkannte Objekte in Bildern (bbox, confidence) |
| `pictures` | Bilder, Ausgangspunkt für Fragen |
| `questions` | Fragen/Sätze für den Lernfeed |
| `questions_words` | Junction: M2M Fragen ↔ Wörter |
| `user_progress` | Lernfortschritt pro Nutzer × Kachel |
| `users_language` | Öffentliches Profil (Username, Avatar) |
| `words` | Vokabeln in DE/EN/SE mit Level & Kategorie |
---
## Collection-Details
### `categories`
Kategorien für Wörter und Objekte.
| Feld | Typ | Notiz |
|---|---|---|
| `id` | uuid (PK) | |
| `status` | string | draft/published/archived |
| `sort` | integer | Sortierreihenfolge |
| `Icon` | string | Material-Icon-Name |
| `type` | string | Dropdown, Werte undefiniert |
| `title_en/de/se` | string | Titel in 3 Sprachen |
---
### `language_options`
Verfügbare Sprachen.
| Feld | Typ | Notiz |
|---|---|---|
| `id` | uuid (PK) | |
| `status` | string | |
| `short` | string | Kurzcode: `de`, `en`, `se` |
| `title_en/de/se` | string | |
> Frontend-Mapping: `LANGUAGE_OPTIONS` in `directus.js` ergänzt die DB-Werte um `suffix` (= `short`), `speech` (Web Speech API Code), `flag` (Emoji) und `label` (Anzeigename auf Deutsch).
**Aktuelle Einträge:**
- DE: `88053026-3d7e-4799-b10d-67187f7c1709`
- EN: `99fbaa9d-3cac-48cb-a5e2-dcb320e913e4`
- SE: `25350b32-e9ab-4fec-946e-c0f11eff70dd`
---
### `learning_pairs`
Welche Sprachrichtungen ein Nutzer aktiv lernt.
| Feld | Typ | Notiz |
|---|---|---|
| `id` | uuid (PK) | |
| `user` | uuid M2O → `users_language` | |
| `language_from` | uuid M2O → `language_options` | Ausgangssprache |
| `language_to` | uuid M2O → `language_options` | Zielsprache |
| `active` | boolean | Aktuell aktiv? |
| `current_level` | integer (110) | Aktuelles Level in dieser Richtung |
| `points` | integer | Punkte in dieser Richtung |
---
### `words`
Vokabeln in 3 Sprachen.
| Feld | Typ | Notiz |
|---|---|---|
| `id` | uuid (PK) | |
| `status` | string | |
| `title_de/en/se` | string | Wort in 3 Sprachen |
| `categories` | uuid M2O → `categories` | Ein Wort gehört zu einer Kategorie |
| `image` | uuid file → `directus_files` | Bild zum Wort (optional) |
| `audio_de/en/se` | uuid file → `directus_files` | Aussprache pro Sprache |
| `level` | integer (110) | Schwierigkeitslevel |
| `times_learned` | integer | Global, wie oft insgesamt gelernt |
| `related_questions` | alias M2M → `questions` | Alle Fragen, in denen das Wort vorkommt |
> `getWords()` fetcht aktuell nur `id, title_de, title_en, title_se, level`. `image` und `audio_de/en/se` werden noch nicht geladen → ImagePickCard und AudioQuizCard laufen noch mit Demo-Daten.
> **Hinweis (2026-04-23):** Felder `categories`, `image`, `audio_de`, `audio_en`, `audio_se` wurden mit korrekten FK-Relationen und `special`-Markierungen angelegt.
---
### `questions`
Fragen/Sätze, die im Feed erscheinen.
| Feld | Typ | Notiz |
|---|---|---|
| `id` | uuid (PK) | |
| `status` | string | |
| `question_de/en/se` | string | Frage in 3 Sprachen |
| `answer_de/en/se` | string | Korrekte Antwort |
| `level` | integer (110) | Schwierigkeitslevel |
| `related_words` | alias M2M → `words` | Alle Wörter der Frage |
> Hinweis: `questions.type` (Kachel-Typen) wurde entfernt Kacheln werden dynamisch in der App generiert, nicht pro Frage gespeichert.
---
### `questions_words` (Junction-Collection für M2M)
Verknüpft Fragen und Wörter (M:N).
| Feld | Typ |
|---|---|
| `id` | integer (PK, auto) |
| `questions_id` | uuid → `questions` (CASCADE) |
| `words_id` | uuid → `words` (CASCADE) |
---
### `pictures`
Bilder als Ausgangspunkt für Fragen.
| Feld | Typ | Notiz |
|---|---|---|
| `id` | uuid (PK) | |
| `media` | uuid → directus_files | Das eigentliche Bild |
| `type` | string | `simple_object` / `multi_object` |
| `season` | string | `spring`/`summer`/`fall`/`winter` |
| `objects` | uuid M2O → `objects` | Legacy-Feld (zeigt auf ein einzelnes Objekt, nicht mehr nutzen) |
| `objects` (O2M alias) | alias O2M ← `objects.picture` | Alle Objekte dieses Bildes — über `objects.picture` Relation |
> **Hinweis:** Die korrekte Beziehung läuft über `objects.picture` (M2O von objects zu pictures). Das alte `pictures.objects`-Feld ist ein Legacy-FK und wird nicht mehr befüllt.
---
### `objects`
Von der KI erkannte Objekte innerhalb eines Bildes.
| Feld | Typ | Notiz |
|---|---|---|
| `id` | uuid (PK) | |
| `picture` | uuid M2O → `pictures` | Bild, zu dem dieses Objekt gehört (O2M-Seite: `pictures.objects`) |
| `media` | uuid → directus_files | Optionaler Zuschnitt des Objekts im Bild |
| `categories` | uuid M2O → `categories` | |
| `label` | uuid M2O → `words` | Name des Objekts als Wort |
| `color` | uuid M2O → `words` | Farbe des Objekts (optional) |
| `action` | uuid M2O → `words` | Aktion des Objekts (z. B. "fliegen") |
| `questions` | uuid M2O → `questions` | Generierte Frage zum Objekt |
| `resolution` | string | z. B. `1920x1080` |
| `confidence` | float | KI-Konfidenz 01 |
| `bbox` | json | `{x, y, w, h}` |
| `polygon` | json | Polygon-Koordinaten |
| `parent` | uuid M2O → `objects` | Hierarchie (Szene → Teilobjekte) |
> **Hinweis (2026-04-23):** `objects.picture` wurde ergänzt, um die O2M-Beziehung zu `pictures` herzustellen. Ein Bild kann nun mehrere Objekte haben. Bestehende Testdaten (Möwen-Objekt) wurden verknüpft.
---
### `users_language`
Öffentliches Nutzerprofil (gekoppelt mit `directus_users`).
| Feld | Typ | Notiz |
|---|---|---|
| `id` | uuid (PK) | |
| `username_public` | string | Angezeigter Name |
| `username_lowercases` | string | Für Suche/Eindeutigkeit |
| `user` | uuid M2O → `directus_users` | Verknüpfung zum System-User |
---
### `directus_users` (System-Collection, custom fields)
| Feld | Typ | Notiz |
|---|---|---|
| `app_type` | string | `socialmedia` / `language` |
| `language_native` | uuid M2O → `language_options` | Muttersprache |
| `language_target` | uuid M2O → `language_options` | Zielsprache |
| `username` | uuid M2O → `users_language` | Verknüpfung zum Profil |
| `points_total` | integer | Gesamtpunkte über alle Richtungen |
| `streak_days` | integer | Aktuelle Lernserie (Tage) |
| `streak_last_activity` | date | Für Streak-Berechnung |
---
### `user_progress`
Trackt jede gelöste/übersprungene Kachel pro Nutzer.
> **Status:** Schema definiert, aber noch **nicht in der App implementiert**. Karten-Ergebnisse werden aktuell nur lokal angezeigt, nicht in die DB geschrieben. Nächster Schritt: `saveProgress()`-Funktion in `directus.js` + Aufruf nach Kartenabschluss.
| Feld | Typ | Notiz |
|---|---|---|
| `id` | uuid (PK) | |
| `user` | uuid M2O → `users_language` | Nutzerprofil |
| `question` | uuid M2O → `questions` | Wenn Kachel zu einer Frage gehört |
| `word` | uuid M2O → `words` | Wenn Kachel zu einem Einzelwort gehört |
| `card_type` | string | `listen` / `speak` / `write` / `multiple_choice` / `image_match` / `sentence_fill` |
| `result` | string | `correct` / `wrong` / `skipped` |
| `points_earned` | integer | Verdiente EP (1 pro gelöster Kachel) |
| `language_from` | uuid M2O → `language_options` | Ausgangssprache |
| `language_to` | uuid M2O → `language_options` | Zielsprache |
> Entweder `word` ODER `question` ist gesetzt, je nachdem ob die Kachel ein Einzelwort oder eine Satzfrage trainiert.
---
## Beziehungs-Diagramm
```
users_language ──┬── learning_pairs (pro Sprachrichtung)
├── user_progress (pro Kachel)
└── directus_users (System-User)
pictures ──── objects (O2M via objects.picture) ──── questions
│ │ │
└── media ├── picture → pictures (M2O) ├── related_words (M2M via questions_words)
├── label → words └── answer (de/en/se)
├── color → words
└── action → words
words ──── categories (M2O)
├── image → directus_files
└── audio_de/en/se → directus_files
```
---
## Testdaten (Tim, Stand 2026-04-22)
- **User (users_language):** `24f9a499-e36d-4df8-91f7-6a7abc6f42ce` (Tim1505)
- **3 Kategorien:** Farben, Zuhause, Tiere
- **12 Wörter** (Rot, Blau, Grün, Tisch, Stuhl, Fenster, Hund, Katze, Vogel, Möwe, fliegen, sehen)
- **8 Fragen** (7 Single-Word + 1 komplexe Möwen-Frage mit 3 verknüpften Wörtern)
- **1 Bild** im Herbst (`multi_object`) mit erkanntem Möwen-Objekt (94% confidence)
- **2 learning_pairs:** DE→SE (aktiv, 30P), DE→EN (inaktiv, 10P)
- **6 user_progress-Einträge** mit verschiedenen card_types (listen/speak/write/multiple_choice/image_match)
---
## Offene Punkte / Nächste Schritte
| Priorität | Aufgabe |
|-----------|---------|
| 🔴 | `user_progress` schreiben nach Kartenabschluss |
| 🔴 | `learning_pairs.points` + `directus_users.points_total` updaten |
| 🟠 | `words.image` + `words.audio_*` in `getWords()` laden → echte ImagePickCard / AudioQuizCard |
| 🟠 | `directus_users.streak_days` + `streak_last_activity` serverseitig aktualisieren |
| 🟡 | `questions` Collection in Feed integrieren (Satzlevel-Kacheln) |
| 🟡 | Game und Pro Pages implementieren |
---
## Kachel-Logik (für die App)
Die App generiert Kacheln dynamisch aus den vorhandenen Daten eines Wortes:
- `audio_de/en/se` vorhanden → `listen`, `speak` möglich
- `image` vorhanden → `image_match` möglich
- Mindestens 3 Wörter in gleicher Kategorie → `multiple_choice` möglich
- Zielsprache setzen via `learning_pairs.language_to`
Pro Abschluss: +1 EP in `user_progress.points_earned` + Update in `learning_pairs.points`.