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>
5.1 KiB
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.
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
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:
getProjectfiltert aufrecordings.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.purgeRecordingräumtproject_pinsmit ab (in derselben Schleife wiemarkers/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:
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.