# 10 — Projektansicht (Ordner, Unterreiter, angeheftete Aufnahmen)
Dazugekommen am 2026-08-02. Ein eigener Reiter **„Projektansicht"** neben „Aufnahmen": Aufnahmen lassen
sich in Projektordnern bündeln, ohne dass an der Aufnahme selbst etwas verändert wird.
## Modell in einem Satz
Ein **Pin ist eine Referenz, kein Besitz.** Die Aufnahme bleibt unverändert in der Aufnahmenliste, darf in
beliebig vielen Ordnern und Reitern hängen, und das Lösen einer Anheftung löscht nichts.
```mermaid
flowchart TD
P["📁 Ordner
„Project X“"] --> L0["Ebene „Ordner“
tab_id = 0"]
P --> T1["📑 Unterreiter
„Projektrelevante Aufzeichnungen“"]
P --> T2["📑 Unterreiter
„Angebote“"]
L0 --> R1["📌 #87 „1. Gespräch DFG – Projektstatusmeeting“"]
T1 --> R2["📌 #86"]
T1 --> R3["📌 #74"]
R1 -.->|"Klick"| REC["Reiter „Aufnahmen“
öffnet die Aufnahme im Detailbereich"]
R2 -.->|"Klick"| REC
```
Zwei Ebenen, mehr nicht: Ordner und darin Unterreiter. Auf **beiden** kann angeheftet werden — der Ordner
selbst ist also nicht nur ein Behälter für Reiter.
Die Ordner-Ebene ist genauso benennbar wie ein Unterreiter (Vorgabe: „Ordner"). Ihr Name steht aber in
`projects.root_tab_name` und nicht als Zeile in `project_tabs`, weil sie kein echter Reiter ist: es gibt
sie immer, sie hat keine eigene id und lässt sich nicht löschen. `PATCH …/tabs/0` schreibt deshalb ins
Projekt; ein leerer Name setzt auf „Ordner" zurück.
## Datenbank
```sql
CREATE TABLE projects (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(190) NOT NULL,
root_tab_name VARCHAR(190) NULL, -- Beschriftung der Ordner-Ebene, NULL = „Ordner"
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
CREATE TABLE project_tabs (
id INT AUTO_INCREMENT PRIMARY KEY,
project_id INT NOT NULL,
name VARCHAR(190) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX proj_idx (project_id));
CREATE TABLE project_pins (
id INT AUTO_INCREMENT PRIMARY KEY,
project_id INT NOT NULL,
tab_id INT NOT NULL DEFAULT 0, -- 0 = direkt im Ordner
recording_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_pin (project_id, tab_id, recording_id),
INDEX rec_idx (recording_id));
```
**Warum `tab_id = 0` statt `NULL`:** MySQL/MariaDB schließt `NULL`-Werte von einem `UNIQUE`-Index aus.
Mit `tab_id NULL` für „Ordner-Ebene" ließe sich dieselbe Aufnahme beliebig oft auf dieselbe Ebene
anheften, ohne dass der Index das verhindert. Mit `0` greift `UNIQUE` — das Anheften ist damit über
`INSERT IGNORE` idempotent, ein Doppelklick erzeugt keinen zweiten Eintrag.
Weitere Regeln:
- **Papierkorb:** `getProject` filtert auf `recordings.deleted_at IS NULL`. Eine gelöschte Aufnahme
verschwindet aus dem Ordner, ihr Pin bleibt aber stehen — nach dem Wiederherstellen ist sie wieder da,
ohne dass man erneut anheften muss.
- **Endgültiges Löschen:** `db.purgeRecording` räumt `project_pins` mit ab (in derselben Schleife wie
`markers`/`words`/`passages`), sonst blieben tote Verweise zurück.
- **Ordner/Reiter löschen** entfernt nur die Zeilen in `project_pins` — nie eine Aufnahme.
## Bedienung
Zwei Wege zum selben Ziel, weil beide Richtungen im Alltag vorkommen:
```mermaid
flowchart LR
subgraph A["Weg 1 — aus dem Projekt heraus"]
A1["Projektansicht → Ordner öffnen"] --> A2["Ebene wählen
(Ordner oder Unterreiter)"]
A2 --> A3["📌 Aufnahme anheften"] --> A4["Dropdown mit allen Aufnahmen
+ Filterfeld"]
end
subgraph B["Weg 2 — aus der Aufnahmenliste"]
B1["Aufnahmen → 📌 an der Zeile"] --> B2["Dialog: Ordner + Ebene wählen
(inkl. „+ Neuen Ordner anlegen…“)"]
B2 --> B3["zeigt zugleich alle bestehenden
Anheftungen dieser Aufnahme, löschbar"]
end
```
Der 📌-Knopf in der Aufnahmenliste trägt die Zahl der Projekte, in denen die Aufnahme hängt, und ist
farbig markiert, sobald sie irgendwo angeheftet ist (`GET /api/pins/counts` — ein Request für die ganze
Liste, nicht einer pro Zeile). Die Zahl steckt zusätzlich in der Listen-Signatur `state.lastListSig`, damit
eine neue Anheftung die Liste neu zeichnet, ohne die 5-Sekunden-Regel („keine Änderung → DOM nicht
anfassen") aufzuweichen.
Ein Klick auf eine angeheftete Aufnahme springt in den Reiter „Aufnahmen" und öffnet sie dort im
Detailbereich (`jumpToRecording`, dieselbe Mechanik wie der Sprung aus einem Suchtreffer, nur ohne
Zeitmarke).
## API
| Route | Zweck |
|---|---|
| `GET /api/projects` | Ordnerliste mit Anzahl Reiter und Anheftungen |
| `POST /api/projects` | Ordner anlegen `{name}` |
| `GET /api/projects/:id` | Ordner mit Reitern und aufgelösten Anheftungen |
| `PATCH /api/projects/:id` | Ordner umbenennen |
| `DELETE /api/projects/:id` | Ordner löschen (Reiter + Anheftungen mit) |
| `POST /api/projects/:id/tabs` | Unterreiter anlegen `{name}` |
| `PATCH /api/projects/:id/tabs/:tid` | Unterreiter umbenennen; `:tid = 0` benennt die Ordner-Ebene um (leerer Name = zurück auf „Ordner") |
| `DELETE /api/projects/:id/tabs/:tid` | Unterreiter löschen |
| `POST /api/projects/:id/pins` | anheften `{recording_id, tab_id}` (`tab_id` fehlt/0 = Ordner-Ebene) |
| `DELETE /api/projects/:id/pins/:pinId` | Anheftung lösen |
| `GET /api/recordings/:id/pins` | wo hängt diese Aufnahme überall |
| `GET /api/pins/counts` | `{recording_id: anzahl}` für die ganze Liste |
## Dateien
`app/db.js` (Tabellen + CRUD), `app/server.js` (Routen), `app/web/src/main.js`
(`viewProjects`/`renderProjectList`/`renderProjectDetail`/`pinPickRecordingDialog`/`pinToProjectDialog`/
`openModal`), `app/web/src/style.css` (Ordnergitter, Reiterleiste, Dialog).
Board-Backups: `.bak-projects`.