feat(gui): Projektansicht mit Ordnern, Unterreitern und angehefteten Aufnahmen
Neuer Reiter neben "Aufnahmen": Projektordner bündeln Aufnahmen zweistufig (Ordner + Unterreiter), auf beiden Ebenen anheftbar. Anheften geht aus dem Projekt heraus (Dropdown mit Filter) oder über den 📌-Knopf in der Aufnahmenliste, der Ordner und Ebene abfragt, neue Ordner anlegt und bestehende Anheftungen zum Lösen zeigt. Klick auf eine angeheftete Aufnahme springt in die Aufnahmen-Ansicht. Ein Pin ist eine Referenz, kein Besitz: die Aufnahme bleibt unverändert in der Liste und darf in mehreren Ordnern hängen. project_pins.tab_id ist INT NOT NULL DEFAULT 0 (0 = Ordner-Ebene) statt NULL, weil MariaDB NULL von UNIQUE-Indizes ausschließt — sonst ließe sich dieselbe Aufnahme mehrfach auf dieselbe Ebene anheften. So macht INSERT IGNORE das Anheften idempotent. purgeRecording räumt project_pins mit ab; getProject blendet Aufnahmen im Papierkorb aus, ohne deren Pin zu löschen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
963ce427da
commit
772385713e
@@ -3,6 +3,34 @@
|
||||
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||||
Was, Warum, betroffene Dateien/Backups).
|
||||
|
||||
## 2026-08-02 — Neuer Reiter „Projektansicht": Ordner, Unterreiter, angeheftete Aufnahmen
|
||||
|
||||
**Was:** Aufnahmen lassen sich jetzt in Projektordnern bündeln. Ein Ordner („Project X") nimmt direkt
|
||||
Aufnahmen auf **und** kann Unterreiter („Projektrelevante Aufzeichnungen") enthalten, die ihrerseits
|
||||
Aufnahmen aufnehmen. Ein Klick auf eine angeheftete Aufnahme springt in den Reiter „Aufnahmen" und öffnet
|
||||
sie dort. Angeheftet wird auf zwei Wegen: aus dem Projekt heraus per Dropdown (mit Filterfeld) oder direkt
|
||||
in der Aufnahmenliste über den neuen 📌-Knopf, der Ordner **und** Ebene abfragt, neue Ordner anlegen kann
|
||||
und bestehende Anheftungen der Aufnahme zum Lösen anzeigt.
|
||||
|
||||
**Warum:** Die chronologische Aufnahmenliste beantwortet „wann war das", nicht „was gehört zu diesem
|
||||
Projekt". Reihenfolge nach Datum ist für die tägliche Arbeit an mehreren Projekten die falsche Sortierung.
|
||||
|
||||
**Wie:** Drei Tabellen (`projects`, `project_tabs`, `project_pins`), angelegt in `ensureSchema` wie alle
|
||||
anderen auch. Ein Pin ist eine reine Referenz — die Aufnahme selbst wird nie verschoben oder kopiert und
|
||||
darf in mehreren Ordnern hängen.
|
||||
|
||||
Zwei Details, die sonst später weh getan hätten:
|
||||
|
||||
- `project_pins.tab_id` ist **`INT NOT NULL DEFAULT 0`** (0 = Ordner-Ebene), nicht `NULL`. MariaDB
|
||||
schließt `NULL` von `UNIQUE`-Indizes aus; mit `NULL` ließe sich dieselbe Aufnahme mehrfach auf dieselbe
|
||||
Ebene anheften. Mit `0` greift `UNIQUE (project_id, tab_id, recording_id)`, und `INSERT IGNORE` macht
|
||||
das Anheften idempotent.
|
||||
- `db.purgeRecording` räumt `project_pins` mit ab, und `getProject` blendet Aufnahmen im Papierkorb aus,
|
||||
ohne ihren Pin zu löschen — nach dem Wiederherstellen ist die Aufnahme wieder im Ordner.
|
||||
|
||||
Dateien: `app/db.js`, `app/server.js`, `app/web/src/main.js`, `app/web/src/style.css`, neues Kapitel
|
||||
[10 – Projektansicht](10-projektansicht.md). Board-Backups: `.bak-projects`.
|
||||
|
||||
## 2026-07-31 (spät) — Zwei Fehler aus dem Suche-Feature behoben
|
||||
|
||||
Beide beim Nachsehen aufgefallen, warum die GUI bei einer 29:32-Aufnahme **2 h 34 min Analysezeit** anzeigte.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# 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<br/>„Project X“"] --> L0["Ebene „Ordner“<br/>tab_id = 0"]
|
||||
P --> T1["📑 Unterreiter<br/>„Projektrelevante Aufzeichnungen“"]
|
||||
P --> T2["📑 Unterreiter<br/>„Angebote“"]
|
||||
L0 --> R1["📌 #87 „1. Gespräch DFG – Projektstatusmeeting“"]
|
||||
T1 --> R2["📌 #86"]
|
||||
T1 --> R3["📌 #74"]
|
||||
R1 -.->|"Klick"| REC["Reiter „Aufnahmen“<br/>ö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.
|
||||
|
||||
## Datenbank
|
||||
|
||||
```sql
|
||||
CREATE TABLE projects (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(190) NOT NULL,
|
||||
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<br/>(Ordner oder Unterreiter)"]
|
||||
A2 --> A3["📌 Aufnahme anheften"] --> A4["Dropdown mit allen Aufnahmen<br/>+ Filterfeld"]
|
||||
end
|
||||
subgraph B["Weg 2 — aus der Aufnahmenliste"]
|
||||
B1["Aufnahmen → 📌 an der Zeile"] --> B2["Dialog: Ordner + Ebene wählen<br/>(inkl. „+ Neuen Ordner anlegen…“)"]
|
||||
B2 --> B3["zeigt zugleich alle bestehenden<br/>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 |
|
||||
| `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`.
|
||||
Reference in New Issue
Block a user