From 92889711a03caead3fe5d586c7851eb69c016a1c Mon Sep 17 00:00:00 2001 From: Team3 Date: Fri, 24 Jul 2026 22:39:23 +0200 Subject: [PATCH] init --- .gitignore | 4 + Makefile | 16 + README.md | 41 + backend/config.py | 4 + backend/features.py | 74 ++ backend/parser.py | 72 ++ backend/projekte.py | 62 ++ backend/pytest.ini | 3 + backend/routes.py | 87 ++ backend/server.py | 24 + backend/tests/conftest.py | 27 + backend/tests/test_features.py | 80 ++ backend/tests/test_parser.py | 92 ++ backend/tests/test_routes.py | 59 ++ daten/.gitkeep | 0 daten/Planer2/neues-feature-2.md | 11 + frontend/index.html | 21 + frontend/package-lock.json | 1109 ++++++++++++++++++++++ frontend/package.json | 22 + frontend/src/App.vue | 69 ++ frontend/src/api.js | 42 + frontend/src/assets/base.css | 117 +++ frontend/src/components/FeatureKarte.vue | 341 +++++++ frontend/src/components/FeatureListe.vue | 87 ++ frontend/src/components/SeitenLeiste.vue | 156 +++ frontend/src/main.js | 5 + frontend/vite.config.js | 15 + 27 files changed, 2640 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 backend/config.py create mode 100644 backend/features.py create mode 100644 backend/parser.py create mode 100644 backend/projekte.py create mode 100644 backend/pytest.ini create mode 100644 backend/routes.py create mode 100644 backend/server.py create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_features.py create mode 100644 backend/tests/test_parser.py create mode 100644 backend/tests/test_routes.py create mode 100644 daten/.gitkeep create mode 100644 daten/Planer2/neues-feature-2.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api.js create mode 100644 frontend/src/assets/base.css create mode 100644 frontend/src/components/FeatureKarte.vue create mode 100644 frontend/src/components/FeatureListe.vue create mode 100644 frontend/src/components/SeitenLeiste.vue create mode 100644 frontend/src/main.js create mode 100644 frontend/vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..098ee61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +.pytest_cache/ +node_modules/ +frontend/dist/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2f5c397 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: test dev build web + +test: + cd backend && python3 -m pytest -q + +# Entwicklung: Backend (8200) + Vite (5274) +dev: + cd backend && uvicorn server:app --reload --port 8200 & \ + cd frontend && npx vite --port 5274 + +# Produktion: Frontend bauen, dann nur den Server starten (serviert frontend/dist) +build: + cd frontend && npm install && npm run build + +web: + cd backend && uvicorn server:app --port 8200 diff --git a/README.md b/README.md new file mode 100644 index 0000000..250a31b --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Planer2 — Feature-Verwaltung + +Verwaltet Features meiner Projekte als Cards und definiert damit die „Wahrheit" je Projekt. +Rein manuelle Pflege: Sidebar links = Projekte, Hauptbereich = Feature-Cards. + +- `make dev` — Entwicklung: Backend auf 8200, Frontend auf http://localhost:5274 +- `make build && make web` — Produktion: http://localhost:8200 +- `make test` — Backend-Tests + +## Datenablage + +Markdown-Dateien unter `daten//.md` — Projekt = Ordner, die Datei ist +die Wahrheit. Der Editor bearbeitet immer den rohen Text; geparst wird nur für die Anzeige. + +Format (Marker allein auf einer Zeile; `` ist wiederholbar, jede +`` gehört zur davorstehenden Funktion; Trennzeilen aus `---` sind Optik): + +```markdown + +Login + +kern + +Kurzer Satz, was das Ziel ist. + +Anmeldung mit E-Mail + +Passwort nie loggen + +Abmeldung + +Notizen. +``` + +Text vor dem ersten Marker, Inhalt unbekannter Marker und Regeln ohne Funktion erscheinen +in der Anzeige unter „Sonstiges"; die Datei bleibt unangetastet. Bearbeitet wird live auf +der Karte (Klick ins Feld, Verlassen speichert, Escape verwirft); jedes Speichern setzt +die Datei neu zusammen. Titeländerungen benennen die Datei passend um. + +Hinweis: Sektionen werden im Frontend als Markdown gerendert (`marked`, ohne Sanitizing) — +die Daten sind lokal und selbst geschrieben. diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..fbd78b7 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,4 @@ +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +DATEN_DIR = PROJECT_ROOT / "daten" diff --git a/backend/features.py b/backend/features.py new file mode 100644 index 0000000..1b3e91a --- /dev/null +++ b/backend/features.py @@ -0,0 +1,74 @@ +"""Feature-Dateien: eine .md je Feature, Rohtext wird verbatim gespeichert.""" + +import re + +import parser +import projekte + +_UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss"}) + + +def _slug(titel: str) -> str: + s = titel.lower().translate(_UMLAUTE) + s = re.sub(r"[^a-z0-9]+", "-", s).strip("-") + return s or "feature" + + +def _eintrag(datei) -> dict: + roh = datei.read_text() + return {"id": datei.stem, "roh": roh, "sektionen": parser.zerlege(roh)} + + +def _datei(projekt: str, id: str): + datei = projekte.wurzel(projekt) / f"{id}.md" + if not datei.is_file(): + raise FileNotFoundError(f"Feature „{id}“ nicht gefunden") + return datei + + +def liste(projekt: str) -> list[dict]: + dateien = projekte.wurzel(projekt).glob("*.md") + eintraege = [_eintrag(d) for d in sorted(dateien)] + return sorted(eintraege, key=lambda e: e["sektionen"]["titel"].lower()) + + +def anlegen(projekt: str, roh: str) -> dict: + ordner = projekte.wurzel(projekt) + titel = parser.zerlege(roh)["titel"] + if not titel: + raise ValueError("Titel fehlt") + slug = _slug(titel) + datei = ordner / f"{slug}.md" + n = 2 + while datei.exists(): + datei = ordner / f"{slug}-{n}.md" + n += 1 + datei.write_text(roh) + return _eintrag(datei) + + +def _folge_titel(datei, titel: str): + """Benennt die Datei um, wenn der Slug nicht mehr zum Titel passt.""" + slug = _slug(titel) + if re.fullmatch(re.escape(slug) + r"(-\d+)?", datei.stem): + return datei # passt schon (inkl. Kollisions-Suffix) + ziel = datei.parent / f"{slug}.md" + n = 2 + while ziel.exists(): + ziel = datei.parent / f"{slug}-{n}.md" + n += 1 + datei.rename(ziel) + return ziel + + +def speichern(projekt: str, id: str, roh: str) -> dict: + datei = _datei(projekt, id) + datei.write_text(roh) + titel = parser.zerlege(roh)["titel"] + if titel: + datei = _folge_titel(datei, titel) + return _eintrag(datei) + + +def loeschen(projekt: str, id: str): + _datei(projekt, id).unlink() diff --git a/backend/parser.py b/backend/parser.py new file mode 100644 index 0000000..71f1b08 --- /dev/null +++ b/backend/parser.py @@ -0,0 +1,72 @@ +"""Zerlegt den rohen Feature-Text in Sektionen — nur für die Anzeige. + +Format: titel (eine Zeile), ziel, dann wiederholbare -Blöcke, +denen -Blöcke zugeordnet werden, zuletzt sonstiges. +Präambel, unbekannte Marker und Regeln ohne Funktion landen in `sonstiges`. +Trennzeilen aus Bindestrichen (---) sind reine Optik und werden ignoriert. +""" + +import re + +MARKER = re.compile(r"^\s*$") +TRENNER = re.compile(r"^-{3,}\s*$") + + +def zerlege(roh: str) -> dict: + # Erst in Blöcke (schluessel, text) in Dateireihenfolge schneiden + bloecke = [] + aktuell = None # None = Präambel vor dem ersten Marker + puffer = [] + + def ablegen(): + text = "\n".join(z for z in puffer if not TRENNER.match(z)).strip() + if text: + bloecke.append((aktuell, text)) + puffer.clear() + + for zeile in roh.splitlines(): + m = MARKER.match(zeile) + if m: + ablegen() + aktuell = m.group(1) + else: + puffer.append(zeile) + ablegen() + + titel = [] + ziel = [] + funktionen = [] + sonstiges = [] + art = [] + for schluessel, text in bloecke: + if schluessel == "titel": + titel.append(text) + elif schluessel == "art": + art.append(text) + elif schluessel == "ziel": + ziel.append(text) + elif schluessel == "funktion": + funktionen.append({"text": text, "regeln": []}) + elif schluessel == "regel" and funktionen: + funktionen[-1]["regeln"].append(text) + else: # Präambel, unbekannter Marker, regel ohne funktion + sonstiges.append(text) + + # Titel ist eine Zeile: erste nicht-leere + titel_zeilen = [z.strip() for t in titel for z in t.splitlines() if z.strip()] + + # Funktionen-Bereich mit Markern rekonstruiert — für das Editor-Feld + teile = [] + for f in funktionen: + teile.append(f"\n{f['text']}") + for r in f["regeln"]: + teile.append(f"\n{r}") + + return { + "titel": titel_zeilen[0] if titel_zeilen else "", + "art": "optional" if art and art[0].strip().lower() == "optional" else "kern", + "ziel": "\n\n".join(ziel), + "funktionen": funktionen, + "funktionen_roh": "\n".join(teile), + "sonstiges": "\n\n".join(sonstiges), + } diff --git a/backend/projekte.py b/backend/projekte.py new file mode 100644 index 0000000..d9bdd7a --- /dev/null +++ b/backend/projekte.py @@ -0,0 +1,62 @@ +"""Projekte = Ordner unter daten/. Kein Registry-File, das Listing ist die Liste.""" + +import shutil + +import config + + +def _pruefe_name(name: str): + if not name or not name.strip(): + raise ValueError("Projektname darf nicht leer sein") + if "/" in name or "\\" in name or name.startswith("."): + raise ValueError("Ungültiger Projektname") + + +def _ordner(name: str): + _pruefe_name(name) + return config.DATEN_DIR / name + + +def liste() -> list[dict]: + if not config.DATEN_DIR.exists(): + return [] + ordner = sorted( + (p for p in config.DATEN_DIR.iterdir() if p.is_dir()), + key=lambda p: p.name.lower(), + ) + return [ + {"name": p.name, "anzahl": len(list(p.glob("*.md")))} + for p in ordner + ] + + +def anlegen(name: str): + ziel = _ordner(name) + if ziel.exists(): + raise ValueError(f"Projekt „{name}“ existiert bereits") + ziel.mkdir(parents=True) + + +def umbenennen(alt: str, neu: str): + quelle = _ordner(alt) + ziel = _ordner(neu) + if not quelle.is_dir(): + raise FileNotFoundError(f"Projekt „{alt}“ nicht gefunden") + if ziel.exists(): + raise ValueError(f"Projekt „{neu}“ existiert bereits") + quelle.rename(ziel) + + +def loeschen(name: str): + ordner = _ordner(name) + if not ordner.is_dir(): + raise FileNotFoundError(f"Projekt „{name}“ nicht gefunden") + shutil.rmtree(ordner) + + +def wurzel(name: str): + """Projektordner für Feature-Zugriffe; FileNotFoundError bei unbekanntem Projekt.""" + ordner = _ordner(name) + if not ordner.is_dir(): + raise FileNotFoundError(f"Projekt „{name}“ nicht gefunden") + return ordner diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..78c5011 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +asyncio_mode = auto +testpaths = tests diff --git a/backend/routes.py b/backend/routes.py new file mode 100644 index 0000000..81758bd --- /dev/null +++ b/backend/routes.py @@ -0,0 +1,87 @@ +"""Alle API-Routen. Fachlogik wirft ValueError/FileNotFoundError, +hier wird auf 400/404/409 gemappt.""" + +from fastapi import APIRouter, HTTPException + +import features +import projekte + +router = APIRouter(prefix="/api") + + +@router.get("/health") +async def health(): + return {"ok": True} + + +@router.get("/projekte") +async def projekte_liste(): + return projekte.liste() + + +@router.post("/projekte") +async def projekt_anlegen(body: dict): + name = (body.get("name") or "").strip() + try: + projekte.anlegen(name) + except ValueError as e: + raise HTTPException(409 if "existiert" in str(e) else 400, str(e)) + return {"name": name} + + +@router.put("/projekte/{name}") +async def projekt_umbenennen(name: str, body: dict): + neu = (body.get("name") or "").strip() + try: + projekte.umbenennen(name, neu) + except FileNotFoundError as e: + raise HTTPException(404, str(e)) + except ValueError as e: + raise HTTPException(409 if "existiert" in str(e) else 400, str(e)) + return {"name": neu} + + +@router.delete("/projekte/{name}") +async def projekt_loeschen(name: str): + try: + projekte.loeschen(name) + except FileNotFoundError as e: + raise HTTPException(404, str(e)) + except ValueError as e: + raise HTTPException(400, str(e)) + return {"ok": True} + + +@router.get("/projekte/{name}/features") +async def features_liste(name: str): + try: + return features.liste(name) + except (FileNotFoundError, ValueError) as e: + raise HTTPException(404, str(e)) + + +@router.post("/projekte/{name}/features") +async def feature_anlegen(name: str, body: dict): + try: + return features.anlegen(name, body.get("roh") or "") + except FileNotFoundError as e: + raise HTTPException(404, str(e)) + except ValueError as e: + raise HTTPException(400, str(e)) + + +@router.put("/projekte/{name}/features/{id}") +async def feature_speichern(name: str, id: str, body: dict): + try: + return features.speichern(name, id, body.get("roh") or "") + except (FileNotFoundError, ValueError) as e: + raise HTTPException(404, str(e)) + + +@router.delete("/projekte/{name}/features/{id}") +async def feature_loeschen(name: str, id: str): + try: + features.loeschen(name, id) + except (FileNotFoundError, ValueError) as e: + raise HTTPException(404, str(e)) + return {"ok": True} diff --git a/backend/server.py b/backend/server.py new file mode 100644 index 0000000..dbff62c --- /dev/null +++ b/backend/server.py @@ -0,0 +1,24 @@ +"""Planer2-Server: API + gebautes Frontend. + +Start: uvicorn server:app --port 8200 +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from fastapi import FastAPI # noqa: E402 +from fastapi.staticfiles import StaticFiles # noqa: E402 + +import config # noqa: E402 +from routes import router # noqa: E402 + +app = FastAPI(title="Planer2") +app.include_router(router) + +config.DATEN_DIR.mkdir(exist_ok=True) + +FRONTEND_DIST = config.PROJECT_ROOT / "frontend" / "dist" +if FRONTEND_DIST.exists(): + app.mount("/", StaticFiles(directory=FRONTEND_DIST, html=True), name="frontend") diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..0c9fee3 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,27 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import httpx +import pytest + +import config + + +@pytest.fixture +def datenordner(tmp_path, monkeypatch): + """Leitet DATEN_DIR auf einen frischen Ordner um.""" + ordner = tmp_path / "daten" + ordner.mkdir() + monkeypatch.setattr(config, "DATEN_DIR", ordner) + return ordner + + +@pytest.fixture +async def client(datenordner): + from server import app + + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: + yield c diff --git a/backend/tests/test_features.py b/backend/tests/test_features.py new file mode 100644 index 0000000..444255f --- /dev/null +++ b/backend/tests/test_features.py @@ -0,0 +1,80 @@ +import pytest + +import features +import projekte + + +@pytest.fixture +def projekt(datenordner): + projekte.anlegen("Test") + return "Test" + + +def test_anlegen_erzeugt_slug(projekt, datenordner): + f = features.anlegen(projekt, "\nÜber Ätzend!\n") + assert f["id"] == "ueber-aetzend" + assert (datenordner / "Test" / "ueber-aetzend.md").is_file() + + +def test_slug_kollision(projekt): + a = features.anlegen(projekt, "\nGleich\n") + b = features.anlegen(projekt, "\nGleich\n") + assert a["id"] == "gleich" + assert b["id"] == "gleich-2" + + +def test_titel_pflicht(projekt): + with pytest.raises(ValueError): + features.anlegen(projekt, "nur Text ohne Titel-Marker") + + +def test_titelaenderung_benennt_datei_um(projekt, datenordner): + f = features.anlegen(projekt, "\nAlt\n") + g = features.speichern(projekt, f["id"], "\nNeu\n") + assert g["id"] == "neu" + assert not (datenordner / "Test" / "alt.md").exists() + assert (datenordner / "Test" / "neu.md").is_file() + + +def test_umbenennen_mit_kollision(projekt): + features.anlegen(projekt, "\nZiel\n") + f = features.anlegen(projekt, "\nAnders\n") + g = features.speichern(projekt, f["id"], "\nZiel\n") + assert g["id"] == "ziel-2" + + +def test_gleicher_titel_behaelt_kollisions_suffix(projekt): + features.anlegen(projekt, "\nGleich\n") + f = features.anlegen(projekt, "\nGleich\n") + g = features.speichern(projekt, f["id"], "\nGleich\n\nx\n") + assert g["id"] == "gleich-2" + + +def test_speichern_verbatim(projekt, datenordner): + f = features.anlegen(projekt, "\nX\n") + roh = "Vorspann\n\nX\n\n unangetastet \n" + features.speichern(projekt, f["id"], roh) + assert (datenordner / "Test" / "x.md").read_text() == roh + + +def test_liste_sortiert_nach_titel(projekt): + features.anlegen(projekt, "\nZebra\n") + features.anlegen(projekt, "\nanton\n") + titel = [f["sektionen"]["titel"] for f in features.liste(projekt)] + assert titel == ["anton", "Zebra"] + + +def test_loeschen(projekt): + f = features.anlegen(projekt, "\nWeg\n") + features.loeschen(projekt, f["id"]) + assert features.liste(projekt) == [] + + +def test_unbekannte_id(projekt): + with pytest.raises(FileNotFoundError): + features.speichern(projekt, "gibts-nicht", "x") + + +def test_unbekanntes_projekt(datenordner): + with pytest.raises(FileNotFoundError): + features.liste("Fremd") diff --git a/backend/tests/test_parser.py b/backend/tests/test_parser.py new file mode 100644 index 0000000..a8de7e5 --- /dev/null +++ b/backend/tests/test_parser.py @@ -0,0 +1,92 @@ +from parser import zerlege + +VOLL = """ +Login +---------------------------------------- + +Nutzer können sich anmelden. +---------------------------------------- + +Anmeldung mit E-Mail + +Passwort nie loggen + +Sperre nach 5 Fehlversuchen + +Abmeldung +---------------------------------------- + +Offen: 2FA. +""" + + +def test_alle_sektionen(): + s = zerlege(VOLL) + assert s["titel"] == "Login" + assert s["ziel"] == "Nutzer können sich anmelden." + assert s["funktionen"] == [ + {"text": "Anmeldung mit E-Mail", "regeln": ["Passwort nie loggen", "Sperre nach 5 Fehlversuchen"]}, + {"text": "Abmeldung", "regeln": []}, + ] + assert s["sonstiges"] == "Offen: 2FA." + + +def test_funktionen_roh_rekonstruiert_marker(): + s = zerlege(VOLL) + assert s["funktionen_roh"] == ( + "\nAnmeldung mit E-Mail\n" + "\nPasswort nie loggen\n" + "\nSperre nach 5 Fehlversuchen\n" + "\nAbmeldung" + ) + + +def test_trenner_zeilen_werden_ignoriert(): + s = zerlege("\nText\n-----\n") + assert s["ziel"] == "Text" + + +def test_praeambel_landet_in_sonstiges(): + s = zerlege("Vorspann ohne Marker\n\nX\n") + assert s["titel"] == "X" + assert s["sonstiges"] == "Vorspann ohne Marker" + + +def test_unbekannter_marker_landet_in_sonstiges(): + s = zerlege("\nX\n\nGeheim\n") + assert s["sonstiges"] == "Geheim" + + +def test_regel_ohne_funktion_landet_in_sonstiges(): + s = zerlege("\nverwaist\n") + assert s["funktionen"] == [] + assert s["sonstiges"] == "verwaist" + + +def test_doppelter_marker_wird_konkateniert(): + s = zerlege("\na\n\nX\n\nb\n") + assert s["ziel"] == "a\n\nb" + + +def test_leerer_text(): + s = zerlege("") + assert s == {"titel": "", "art": "kern", "ziel": "", "funktionen": [], "funktionen_roh": "", "sonstiges": ""} + + +def test_art_optional(): + assert zerlege("\nOptional\n")["art"] == "optional" + + +def test_art_default_und_unbekannter_wert(): + assert zerlege("\nX\n")["art"] == "kern" + assert zerlege("\nquatsch\n")["art"] == "kern" + + +def test_marker_mit_leerraum(): + s = zerlege("\nX\n") + assert s["titel"] == "X" + + +def test_titel_erste_nicht_leere_zeile(): + s = zerlege("\n\nErste\nZweite\n") + assert s["titel"] == "Erste" diff --git a/backend/tests/test_routes.py b/backend/tests/test_routes.py new file mode 100644 index 0000000..2779597 --- /dev/null +++ b/backend/tests/test_routes.py @@ -0,0 +1,59 @@ +ROH = "\nLogin\n\nAnmeldung\n\nPasswort nie loggen\n" + + +async def test_health(client): + res = await client.get("/api/health") + assert res.status_code == 200 + assert res.json() == {"ok": True} + + +async def test_projekt_crud(client): + assert (await client.get("/api/projekte")).json() == [] + + res = await client.post("/api/projekte", json={"name": "Alpha"}) + assert res.status_code == 200 + + # Duplikat → 409, ungültig → 400 + assert (await client.post("/api/projekte", json={"name": "Alpha"})).status_code == 409 + assert (await client.post("/api/projekte", json={"name": ""})).status_code == 400 + assert (await client.post("/api/projekte", json={"name": "../boese"})).status_code == 400 + + res = await client.put("/api/projekte/Alpha", json={"name": "Beta"}) + assert res.status_code == 200 + assert res.json() == {"name": "Beta"} + assert (await client.put("/api/projekte/Alpha", json={"name": "X"})).status_code == 404 + + assert [p["name"] for p in (await client.get("/api/projekte")).json()] == ["Beta"] + + assert (await client.delete("/api/projekte/Beta")).status_code == 200 + assert (await client.delete("/api/projekte/Beta")).status_code == 404 + + +async def test_feature_crud(client): + await client.post("/api/projekte", json={"name": "Alpha"}) + + res = await client.post("/api/projekte/Alpha/features", json={"roh": ROH}) + assert res.status_code == 200 + f = res.json() + assert f["id"] == "login" + assert f["roh"] == ROH + assert f["sektionen"]["titel"] == "Login" + + # Titel fehlt → 400 + res = await client.post("/api/projekte/Alpha/features", json={"roh": "kein Titel"}) + assert res.status_code == 400 + + res = await client.put("/api/projekte/Alpha/features/login", json={"roh": "mehr\n" + ROH}) + assert res.status_code == 200 + assert res.json()["sektionen"]["sonstiges"] == "mehr" + + liste = (await client.get("/api/projekte/Alpha/features")).json() + assert len(liste) == 1 + + assert (await client.delete("/api/projekte/Alpha/features/login")).status_code == 200 + assert (await client.delete("/api/projekte/Alpha/features/login")).status_code == 404 + + +async def test_unbekanntes_projekt(client): + assert (await client.get("/api/projekte/Fremd/features")).status_code == 404 + assert (await client.post("/api/projekte/Fremd/features", json={"roh": ROH})).status_code == 404 diff --git a/daten/.gitkeep b/daten/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/daten/Planer2/neues-feature-2.md b/daten/Planer2/neues-feature-2.md new file mode 100644 index 0000000..a3e8638 --- /dev/null +++ b/daten/Planer2/neues-feature-2.md @@ -0,0 +1,11 @@ + +Neues Feature +---------------------------------------- + +kern +---------------------------------------- + +adsda +---------------------------------------- + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..310ae82 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,21 @@ + + + + + + Planer2 + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..78d8037 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1109 @@ +{ + "name": "planer2-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "planer2-frontend", + "version": "0.0.0", + "dependencies": { + "marked": "^18.0.4", + "vue": "^3.5.32" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.6", + "vite": "^8.0.8" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "18.0.7", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.7.tgz", + "integrity": "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1565510 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "planer2-frontend", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "marked": "^18.0.4", + "vue": "^3.5.32" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.6", + "vite": "^8.0.8" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..a9c6a06 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..a936447 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,42 @@ +const BASE = '/api' + +// Ein Request-Weg für alle Aufrufe: Backend-Fehler werfen mit err.status, +// damit Aufrufer 404/409 gesondert behandeln können. +async function req(path, { method = 'GET', body } = {}) { + const opts = { method } + if (body !== undefined) { + opts.headers = { 'Content-Type': 'application/json' } + opts.body = JSON.stringify(body) + } + const res = await fetch(`${BASE}${path}`, opts) + if (!res.ok) { + let detail = `Fehler (HTTP ${res.status})` + try { + const data = await res.json() + if (data.detail) detail = typeof data.detail === 'string' ? data.detail : JSON.stringify(data.detail) + } catch { /* kein JSON-Body */ } + const err = new Error(detail) + err.status = res.status + throw err + } + const text = await res.text() + return text ? JSON.parse(text) : null +} + +const p = encodeURIComponent + +export const fetchProjekte = () => req('/projekte') +export const projektAnlegen = (name) => + req('/projekte', { method: 'POST', body: { name } }) +export const projektUmbenennen = (alt, neu) => + req(`/projekte/${p(alt)}`, { method: 'PUT', body: { name: neu } }) +export const projektLoeschen = (name) => + req(`/projekte/${p(name)}`, { method: 'DELETE' }) + +export const fetchFeatures = (projekt) => req(`/projekte/${p(projekt)}/features`) +export const featureAnlegen = (projekt, roh) => + req(`/projekte/${p(projekt)}/features`, { method: 'POST', body: { roh } }) +export const featureSpeichern = (projekt, id, roh) => + req(`/projekte/${p(projekt)}/features/${p(id)}`, { method: 'PUT', body: { roh } }) +export const featureLoeschen = (projekt, id) => + req(`/projekte/${p(projekt)}/features/${p(id)}`, { method: 'DELETE' }) diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css new file mode 100644 index 0000000..d467d33 --- /dev/null +++ b/frontend/src/assets/base.css @@ -0,0 +1,117 @@ +:root { + --bg: #f6f7f9; + --panel: #ffffff; + --text: #1a202c; + --dezent: #64748b; + --rand: #e2e8f0; + --akzent: #2563eb; + --akzent-text: #ffffff; + --gruen: #16a34a; + --gruen-bg: #dcfce7; + --gelb: #b45309; + --gelb-bg: #fef3c7; + --rot: #b91c1c; + --rot-bg: #fee2e2; + --code-bg: #f1f5f9; +} + +html.dark { + --bg: #0f1420; + --panel: #1a2130; + --text: #e2e8f0; + --dezent: #94a3b8; + --rand: #2d3748; + --akzent: #60a5fa; + --akzent-text: #0f1420; + --gruen: #86efac; + --gruen-bg: #14532d; + --gelb: #fcd34d; + --gelb-bg: #451a03; + --rot: #fca5a5; + --rot-bg: #450a0a; + --code-bg: #111827; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font: 15px/1.55 system-ui, sans-serif; +} + +button { + font: inherit; + cursor: pointer; + border: 1px solid var(--rand); + border-radius: 6px; + background: var(--panel); + color: var(--text); + padding: 0.35rem 0.8rem; +} + +button.primaer { + background: var(--akzent); + color: var(--akzent-text); + border-color: var(--akzent); +} + +button:disabled { opacity: 0.5; cursor: default; } + +input, textarea { + font: inherit; + border: 1px solid var(--rand); + border-radius: 6px; + background: var(--panel); + color: var(--text); + padding: 0.35rem 0.6rem; +} + +.karte { + background: var(--panel); + border: 1px solid var(--rand); + border-radius: 10px; + padding: 0.9rem 1.1rem; +} + +/* Rahmenlose Icon-Knöpfe (✎ ✕ …) */ +.icon-knopf { + border: none; + background: none; + padding: 0.1rem 0.3rem; + color: var(--dezent); + font-size: 0.95rem; +} +.icon-knopf:hover { color: var(--text); } +.icon-knopf.rot:hover { color: var(--rot); } + +.meldung { color: var(--rot); font-size: 0.85rem; } + +.badge { + font-size: 0.72rem; + font-weight: 600; + padding: 0.1rem 0.5rem; + border-radius: 99px; + text-transform: uppercase; + letter-spacing: 0.04em; + border: none; +} +.badge.blau { color: var(--akzent); background: var(--code-bg); } +.badge.gruen { color: var(--gruen); background: var(--gruen-bg); } + +pre { + background: var(--code-bg); + border-radius: 6px; + padding: 0.6rem 0.8rem; + font-size: 0.78rem; + line-height: 1.45; + overflow-x: auto; +} + +code { + background: var(--code-bg); + border-radius: 4px; + padding: 0.05rem 0.3rem; + font-size: 0.85em; +} diff --git a/frontend/src/components/FeatureKarte.vue b/frontend/src/components/FeatureKarte.vue new file mode 100644 index 0000000..093d88b --- /dev/null +++ b/frontend/src/components/FeatureKarte.vue @@ -0,0 +1,341 @@ + + +