377 lines
19 KiB
Markdown
377 lines
19 KiB
Markdown
# Ausbaustufe 7 Implementation Plan — Szenarien-Seite, Formular-UX, Kategorien (v0.8.0)
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
|
||
|
||
**Goal:** Szenarien auf eigene Seite `/szenarien`, verständliches Eintrags-Formular (Paar-Layout, Gesperrt-Kennzeichnung, dynamisches Wert-Label, abgesetzter Durchrechnen-Button), Kategorien-Verwaltung auf der Admin-Seite; Release v0.8.0.
|
||
|
||
**Architecture:** Kein Datenmodell-/Migrationsbedarf. Template-Umzug der Szenarien-Sektion aus `planning.html` in neues `szenarien.html` + Route in `gui.py`; CSS-Regeln in `style.css`; ein neuer Endpunkt `PATCH /api/categories/{id}`.
|
||
|
||
**Tech Stack:** FastAPI, Jinja2, htmx + json-form, CSS, pytest.
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-07-20-szenarien-seite-ux-design.md`
|
||
|
||
## Global Constraints
|
||
|
||
- GUI deutsch, TT.MM.JJJJ, `|eur`; API-Werte englisch; Geldbeträge `Decimal`.
|
||
- UX-Regel: gesperrte Felder sichtbar + `disabled` + optisch gekennzeichnet; Edit-Zeilen per `hidden`.
|
||
- Fable-Testagent-Gate je Task VOR dem Commit; Ledger-Eintrag je Task in `.superpowers/sdd/progress.md`.
|
||
- DATENSCHUTZ: keine echten Kontodaten in Commits/Tests/Doku.
|
||
- Testlauf: `cd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q` — Basis 181 passed, muss grün bleiben.
|
||
- Pfade relativ zu `/home/wlfb/bin/finance` (Ledger/Plan unter `/home/wlfb/bin`).
|
||
|
||
---
|
||
|
||
### Task 1: Szenarien-Seite `/szenarien`
|
||
|
||
**Files:**
|
||
- Create: `app/templates/szenarien.html`
|
||
- Modify: `app/templates/planning.html` (Szenarien-`<section>` Zeilen ~263-464 und szenario-spezifisches JS entfernen), `app/templates/base.html` (Nav + `toggleEdit`), `app/routers/gui.py` (neue Route, `planung_page` verschlanken), `app/templates/hilfe.html` (Ortsangaben)
|
||
- Modify: `tests/test_gui.py`
|
||
|
||
**Interfaces:**
|
||
- Consumes: bestehenden Szenario-Kontext (`_scenario_rows`, `modifier_kinds`), `gui_session`, alle bestehenden Templates/JS-Helfer.
|
||
- Produces: `GET /szenarien` (login-geschützt) mit Kontext `scenario_rows`, `loans`, `categories`, `category_names`, `recurring`, `recurring_names`, `modifier_kinds`; `toggleEdit` global in `base.html`. Task 2 baut auf `szenarien.html` auf.
|
||
|
||
- [x] **Step 1: Failing GUI-Tests** — in `tests/test_gui.py`:
|
||
|
||
```python
|
||
def test_szenarien_seite_und_nav(client, db):
|
||
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||
db.add(Scenario(name="Seiten-Test", description="d"))
|
||
db.commit()
|
||
r = client.get("/szenarien")
|
||
assert r.status_code == 200
|
||
assert "Seiten-Test" in r.text and "Neues Szenario anlegen" in r.text
|
||
assert 'href="/szenarien"' in r.text # Nav-Punkt
|
||
# Planung enthaelt keine Szenarien-Sektion mehr:
|
||
p = client.get("/planung").text
|
||
assert "Seiten-Test" not in p and "Neues Szenario anlegen" not in p
|
||
assert "Wiederkehrende Posten" in p and "Kredite" in p
|
||
|
||
|
||
def test_szenarien_seite_braucht_login(client):
|
||
r = client.get("/szenarien", follow_redirects=False)
|
||
assert r.status_code in (302, 303) and r.headers["location"] == "/login"
|
||
```
|
||
|
||
Run: `.venv/bin/python -m pytest tests/test_gui.py -q` → neue Tests FAIL (404)
|
||
|
||
- [x] **Step 2: Route** — `app/routers/gui.py`: in `planung_page` die Zeilen `scenarios = _list_scenarios(...)`, `"scenario_rows": ...` und `"modifier_kinds": ...` entfernen (die übrigen Kontexteinträge bleiben). Neue Route direkt darunter:
|
||
|
||
```python
|
||
@router.get("/szenarien", dependencies=[Depends(gui_session)])
|
||
def szenarien_page(request: Request, session: Session = Depends(get_session)):
|
||
categories = session.execute(select(Category)).scalars().all()
|
||
recurring = _list_recurring(session=session)
|
||
loans = _list_loans(session=session)
|
||
scenarios = _list_scenarios(session=session)
|
||
return templates.TemplateResponse(request, "szenarien.html", {
|
||
"categories": categories,
|
||
"category_names": {c.id: c.name for c in categories},
|
||
"recurring": recurring,
|
||
"recurring_names": {r.id: r.name for r in recurring},
|
||
"loans": loans,
|
||
"scenario_rows": _scenario_rows(session, scenarios, loans),
|
||
"modifier_kinds": ["percent", "absolute", "remove", "ende"],
|
||
})
|
||
```
|
||
|
||
- [x] **Step 3: Template-Umzug** — Neues `app/templates/szenarien.html`:
|
||
|
||
```html
|
||
{% extends "base.html" %}
|
||
{% block title %}Szenarien – Finanzberatung{% endblock %}
|
||
{% block content %}
|
||
<h1>Szenarien</h1>
|
||
|
||
<section class="planning-section">
|
||
<!-- HIER: die komplette bisherige Szenarien-<section>-INNEREI aus
|
||
planning.html unverändert einfügen (alles zwischen
|
||
'<h2>Szenarien</h2>' … einschließlich des Fieldsets
|
||
'Neues Szenario anlegen'), ohne das umschließende <section>-Tag
|
||
doppelt zu setzen. -->
|
||
</section>
|
||
|
||
<script>
|
||
// Szenario-spezifische Helfer (aus planning.html hierher umgezogen):
|
||
// onModTargetTypeChange, onEntryArtChange, toggleScenarioLoan
|
||
// unverändert einfügen.
|
||
</script>
|
||
{% endblock %}
|
||
```
|
||
|
||
Der `<h2>Szenarien</h2>` entfällt (die Seite hat die `<h1>`-Überschrift). In `planning.html`: die gesamte Szenarien-`<section>` löschen; aus dem Script-Block `onModTargetTypeChange`, `onEntryArtChange`, `toggleScenarioLoan` und `toggleEdit` entfernen — `toggleEdit` zieht in den zentralen Script-Block von `base.html` (dort nach der json-form-Extension einfügen, unverändert inkl. Kommentar); `loadLoanSchedule`/`fmtEur`/`formatIsoDate` und `loadedSchedules` bleiben in `planning.html`.
|
||
|
||
- [x] **Step 4: Nav + Hilfe** — `base.html`: `<a href="/szenarien">Szenarien</a>` zwischen Planung- und Admin-Link. `hilfe.html`: Formulierungen, die die Szenarien auf der Planung-Seite verorten, auf „Szenarien-Seite" anpassen (grep nach „Szenari" in `hilfe.html` und Ortsangaben prüfen).
|
||
|
||
- [x] **Step 5: Bestehende Tests umziehen** — `tests/test_gui.py`: die Szenario-GUI-Tests (`test_szenario_gui_ende_und_einmalzahlungen`, `test_szenario_eintraege_tabelle`, Szenario-Anteile von `test_planung_hat_bearbeiten_formulare`) von `client.get("/planung")` auf `client.get("/szenarien")` umstellen. In `test_planung_hat_bearbeiten_formulare` den Scenario-Teil (Scenario-Seed + `hx-patch="/api/scenarios/`-Assertion) in einen neuen Test `test_szenarien_hat_bearbeiten_formular` auslagern, der `/szenarien` lädt; der Planung-Test behält rec/pln/loan mit `>= 3` Bearbeiten-Buttons. `test_pages_require_login`/`test_pages_render_after_login`: Pfad `/szenarien` in die jeweilige Pfadliste aufnehmen.
|
||
|
||
- [x] **Step 6: Suite grün**
|
||
|
||
Run: `.venv/bin/python -m pytest -q` → PASS
|
||
|
||
- [x] **Step 7: Fable-Testagent-Abnahme** (Live-Approximation: beide Seiten rendern; keine Szenario-Reste auf /planung; `toggleEdit` genau EINMAL definiert [base.html], auf beiden Seiten funktionale Edit-Formulare; keine JS-Referenzen auf entfernte Funktionen in planning.html; Nav auf allen Seiten). Erst nach VERIFIED weiter.
|
||
|
||
- [x] **Step 8: Commit**
|
||
|
||
```bash
|
||
cd /home/wlfb/bin && git add finance/app/ finance/tests/test_gui.py
|
||
git commit -m "feat: eigene Szenarien-Seite /szenarien"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Formular-UX — Paar-Layout, Gesperrt-Kennzeichnung, klare Struktur
|
||
|
||
**Files:**
|
||
- Modify: `app/static/style.css`, `app/templates/szenarien.html`, `tests/test_gui.py`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `szenarien.html` aus Task 1 (Neuer-Eintrag-Formular mit Feldern kind/target_type/target_id/value/end_date/name/amount/due; `onEntryArtChange`).
|
||
- Produces: CSS-Klassen `.entry-form`, `.entry-art`, `.value-label`; dynamisches Wert-Label in `onEntryArtChange`; abgesetzter Durchrechnen-Block.
|
||
|
||
- [x] **Step 1: Failing GUI-Test**
|
||
|
||
```python
|
||
def test_neuer_eintrag_formular_struktur(client, db):
|
||
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||
db.add(Scenario(name="UX-Test", description=""))
|
||
db.commit()
|
||
r = client.get("/szenarien").text
|
||
assert 'class="entry-form"' in r
|
||
assert 'class="entry-art"' in r # Eintragsart-Zeile
|
||
assert 'class="value-label"' in r # dynamisches Wert-Label
|
||
assert 'Für diese Eintragsart nicht relevant' in r # Tooltip an Umschaltfeldern
|
||
assert '<hr' in r # Durchrechnen abgesetzt
|
||
```
|
||
|
||
Run: → FAIL
|
||
|
||
- [x] **Step 2: CSS** — `app/static/style.css` ergänzen:
|
||
|
||
```css
|
||
/* Ausbaustufe 7: Formular-Paare als Einheit, gesperrte Felder erkennbar. */
|
||
.entry-form label,
|
||
.planning-section form label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
margin: 0 1.25rem 0.6rem 0;
|
||
white-space: nowrap;
|
||
}
|
||
.entry-form .entry-art {
|
||
display: block;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
.entry-form .entry-art > label { font-weight: bold; }
|
||
.entry-form .entry-art .muted { margin-left: 0.5rem; }
|
||
input:disabled, select:disabled {
|
||
background: #e3e3e3;
|
||
color: #8a8a8a;
|
||
cursor: not-allowed;
|
||
}
|
||
label:has(input:disabled), label:has(select:disabled) {
|
||
opacity: 0.55;
|
||
}
|
||
.project-block {
|
||
margin-top: 1rem;
|
||
}
|
||
```
|
||
|
||
(Falls `style.css` bereits eine kollidierende `label`-Regel für `.planning-section` hat: die neue Regel dahinter einsortieren, Spezifität prüfen.)
|
||
|
||
- [x] **Step 3: Formular-Markup** — in `szenarien.html` das Neuer-Eintrag-Formular umbauen:
|
||
- `<form … class="entry-form" …>` (Attribute inkl. `data-modifiers-url`/`data-planned-url` unverändert).
|
||
- Statt `<strong>Neuer Eintrag:</strong>`: erste Zeile
|
||
|
||
```html
|
||
<div class="entry-art">
|
||
<label>Eintragsart
|
||
<select name="kind" data-type="str" onchange="onEntryArtChange(this)">
|
||
{% for k in modifier_kinds %}<option value="{{ k }}">{{ k|de_label }}</option>{% endfor %}
|
||
<option value="einmal">Einmalzahlung</option>
|
||
</select>
|
||
</label>
|
||
<span class="muted">— die zur Art passenden Felder werden aktiv, gesperrte Felder sind ausgegraut.</span>
|
||
</div>
|
||
```
|
||
|
||
- Wert-Label dynamisch: `<label><span class="value-label">Prozentsatz (%)</span> <input type="text" name="value" data-type="amount" value="0" title="Für diese Eintragsart nicht relevant"></label>` (Startzustand passend zur Default-Art `percent`).
|
||
- Die umschaltbaren Felder `end_date`, `name`, `amount`, `due` sowie die beiden Ziel-Selects bekommen `title="Für diese Eintragsart nicht relevant"`.
|
||
- Fieldset-Titel davor: `<h3>Einträge</h3>`-Tabelle bleibt; über dem Formular `<h4>Neuer Eintrag</h4>` (ersetzt das bisherige Inline-`<strong>`).
|
||
- Durchrechnen-Formular in einen abgesetzten Block:
|
||
|
||
```html
|
||
<hr>
|
||
<div class="project-block">
|
||
<form hx-post="/api/scenarios/{{ sc.id }}/project" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<button type="submit">Durchrechnen</button>
|
||
</form>
|
||
</div>
|
||
```
|
||
|
||
- [x] **Step 4: Dynamisches Wert-Label** — in `onEntryArtChange` (szenarien.html) nach der `setDisabled('value', …)`-Zeile ergänzen:
|
||
|
||
```javascript
|
||
var valueLabel = form.querySelector('.value-label');
|
||
if (valueLabel) {
|
||
valueLabel.textContent = art === 'percent' ? 'Prozentsatz (%)'
|
||
: art === 'absolute' ? 'Kürzung (€)' : 'Wert';
|
||
}
|
||
```
|
||
|
||
- [x] **Step 5: Suite grün** — `.venv/bin/python -m pytest -q` → PASS
|
||
|
||
- [x] **Step 6: Fable-Testagent-Abnahme** — Live-Approximation + Handtrace: gerenderte Struktur (entry-art zuerst, Paare als `inline-flex`-Einheiten via CSS-Regeln vorhanden, Tooltips an allen Umschaltfeldern, `<hr>`+project-block), Wert-Label-Wechsel für alle 5 Arten, Startzustand konsistent (percent: Wert aktiv mit Label „Prozentsatz (%)"). Erst nach VERIFIED weiter.
|
||
|
||
- [x] **Step 7: Commit**
|
||
|
||
```bash
|
||
cd /home/wlfb/bin && git add finance/app/static/style.css finance/app/templates/szenarien.html finance/tests/test_gui.py
|
||
git commit -m "feat: Eintrags-Formular mit Paar-Layout und Gesperrt-Kennzeichnung"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Kategorien-Verwaltung auf der Admin-Seite
|
||
|
||
**Files:**
|
||
- Modify: `app/routers/categories.py` (PATCH), `app/routers/admin.py` (Kontext), `app/templates/admin.html`
|
||
- Modify: `tests/test_crud_api.py`, `tests/test_admin.py`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `CategoryIn`/`CategoryOut`, `POST /api/categories` (vorhanden); `toggleEdit` (seit Task 1 global in base.html); json-form.
|
||
- Produces: `PATCH /api/categories/{category_id}` (404 „Kategorie nicht gefunden", 409 „Kategorie existiert bereits"); Admin-Abschnitt „Kategorien" mit IDs `cat-row-{id}`/`cat-edit-{id}`.
|
||
|
||
- [x] **Step 1: Failing API-Test** — `tests/test_crud_api.py`:
|
||
|
||
```python
|
||
def test_category_patch(client):
|
||
a = client.post("/api/categories", headers=H, json={"name": "Kat-A"}).json()
|
||
b = client.post("/api/categories", headers=H, json={"name": "Kat-B"}).json()
|
||
# Umbenennen
|
||
r = client.patch(f"/api/categories/{a['id']}", headers=H, json={"name": "Kat-A-neu"})
|
||
assert r.status_code == 200 and r.json()["name"] == "Kat-A-neu"
|
||
# Umbenennen auf den EIGENEN Namen ist erlaubt (kein 409)
|
||
r = client.patch(f"/api/categories/{a['id']}", headers=H, json={"name": "Kat-A-neu"})
|
||
assert r.status_code == 200
|
||
# Kollision mit anderer Kategorie -> 409
|
||
r = client.patch(f"/api/categories/{a['id']}", headers=H, json={"name": "Kat-B"})
|
||
assert r.status_code == 409
|
||
# unbekannte id -> 404
|
||
assert client.patch("/api/categories/99999", headers=H,
|
||
json={"name": "x"}).status_code == 404
|
||
```
|
||
|
||
Run: `.venv/bin/python -m pytest tests/test_crud_api.py -q` → FAIL (405)
|
||
|
||
- [x] **Step 2: PATCH-Endpunkt** — `app/routers/categories.py`, nach `create_category`:
|
||
|
||
```python
|
||
@router.patch("/categories/{category_id}", response_model=CategoryOut)
|
||
def patch_category(category_id: int, data: CategoryIn,
|
||
session: Session = Depends(get_session)):
|
||
cat = session.get(Category, category_id)
|
||
if cat is None:
|
||
raise HTTPException(404, "Kategorie nicht gefunden")
|
||
clash = session.execute(
|
||
select(Category).where(Category.name == data.name)).scalar()
|
||
if clash is not None and clash.id != category_id:
|
||
raise HTTPException(409, "Kategorie existiert bereits")
|
||
cat.name = data.name
|
||
session.commit()
|
||
session.refresh(cat)
|
||
return CategoryOut.model_validate(cat)
|
||
```
|
||
|
||
- [x] **Step 3: Failing GUI-Test** — `tests/test_admin.py` (Muster für Login dort übernehmen; falls die Datei GUI-Zugriffe anders aufbaut, an bestehende Fixtures anlehnen):
|
||
|
||
```python
|
||
def test_admin_zeigt_kategorien_verwaltung(client, db):
|
||
from app.models.tables import Category
|
||
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||
db.add(Category(name="Admin-Kat"))
|
||
db.commit()
|
||
r = client.get("/admin").text
|
||
assert "Kategorien" in r and "Admin-Kat" in r
|
||
assert "Neue Kategorie anlegen" in r
|
||
assert 'hx-patch="/api/categories/' in r
|
||
assert 'hx-post="/api/categories"' in r
|
||
```
|
||
|
||
Run: → FAIL
|
||
|
||
- [x] **Step 4: Admin-Kontext + Template** — `app/routers/admin.py`: in `admin_page` (und im Fehler-/Erfolgs-Re-Render von `admin_change_password`, damit der Abschnitt nie verschwindet — UX-Regel) `"categories": session.execute(select(Category)).scalars().all()` in den Template-Kontext aufnehmen (Imports `select`, `Category` ergänzen; `session`-Dependency, falls die Route noch keine hat). `admin.html`, neuer Abschnitt vor dem `<script>`:
|
||
|
||
```html
|
||
<section class="admin-section">
|
||
<h2>Kategorien</h2>
|
||
<p class="muted">Kategorien für Buchungen, Fixposten und Szenario-Modifikatoren.
|
||
Löschen ist bewusst nicht vorgesehen (Kategorien hängen an Buchungen und Regeln).</p>
|
||
<table>
|
||
<thead><tr><th>Name</th><th></th></tr></thead>
|
||
<tbody>
|
||
{% for c in categories %}
|
||
<tr id="cat-row-{{ c.id }}">
|
||
<td>{{ c.name }}</td>
|
||
<td><button type="button" onclick="toggleEdit('cat', {{ c.id }}, true)">Bearbeiten</button></td>
|
||
</tr>
|
||
<tr id="cat-edit-{{ c.id }}" hidden>
|
||
<td colspan="2">
|
||
<form hx-ext="json-form" hx-patch="/api/categories/{{ c.id }}" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<label>Name <input type="text" name="name" value="{{ c.name }}" required maxlength="100"></label>
|
||
<button type="submit">Speichern</button>
|
||
<button type="button" onclick="toggleEdit('cat', {{ c.id }}, false)">Abbrechen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="2">Noch keine Kategorien.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
<fieldset>
|
||
<legend>Neue Kategorie anlegen</legend>
|
||
<form hx-ext="json-form" hx-post="/api/categories" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<label>Name <input type="text" name="name" required maxlength="100"></label>
|
||
<button type="submit">Anlegen</button>
|
||
</form>
|
||
</fieldset>
|
||
</section>
|
||
```
|
||
|
||
- [x] **Step 5: Suite grün** — `.venv/bin/python -m pytest -q` → PASS
|
||
|
||
- [x] **Step 6: Fable-Testagent-Abnahme** (Live-Approximation: Anlegen → erscheint in Liste UND in den Kategorie-Dropdowns von Planung/Szenarien; Umbenennen-Roundtrip; 409 im Alert-Pfad; Admin-Fehler-Re-Render zeigt Abschnitt weiterhin). Erst nach VERIFIED weiter.
|
||
|
||
- [x] **Step 7: Commit**
|
||
|
||
```bash
|
||
cd /home/wlfb/bin && git add finance/app/routers/categories.py finance/app/routers/admin.py finance/app/templates/admin.html finance/tests/
|
||
git commit -m "feat: Kategorien-Verwaltung auf der Admin-Seite"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Release v0.8.0 — Redeploy, Live-Check, Ledger
|
||
|
||
**Files:** `finance/VERSION` (`0.7.0` → `0.8.0`), `.superpowers/sdd/progress.md`, Plan-Häkchen.
|
||
|
||
- [x] **Step 1: Suite final** — `.venv/bin/python -m pytest -q` → alle grün.
|
||
- [x] **Step 2: Version + Commit** — `echo "0.8.0" > finance/VERSION`, Commit `chore: Version 0.8.0`.
|
||
- [x] **Step 3: Redeploy** — `./create_pod_finance.sh` (keine Migration). Service aktiv, Readiness 200.
|
||
- [x] **Step 4: Live-Smoke** — `/api/version` == 0.8.0; Nav zeigt „Szenarien"; `/szenarien` zeigt Best Case mit Einträgen; `/planung` ohne Szenarien; `/admin` mit Kategorien-Abschnitt.
|
||
- [x] **Step 5: Fable-Testagent-Abnahme (Release-Gate)** — Live: Wegwerf-Kategorie „SMOKE-A7" anlegen → umbenennen → in Dropdowns sichtbar (bleibt stehen, Hinweis im Bericht — Kategorien sind nicht löschbar; Namenswahl „zz-Smoke" damit sie unten einsortiert? Nein: Kategorie „SMOKE-A7" wird nach dem Test per direktem psql-DELETE entfernt, NUR wenn keine FK-Referenzen existieren — vorher COUNT-Checks auf transactions/category_rules/recurring_items/scenario_modifiers); Best-Case-Einträge unverändert; Formular-Struktur auf /szenarien (entry-art, Tooltips, hr). Erst nach VERIFIED weiter.
|
||
- [x] **Step 6: Ledger + Plan-Häkchen + Push.**
|
||
|
||
---
|
||
|
||
## Self-Review (beim Planschreiben)
|
||
|
||
- **Spec-Abdeckung:** Seite → Task 1; UX/CSS/Label/Durchrechnen → Task 2; Kategorien (PATCH + Admin-GUI) → Task 3; Release → Task 4. Vollständig.
|
||
- **Platzhalter:** Der Template-Umzug in Task 1 Step 3 ist bewusst als präziser Move beschrieben (Quelle: bestehende Szenarien-Sektion) statt als 200-Zeilen-Duplikat — die Quelle ist eindeutig benannt und unverändert zu übernehmen.
|
||
- **Typ-Konsistenz:** `toggleEdit` global (Task 1) wird von Task 3 (`cat-`-Präfix) vorausgesetzt; `entry-form`/`entry-art`/`value-label`-Klassen konsistent zwischen CSS (Task 2 Step 2) und Markup (Step 3) und Test (Step 1).
|