Compare commits
10 Commits
main
...
feature/ph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4b1f1e30c | ||
|
|
b58edfc6eb | ||
|
|
5d9d517d18 | ||
|
|
d1831955c7 | ||
|
|
1abf4a647d | ||
|
|
c2eb905d62 | ||
|
|
6af5df32f6 | ||
|
|
80c73595d2 | ||
|
|
cd7a3f7414 | ||
|
|
3dceae930c |
62
.claude/scripts/bump.sh
Normal file
62
.claude/scripts/bump.sh
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# bump.sh – Version erhöhen, CHANGELOG aktualisieren, committen und pushen
|
||||||
|
#
|
||||||
|
# Verwendung:
|
||||||
|
# bash .claude/scripts/bump.sh patch "Beschreibung der Änderung"
|
||||||
|
# bash .claude/scripts/bump.sh minor "Beschreibung der Änderung"
|
||||||
|
# bash .claude/scripts/bump.sh major "Beschreibung der Änderung"
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BUMP_TYPE="${1:-patch}"
|
||||||
|
MESSAGE="${2:-}"
|
||||||
|
|
||||||
|
if [[ -z "$MESSAGE" ]]; then
|
||||||
|
echo "Fehler: Beschreibung fehlt."
|
||||||
|
echo "Verwendung: bash .claude/scripts/bump.sh [patch|minor|major] \"Beschreibung\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
VERSION_FILE="$ROOT/VERSION"
|
||||||
|
CHANGELOG_FILE="$ROOT/CHANGELOG.md"
|
||||||
|
|
||||||
|
# Aktuelle Version lesen
|
||||||
|
CURRENT=$(cat "$VERSION_FILE" | tr -d '[:space:]')
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
||||||
|
|
||||||
|
# Version erhöhen
|
||||||
|
case "$BUMP_TYPE" in
|
||||||
|
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
|
||||||
|
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
|
||||||
|
patch) PATCH=$((PATCH + 1)) ;;
|
||||||
|
*)
|
||||||
|
echo "Fehler: Ungültiger Typ '$BUMP_TYPE'. Erlaubt: patch, minor, major"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
echo "Bump: $CURRENT → $NEW_VERSION ($BUMP_TYPE)"
|
||||||
|
|
||||||
|
# VERSION aktualisieren
|
||||||
|
echo "$NEW_VERSION" > "$VERSION_FILE"
|
||||||
|
|
||||||
|
# CHANGELOG-Eintrag einfügen (nach der ersten ---)
|
||||||
|
ENTRY="## [$NEW_VERSION] - $DATE\n\n### Changed\n- $MESSAGE\n\n---\n"
|
||||||
|
# Füge nach der ersten '---' Zeile ein
|
||||||
|
awk -v entry="$ENTRY" '/^---$/ && !inserted { print; printf "%s", entry; inserted=1; next } { print }' \
|
||||||
|
"$CHANGELOG_FILE" > "$CHANGELOG_FILE.tmp" && mv "$CHANGELOG_FILE.tmp" "$CHANGELOG_FILE"
|
||||||
|
|
||||||
|
# Commit und Push
|
||||||
|
git -C "$ROOT" add "$VERSION_FILE" "$CHANGELOG_FILE"
|
||||||
|
git -C "$ROOT" commit -m "chore: bump version to $NEW_VERSION
|
||||||
|
|
||||||
|
$MESSAGE
|
||||||
|
|
||||||
|
Version: $NEW_VERSION"
|
||||||
|
git -C "$ROOT" push
|
||||||
|
|
||||||
|
echo "Fertig: Version $NEW_VERSION committed und gepusht."
|
||||||
41
.claude/scripts/new-feature.sh
Normal file
41
.claude/scripts/new-feature.sh
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# new-feature.sh – Feature- oder Fix-Branch aus develop erstellen und pushen
|
||||||
|
#
|
||||||
|
# Verwendung:
|
||||||
|
# bash .claude/scripts/new-feature.sh feature <name>
|
||||||
|
# bash .claude/scripts/new-feature.sh fix <name>
|
||||||
|
# bash .claude/scripts/new-feature.sh debug <name>
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TYPE="${1:-}"
|
||||||
|
NAME="${2:-}"
|
||||||
|
|
||||||
|
if [[ -z "$TYPE" || -z "$NAME" ]]; then
|
||||||
|
echo "Verwendung: bash .claude/scripts/new-feature.sh [feature|fix|debug] <name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$TYPE" in
|
||||||
|
feature|fix|debug) ;;
|
||||||
|
*)
|
||||||
|
echo "Fehler: Ungültiger Typ '$TYPE'. Erlaubt: feature, fix, debug"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
BRANCH="$TYPE/$NAME"
|
||||||
|
ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
# Sicherstellen dass develop aktuell ist
|
||||||
|
echo "Wechsle zu develop und aktualisiere..."
|
||||||
|
git -C "$ROOT" checkout develop
|
||||||
|
git -C "$ROOT" pull origin develop
|
||||||
|
|
||||||
|
# Branch erstellen und pushen
|
||||||
|
echo "Erstelle Branch: $BRANCH"
|
||||||
|
git -C "$ROOT" checkout -b "$BRANCH"
|
||||||
|
git -C "$ROOT" push -u origin "$BRANCH"
|
||||||
|
|
||||||
|
echo "Fertig: Branch '$BRANCH' erstellt und gepusht."
|
||||||
|
echo "Aktiver Branch: $(git -C "$ROOT" branch --show-current)"
|
||||||
73
.claude/session-context.md
Normal file
73
.claude/session-context.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Session-Kontext
|
||||||
|
|
||||||
|
> Claude liest diese Datei zu Beginn jeder Session.
|
||||||
|
> Claude aktualisiert sie am Ende jeder Session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Aktueller Stand
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| **Version** | 0.2.3 |
|
||||||
|
| **Aktiver Branch** | feature/phase-1 |
|
||||||
|
| **Basis-Branch** | develop |
|
||||||
|
| **Zuletzt geändert** | 2026-04-05 |
|
||||||
|
|
||||||
|
## Offene Arbeit – nächste Session startet hier
|
||||||
|
|
||||||
|
Phase 1 implementieren. Reihenfolge:
|
||||||
|
|
||||||
|
1. **Backend** – teilweise bereits vorhanden (siehe unten), fehlende Teile ergänzen
|
||||||
|
2. **Frontend** (Agent-Tool) – alle Dateien unter `frontend/`
|
||||||
|
3. **Docker** – `docker-compose.yml`, `docker-compose.dev.yml`, `.env.example`
|
||||||
|
4. Docs aktualisieren, VERSION auf 1.0.0-alpha bumpen, commit + push
|
||||||
|
|
||||||
|
### Backend – bereits vorhanden (committet, Qualität noch nicht geprüft):
|
||||||
|
```
|
||||||
|
backend/Dockerfile, alembic.ini, requirements.txt
|
||||||
|
app/core/: config.py, security.py, deps.py
|
||||||
|
app/db/: base.py, session.py
|
||||||
|
app/models/: user.py, tenant.py, plant.py, bed.py, planting.py
|
||||||
|
app/schemas/: auth.py, user.py, tenant.py
|
||||||
|
```
|
||||||
|
**Noch fehlend:** main.py, crud/, api/, seeds/, alembic/env.py + versions/001_initial.py, schemas/plant.py + bed.py + planting.py
|
||||||
|
|
||||||
|
**Zu Beginn:** vorhandene Dateien kurz prüfen (Konsistenz, async, UUID), dann fehlende ergänzen.
|
||||||
|
|
||||||
|
### Backend-Spec (Referenz):
|
||||||
|
- FastAPI + SQLAlchemy async + Alembic + PostgreSQL (asyncpg)
|
||||||
|
- Models: User, Tenant, UserTenant, PlantFamily, Plant, PlantCompatibility, Bed, BedPlanting
|
||||||
|
- Rollen: READ_ONLY / READ_WRITE / TENANT_ADMIN + Superadmin-Flag auf User
|
||||||
|
- JWT: Access 30min, Refresh 7 Tage
|
||||||
|
- Tenant-Kontext via Header `X-Tenant-ID`
|
||||||
|
- Seed-Daten: ~20 globale Pflanzen + Kompatibilitäten (fertig geplant, siehe Memory)
|
||||||
|
- Endpoints: /api/v1/auth/*, /api/v1/plants/*, /api/v1/plant-families, /api/v1/beds/*, /api/v1/beds/{id}/plantings, /api/v1/plantings/{id}
|
||||||
|
|
||||||
|
### Frontend-Spec:
|
||||||
|
- Vue 3 + Vite + PrimeVue + Pinia + Vue Router + Axios
|
||||||
|
- Views: Login, Beete (DataTable), Beet-Detail, Pflanzenbibliothek
|
||||||
|
- Sprache: Deutsch
|
||||||
|
- Static build → Nginx
|
||||||
|
|
||||||
|
## Git-Status
|
||||||
|
- `feature/grundstruktur` → in `develop` gemergt ✓
|
||||||
|
- `feature/phase-1` → erstellt und gepusht ✓
|
||||||
|
- Git-Auth: PAT im Credential Store hinterlegt ✓
|
||||||
|
|
||||||
|
## Wichtiger Hinweis für nächste Session
|
||||||
|
`.claude/settings.local.json` hat noch spezifische Permissions – bei git push ggf. Approval nötig.
|
||||||
|
Zu Beginn prüfen und ggf. auf breite Patterns updaten (Bash(git *), Bash(bash .claude/scripts/*)).
|
||||||
|
|
||||||
|
## Schnellreferenz
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Version bumpen
|
||||||
|
bash .claude/scripts/bump.sh patch "Was wurde geändert"
|
||||||
|
|
||||||
|
# Neuen Branch erstellen
|
||||||
|
bash .claude/scripts/new-feature.sh feature <name>
|
||||||
|
|
||||||
|
# Aktueller Branch
|
||||||
|
git branch --show-current
|
||||||
|
```
|
||||||
15
.claude/settings.local.json
Normal file
15
.claude/settings.local.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(curl -s -o /dev/null -w \"%{http_code}\" https://tea.jr-family.de/api/v1/repos/Admin/gartenmanager)",
|
||||||
|
"Bash(git -C c:/Projekte/Home/gartenmanager config --get credential.helper)",
|
||||||
|
"Bash(git credential:*)",
|
||||||
|
"Bash(python3 -m json.tool)",
|
||||||
|
"Bash(git -C c:/Projekte/Home/gartenmanager credential fill)",
|
||||||
|
"Bash(python3 -c \"import sys,json; r=json.load\\(sys.stdin\\); print\\('allow_squash_merge:', r.get\\('allow_squash_merge'\\), '| default_merge_style:', r.get\\('default_merge_style'\\)\\)\")",
|
||||||
|
"Bash(bash .claude/scripts/bump.sh patch \"Add autonomous branch-switching rule to workflow docs\")",
|
||||||
|
"Bash(bash .claude/scripts/bump.sh patch \"Update project plan: finalize phases, techstack and architecture decisions\")",
|
||||||
|
"Bash(git -C c:/Projekte/Home/gartenmanager credential approve)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
35
.gitattributes
vendored
Normal file
35
.gitattributes
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Automatische Zeilenendenormalisierung
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Explizit Text-Dateien auf LF normieren
|
||||||
|
*.md text eol=lf
|
||||||
|
*.txt text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.toml text eol=lf
|
||||||
|
*.xml text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
*.scss text eol=lf
|
||||||
|
*.js text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
*.py text eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.env text eol=lf
|
||||||
|
|
||||||
|
# Binärdateien – kein Zeilenenden-Handling
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.svg binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.pdf binary
|
||||||
|
*.zip binary
|
||||||
28
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
28
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
## Beschreibung
|
||||||
|
|
||||||
|
<!-- Was wurde geändert und warum? -->
|
||||||
|
|
||||||
|
## Typ der Änderung
|
||||||
|
|
||||||
|
- [ ] `feat` – neues Feature
|
||||||
|
- [ ] `fix` – Bugfix
|
||||||
|
- [ ] `refactor` – Code-Umbau ohne Verhaltensänderung
|
||||||
|
- [ ] `chore` – Wartung, Build, Konfiguration
|
||||||
|
- [ ] `docs` – nur Dokumentation
|
||||||
|
|
||||||
|
## Checkliste
|
||||||
|
|
||||||
|
- [ ] Alle Tests erfolgreich (`<test-command>`)
|
||||||
|
- [ ] Versionsnummer erhöht (`VERSION` + `CHANGELOG.md`)
|
||||||
|
- [ ] `docs/project-structure.md` aktualisiert (falls Funktionen/Module geändert)
|
||||||
|
- [ ] `README.md` aktualisiert (falls nötig)
|
||||||
|
- [ ] Branch ist aktuell mit `develop` (rebase/merge)
|
||||||
|
- [ ] Kein direkter Push nach `main` – diese PR geht nach `develop`
|
||||||
|
|
||||||
|
## Getestete Szenarien
|
||||||
|
|
||||||
|
<!-- Was wurde manuell oder automatisch getestet? -->
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
<!-- z.B. 0.2.1 -->
|
||||||
61
CHANGELOG.md
Normal file
61
CHANGELOG.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Alle wesentlichen Änderungen am Projekt werden hier dokumentiert.
|
||||||
|
Format: `[MAJOR.MINOR.PATCH] - YYYY-MM-DD`
|
||||||
|
|
||||||
|
---
|
||||||
|
## [0.2.3] - 2026-04-05
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Update project plan: finalize phases, techstack and architecture decisions
|
||||||
|
|
||||||
|
---
|
||||||
|
## [0.2.2] - 2026-04-05
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Add autonomous branch-switching rule to workflow docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.2.1] - 2026-04-05
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `CLAUDE.md` auf reine Dispatch-Tabelle reduziert (keine Regelwiederholungen)
|
||||||
|
- `docs/project-structure.md` als dichte Modulreferenz-Vorlage strukturiert
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `.claude/scripts/bump.sh` – Version + CHANGELOG + commit + push in einem Befehl
|
||||||
|
- `.claude/scripts/new-feature.sh` – Feature/Fix/Debug-Branch aus develop erstellen
|
||||||
|
- `.claude/session-context.md` – Sessionstart-Kontext (Version, Branch, offene Arbeit)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-04-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `.gitattributes` – automatische LF-Normalisierung, keine CRLF-Warnungen mehr
|
||||||
|
- `README.md` – Projektbeschreibung, Features-Übersicht, Links zur Dokumentation
|
||||||
|
- `.gitea/PULL_REQUEST_TEMPLATE.md` – Checkliste für PRs (Tests, Version, Docs)
|
||||||
|
- Branch Protection für `main` und `develop` (serverseitig konfiguriert)
|
||||||
|
- Squash-Merge als Standard-Merge-Strategie (serverseitig konfiguriert)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.1.1] - 2026-04-05
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Entwicklungsstandards um Branching-Regeln, Versionierungsschema und Workflow-Regeln erweitert
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- CHANGELOG.md eingeführt
|
||||||
|
- VERSION-Datei eingeführt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-04-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- CLAUDE.md – Guidance für Claude Code
|
||||||
|
- docs/development-standards.md – allgemeine Entwicklungsstandards
|
||||||
|
- docs/project-structure.md – Projektstruktur und Domänenmodell
|
||||||
|
- docs/branching-strategy.md – Branching-Strategie
|
||||||
24
CLAUDE.md
24
CLAUDE.md
@@ -4,14 +4,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Projekt
|
## Projekt
|
||||||
|
|
||||||
**Gartenmanager** – eine Anwendung zur Verwaltung und Planung von Gartenaktivitäten (Pflanzen, Aussaatkalender, Aufgaben, Bewässerung etc.).
|
**Gartenmanager** – Anwendung zur Verwaltung von Gartenaktivitäten (Pflanzen, Beete, Aussaatkalender, Aufgaben, Bewässerung).
|
||||||
|
|
||||||
## Weiterführende Dokumente
|
## Dokumente – wo was steht
|
||||||
|
|
||||||
| Dokument | Inhalt |
|
| Dokument | Inhalt |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [docs/development-standards.md](docs/development-standards.md) | Allgemeine Entwicklungsstandards (Coding Style, Git, Testing) |
|
| [docs/development-standards.md](docs/development-standards.md) | **Alle Regeln:** Branching, Versionierung, Workflow, Coding, Testing |
|
||||||
| [docs/project-structure.md](docs/project-structure.md) | Projektstruktur und Architekturübersicht |
|
| [docs/project-structure.md](docs/project-structure.md) | **Alle Module & Funktionen** – hier zuerst lesen, bevor Quellcode geöffnet wird |
|
||||||
|
| [docs/branching-strategy.md](docs/branching-strategy.md) | Branch-Diagramm |
|
||||||
|
| [CHANGELOG.md](CHANGELOG.md) | Versionshistorie |
|
||||||
|
| [VERSION](VERSION) | Aktuelle Versionsnummer |
|
||||||
|
| [.claude/session-context.md](.claude/session-context.md) | **Sessionstart hier lesen:** aktiver Branch, Version, offene Arbeit |
|
||||||
|
| [.claude/scripts/](.claude/scripts/) | Automatisierungsscripts (bump, new-feature) |
|
||||||
|
|
||||||
## Techstack
|
## Techstack
|
||||||
|
|
||||||
@@ -39,9 +44,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
# Build für Produktion
|
# Build für Produktion
|
||||||
# <build-command>
|
# <build-command>
|
||||||
|
|
||||||
|
# Version bumpen + commit + push (patch/minor/major)
|
||||||
|
bash .claude/scripts/bump.sh patch "Beschreibung der Änderung"
|
||||||
|
|
||||||
|
# Neuen Feature-Branch erstellen
|
||||||
|
bash .claude/scripts/new-feature.sh <name>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Wichtige Konventionen
|
|
||||||
|
|
||||||
- Sprache: Deutsch für Domänenkonzepte (Pflanzen, Beet, Aussaat …), Englisch für Code-Bezeichner und Commit-Messages
|
|
||||||
- Alle Dateipfade in den `docs/`-Dokumenten aktuell halten, wenn sich die Struktur ändert
|
|
||||||
|
|||||||
32
README.md
Normal file
32
README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Gartenmanager
|
||||||
|
|
||||||
|
Persönliche Webanwendung zur Verwaltung und Planung von Gartenaktivitäten.
|
||||||
|
|
||||||
|
## Features (geplant)
|
||||||
|
|
||||||
|
- **Pflanzenverwaltung** – Pflanzenarten mit Eigenschaften (Aussaatzeit, Wasserbedarf, Abstand …)
|
||||||
|
- **Beetplanung** – Beete anlegen und Pflanzen zuordnen
|
||||||
|
- **Aussaatkalender** – Jahresbasierter Aussaat- und Pflanzungsplan
|
||||||
|
- **Aufgabenverwaltung** – Gartenaufgaben mit Fälligkeitsdatum
|
||||||
|
- **Bewässerungsplan** – Intervalle je Beet oder Pflanze
|
||||||
|
|
||||||
|
## Techstack
|
||||||
|
|
||||||
|
> Noch festzulegen.
|
||||||
|
|
||||||
|
## Entwicklung
|
||||||
|
|
||||||
|
> Build- und Run-Befehle werden hier ergänzt, sobald der Techstack feststeht.
|
||||||
|
|
||||||
|
Weiterführende Dokumentation:
|
||||||
|
|
||||||
|
- [Entwicklungsstandards](docs/development-standards.md)
|
||||||
|
- [Projektstruktur](docs/project-structure.md)
|
||||||
|
- [Branching-Strategie](docs/branching-strategy.md)
|
||||||
|
- [Changelog](CHANGELOG.md)
|
||||||
|
|
||||||
|
## Versionierung
|
||||||
|
|
||||||
|
Aktuelle Version: siehe [VERSION](VERSION)
|
||||||
|
|
||||||
|
Schema: `MAJOR.MINOR.PATCH` – Details in den [Entwicklungsstandards](docs/development-standards.md#versionierung).
|
||||||
6
backend/Dockerfile
Normal file
6
backend/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
41
backend/alembic.ini
Normal file
41
backend/alembic.ini
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[alembic]
|
||||||
|
script_location = alembic
|
||||||
|
prepend_sys_path = .
|
||||||
|
version_path_separator = os
|
||||||
|
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
21
backend/app/core/config.py
Normal file
21
backend/app/core/config.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
case_sensitive=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/gartenmanager"
|
||||||
|
SECRET_KEY: str = "change-me-in-production-use-a-long-random-string"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||||
|
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
||||||
|
|
||||||
|
CORS_ORIGINS: list[str] = ["*"]
|
||||||
|
APP_TITLE: str = "Gartenmanager API"
|
||||||
|
APP_VERSION: str = "1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
161
backend/app/core/deps.py
Normal file
161
backend/app/core/deps.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import Depends, Header, HTTPException, Security, status
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
|
from jose import JWTError
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.security import TOKEN_TYPE_ACCESS, decode_token
|
||||||
|
from app.db.session import get_session
|
||||||
|
from app.models.user import User, UserTenant, TenantRole
|
||||||
|
|
||||||
|
bearer_scheme = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(
|
||||||
|
credentials: Annotated[HTTPAuthorizationCredentials | None, Security(bearer_scheme)],
|
||||||
|
db: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
) -> User:
|
||||||
|
if credentials is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Nicht authentifiziert. Bitte melden Sie sich an.",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
payload = decode_token(credentials.credentials)
|
||||||
|
if payload.get("type") != TOKEN_TYPE_ACCESS:
|
||||||
|
raise JWTError("Falscher Token-Typ")
|
||||||
|
user_id: str | None = payload.get("sub")
|
||||||
|
if user_id is None:
|
||||||
|
raise JWTError("Kein Subject im Token")
|
||||||
|
except JWTError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Ungültiger oder abgelaufener Token.",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
from app.crud.user import crud_user
|
||||||
|
|
||||||
|
user = await crud_user.get(db, id=UUID(user_id))
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Benutzer nicht gefunden.",
|
||||||
|
)
|
||||||
|
if not user.is_active:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Benutzerkonto ist deaktiviert.",
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
CurrentUser = Annotated[User, Depends(get_current_user)]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_tenant_context(
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
x_tenant_id: Annotated[str | None, Header(alias="X-Tenant-ID")] = None,
|
||||||
|
) -> tuple[User, UUID]:
|
||||||
|
"""Returns (current_user, tenant_id). Validates tenant membership."""
|
||||||
|
if x_tenant_id is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Header 'X-Tenant-ID' fehlt.",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
tenant_id = UUID(x_tenant_id)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Ungültige Tenant-ID.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_user.is_superadmin:
|
||||||
|
# Superadmin: verify tenant exists
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.models.tenant import Tenant
|
||||||
|
|
||||||
|
result = await db.execute(select(Tenant).where(Tenant.id == tenant_id))
|
||||||
|
tenant = result.scalar_one_or_none()
|
||||||
|
if tenant is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Tenant nicht gefunden.",
|
||||||
|
)
|
||||||
|
return current_user, tenant_id
|
||||||
|
|
||||||
|
# Regular user: check membership
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(UserTenant).where(
|
||||||
|
UserTenant.user_id == current_user.id,
|
||||||
|
UserTenant.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
membership = result.scalar_one_or_none()
|
||||||
|
if membership is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Kein Zugriff auf diesen Tenant.",
|
||||||
|
)
|
||||||
|
return current_user, tenant_id
|
||||||
|
|
||||||
|
|
||||||
|
async def get_tenant_role(
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
x_tenant_id: Annotated[str | None, Header(alias="X-Tenant-ID")] = None,
|
||||||
|
) -> tuple[User, UUID, TenantRole | None]:
|
||||||
|
"""Returns (current_user, tenant_id, role). Role is None for superadmins."""
|
||||||
|
user, tenant_id = await get_tenant_context(current_user, db, x_tenant_id)
|
||||||
|
|
||||||
|
if user.is_superadmin:
|
||||||
|
return user, tenant_id, None
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(UserTenant).where(
|
||||||
|
UserTenant.user_id == user.id,
|
||||||
|
UserTenant.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
membership = result.scalar_one_or_none()
|
||||||
|
return user, tenant_id, membership.role if membership else None
|
||||||
|
|
||||||
|
|
||||||
|
def require_min_role(min_role: TenantRole):
|
||||||
|
"""Dependency factory: ensures the user has at least the given role."""
|
||||||
|
|
||||||
|
async def _check(
|
||||||
|
ctx: Annotated[
|
||||||
|
tuple[User, UUID, TenantRole | None], Depends(get_tenant_role)
|
||||||
|
],
|
||||||
|
) -> tuple[User, UUID, TenantRole | None]:
|
||||||
|
user, tenant_id, role = ctx
|
||||||
|
if user.is_superadmin:
|
||||||
|
return ctx
|
||||||
|
if role is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Kein Zugriff auf diesen Tenant.",
|
||||||
|
)
|
||||||
|
role_order = {
|
||||||
|
TenantRole.READ_ONLY: 0,
|
||||||
|
TenantRole.READ_WRITE: 1,
|
||||||
|
TenantRole.TENANT_ADMIN: 2,
|
||||||
|
}
|
||||||
|
if role_order[role] < role_order[min_role]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Unzureichende Berechtigungen.",
|
||||||
|
)
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
return _check
|
||||||
53
backend/app/core/security.py
Normal file
53
backend/app/core/security.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
TOKEN_TYPE_ACCESS = "access"
|
||||||
|
TOKEN_TYPE_REFRESH = "refresh"
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_password_hash(password: str) -> str:
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(subject: str | Any, extra_claims: dict | None = None) -> str:
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(
|
||||||
|
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
|
)
|
||||||
|
to_encode: dict[str, Any] = {
|
||||||
|
"sub": str(subject),
|
||||||
|
"exp": expire,
|
||||||
|
"type": TOKEN_TYPE_ACCESS,
|
||||||
|
}
|
||||||
|
if extra_claims:
|
||||||
|
to_encode.update(extra_claims)
|
||||||
|
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
|
||||||
|
|
||||||
|
def create_refresh_token(subject: str | Any) -> str:
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(
|
||||||
|
days=settings.REFRESH_TOKEN_EXPIRE_DAYS
|
||||||
|
)
|
||||||
|
to_encode: dict[str, Any] = {
|
||||||
|
"sub": str(subject),
|
||||||
|
"exp": expire,
|
||||||
|
"type": TOKEN_TYPE_REFRESH,
|
||||||
|
}
|
||||||
|
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_token(token: str) -> dict[str, Any]:
|
||||||
|
"""Decode and validate a JWT. Raises JWTError on failure."""
|
||||||
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
return payload
|
||||||
0
backend/app/db/__init__.py
Normal file
0
backend/app/db/__init__.py
Normal file
13
backend/app/db/base.py
Normal file
13
backend/app/db/base.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Import all models here so Alembic can detect them
|
||||||
|
from app.models.user import User, UserTenant # noqa: F401, E402
|
||||||
|
from app.models.tenant import Tenant # noqa: F401, E402
|
||||||
|
from app.models.plant import PlantFamily, Plant, PlantCompatibility # noqa: F401, E402
|
||||||
|
from app.models.bed import Bed # noqa: F401, E402
|
||||||
|
from app.models.planting import BedPlanting # noqa: F401, E402
|
||||||
33
backend/app/db/session.py
Normal file
33
backend/app/db/session.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
engine = create_async_engine(
|
||||||
|
settings.DATABASE_URL,
|
||||||
|
echo=False,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
pool_size=10,
|
||||||
|
max_overflow=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
AsyncSessionLocal = async_sessionmaker(
|
||||||
|
bind=engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False,
|
||||||
|
autocommit=False,
|
||||||
|
autoflush=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
await session.commit()
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
0
backend/app/models/__init__.py
Normal file
0
backend/app/models/__init__.py
Normal file
66
backend/app/models/bed.py
Normal file
66
backend/app/models/bed.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Numeric, String, Text, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class LocationType(str, enum.Enum):
|
||||||
|
SONNIG = "sonnig"
|
||||||
|
HALBSCHATTEN = "halbschatten"
|
||||||
|
SCHATTEN = "schatten"
|
||||||
|
|
||||||
|
|
||||||
|
class SoilType(str, enum.Enum):
|
||||||
|
NORMAL = "normal"
|
||||||
|
SANDIG = "sandig"
|
||||||
|
LEHMIG = "lehmig"
|
||||||
|
HUMUSREICH = "humusreich"
|
||||||
|
|
||||||
|
|
||||||
|
class Bed(Base):
|
||||||
|
__tablename__ = "beds"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
primary_key=True, default=uuid.uuid4, index=True
|
||||||
|
)
|
||||||
|
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("tenants.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
width_m: Mapped[float] = mapped_column(Numeric(5, 2), nullable=False)
|
||||||
|
length_m: Mapped[float] = mapped_column(Numeric(5, 2), nullable=False)
|
||||||
|
location: Mapped[LocationType] = mapped_column(
|
||||||
|
Enum(LocationType, name="location_type"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
soil_type: Mapped[SoilType] = mapped_column(
|
||||||
|
Enum(SoilType, name="soil_type"),
|
||||||
|
nullable=False,
|
||||||
|
default=SoilType.NORMAL,
|
||||||
|
)
|
||||||
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
onupdate=lambda: datetime.now(timezone.utc),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="beds") # noqa: F821
|
||||||
|
plantings: Mapped[list["BedPlanting"]] = relationship( # noqa: F821
|
||||||
|
"BedPlanting", back_populates="bed", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
133
backend/app/models/plant.py
Normal file
133
backend/app/models/plant.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
|
Enum,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
UniqueConstraint,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class NutrientDemand(str, enum.Enum):
|
||||||
|
SCHWACH = "schwach"
|
||||||
|
MITTEL = "mittel"
|
||||||
|
STARK = "stark"
|
||||||
|
|
||||||
|
|
||||||
|
class WaterDemand(str, enum.Enum):
|
||||||
|
WENIG = "wenig"
|
||||||
|
MITTEL = "mittel"
|
||||||
|
VIEL = "viel"
|
||||||
|
|
||||||
|
|
||||||
|
class CompatibilityRating(str, enum.Enum):
|
||||||
|
GUT = "gut"
|
||||||
|
NEUTRAL = "neutral"
|
||||||
|
SCHLECHT = "schlecht"
|
||||||
|
|
||||||
|
|
||||||
|
class PlantFamily(Base):
|
||||||
|
__tablename__ = "plant_families"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
primary_key=True, default=uuid.uuid4, index=True
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
||||||
|
latin_name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
plants: Mapped[list["Plant"]] = relationship("Plant", back_populates="family")
|
||||||
|
|
||||||
|
|
||||||
|
class Plant(Base):
|
||||||
|
__tablename__ = "plants"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
primary_key=True, default=uuid.uuid4, index=True
|
||||||
|
)
|
||||||
|
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
ForeignKey("tenants.id", ondelete="CASCADE"),
|
||||||
|
nullable=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
family_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("plant_families.id", ondelete="RESTRICT"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
latin_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
|
nutrient_demand: Mapped[NutrientDemand] = mapped_column(
|
||||||
|
Enum(NutrientDemand, name="nutrient_demand"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
water_demand: Mapped[WaterDemand] = mapped_column(
|
||||||
|
Enum(WaterDemand, name="water_demand"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
spacing_cm: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
sowing_start_month: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
sowing_end_month: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
rest_years: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
tenant: Mapped["Tenant | None"] = relationship( # noqa: F821
|
||||||
|
"Tenant", back_populates="plants"
|
||||||
|
)
|
||||||
|
family: Mapped["PlantFamily"] = relationship("PlantFamily", back_populates="plants")
|
||||||
|
plantings: Mapped[list["BedPlanting"]] = relationship( # noqa: F821
|
||||||
|
"BedPlanting", back_populates="plant"
|
||||||
|
)
|
||||||
|
compatibility_a: Mapped[list["PlantCompatibility"]] = relationship(
|
||||||
|
"PlantCompatibility",
|
||||||
|
foreign_keys="PlantCompatibility.plant_id_a",
|
||||||
|
back_populates="plant_a",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
compatibility_b: Mapped[list["PlantCompatibility"]] = relationship(
|
||||||
|
"PlantCompatibility",
|
||||||
|
foreign_keys="PlantCompatibility.plant_id_b",
|
||||||
|
back_populates="plant_b",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlantCompatibility(Base):
|
||||||
|
__tablename__ = "plant_compatibilities"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("plant_id_a", "plant_id_b", name="uq_plant_compatibility"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
primary_key=True, default=uuid.uuid4, index=True
|
||||||
|
)
|
||||||
|
plant_id_a: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("plants.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
plant_id_b: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("plants.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
rating: Mapped[CompatibilityRating] = mapped_column(
|
||||||
|
Enum(CompatibilityRating, name="compatibility_rating"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
plant_a: Mapped["Plant"] = relationship(
|
||||||
|
"Plant", foreign_keys=[plant_id_a], back_populates="compatibility_a"
|
||||||
|
)
|
||||||
|
plant_b: Mapped["Plant"] = relationship(
|
||||||
|
"Plant", foreign_keys=[plant_id_b], back_populates="compatibility_b"
|
||||||
|
)
|
||||||
40
backend/app/models/planting.py
Normal file
40
backend/app/models/planting.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import date, datetime, timezone
|
||||||
|
|
||||||
|
from sqlalchemy import Date, DateTime, ForeignKey, Integer, Numeric, Text, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class BedPlanting(Base):
|
||||||
|
__tablename__ = "bed_plantings"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
primary_key=True, default=uuid.uuid4, index=True
|
||||||
|
)
|
||||||
|
bed_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("beds.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
plant_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("plants.id", ondelete="RESTRICT"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
area_m2: Mapped[float | None] = mapped_column(Numeric(5, 2), nullable=True)
|
||||||
|
count: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||||
|
planted_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
removed_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
bed: Mapped["Bed"] = relationship("Bed", back_populates="plantings") # noqa: F821
|
||||||
|
plant: Mapped["Plant"] = relationship("Plant", back_populates="plantings") # noqa: F821
|
||||||
40
backend/app/models/tenant.py
Normal file
40
backend/app/models/tenant.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, String, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Tenant(Base):
|
||||||
|
__tablename__ = "tenants"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
primary_key=True, default=uuid.uuid4, index=True
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
onupdate=lambda: datetime.now(timezone.utc),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user_tenants: Mapped[list["UserTenant"]] = relationship( # noqa: F821
|
||||||
|
"UserTenant", back_populates="tenant", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
beds: Mapped[list["Bed"]] = relationship( # noqa: F821
|
||||||
|
"Bed", back_populates="tenant", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
plants: Mapped[list["Plant"]] = relationship( # noqa: F821
|
||||||
|
"Plant", back_populates="tenant"
|
||||||
|
)
|
||||||
71
backend/app/models/user.py
Normal file
71
backend/app/models/user.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, String, UniqueConstraint, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class TenantRole(str, enum.Enum):
|
||||||
|
READ_ONLY = "READ_ONLY"
|
||||||
|
READ_WRITE = "READ_WRITE"
|
||||||
|
TENANT_ADMIN = "TENANT_ADMIN"
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
primary_key=True, default=uuid.uuid4, index=True
|
||||||
|
)
|
||||||
|
email: Mapped[str] = mapped_column(
|
||||||
|
String(255), unique=True, nullable=False, index=True
|
||||||
|
)
|
||||||
|
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
full_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
|
is_superadmin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
onupdate=lambda: datetime.now(timezone.utc),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user_tenants: Mapped[list["UserTenant"]] = relationship(
|
||||||
|
"UserTenant", back_populates="user", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserTenant(Base):
|
||||||
|
__tablename__ = "user_tenants"
|
||||||
|
__table_args__ = (UniqueConstraint("user_id", "tenant_id", name="uq_user_tenant"),)
|
||||||
|
|
||||||
|
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("users.id", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("tenants.id", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
role: Mapped[TenantRole] = mapped_column(
|
||||||
|
Enum(TenantRole, name="tenant_role"),
|
||||||
|
nullable=False,
|
||||||
|
default=TenantRole.READ_ONLY,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user: Mapped["User"] = relationship("User", back_populates="user_tenants")
|
||||||
|
tenant: Mapped["Tenant"] = relationship( # noqa: F821
|
||||||
|
"Tenant", back_populates="user_tenants"
|
||||||
|
)
|
||||||
0
backend/app/schemas/__init__.py
Normal file
0
backend/app/schemas/__init__.py
Normal file
26
backend/app/schemas/auth.py
Normal file
26
backend/app/schemas/auth.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
|
||||||
|
from app.schemas.tenant import TenantRead
|
||||||
|
from app.schemas.user import UserRead
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequest(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshRequest(BaseModel):
|
||||||
|
refresh_token: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenResponse(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
refresh_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
user: UserRead
|
||||||
|
tenants: list[TenantRead]
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenResponse(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
45
backend/app/schemas/tenant.py
Normal file
45
backend/app/schemas/tenant.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, field_validator
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class TenantBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
slug: str
|
||||||
|
|
||||||
|
@field_validator("slug")
|
||||||
|
@classmethod
|
||||||
|
def slug_format(cls, v: str) -> str:
|
||||||
|
if not re.match(r"^[a-z0-9-]+$", v):
|
||||||
|
raise ValueError(
|
||||||
|
"Der Slug darf nur Kleinbuchstaben, Zahlen und Bindestriche enthalten."
|
||||||
|
)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class TenantCreate(TenantBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TenantUpdate(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
slug: str | None = None
|
||||||
|
|
||||||
|
@field_validator("slug")
|
||||||
|
@classmethod
|
||||||
|
def slug_format(cls, v: str | None) -> str | None:
|
||||||
|
if v is not None and not re.match(r"^[a-z0-9-]+$", v):
|
||||||
|
raise ValueError(
|
||||||
|
"Der Slug darf nur Kleinbuchstaben, Zahlen und Bindestriche enthalten."
|
||||||
|
)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class TenantRead(TenantBase):
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
id: uuid.UUID
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
44
backend/app/schemas/user.py
Normal file
44
backend/app/schemas/user.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, EmailStr, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
full_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
password: str
|
||||||
|
|
||||||
|
@field_validator("password")
|
||||||
|
@classmethod
|
||||||
|
def password_min_length(cls, v: str) -> str:
|
||||||
|
if len(v) < 8:
|
||||||
|
raise ValueError("Das Passwort muss mindestens 8 Zeichen lang sein.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
full_name: str | None = None
|
||||||
|
email: EmailStr | None = None
|
||||||
|
password: str | None = None
|
||||||
|
is_active: bool | None = None
|
||||||
|
|
||||||
|
@field_validator("password")
|
||||||
|
@classmethod
|
||||||
|
def password_min_length(cls, v: str | None) -> str | None:
|
||||||
|
if v is not None and len(v) < 8:
|
||||||
|
raise ValueError("Das Passwort muss mindestens 8 Zeichen lang sein.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class UserRead(UserBase):
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
id: uuid.UUID
|
||||||
|
is_active: bool
|
||||||
|
is_superadmin: bool
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
11
backend/requirements.txt
Normal file
11
backend/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fastapi==0.111.0
|
||||||
|
uvicorn[standard]==0.29.0
|
||||||
|
sqlalchemy==2.0.30
|
||||||
|
alembic==1.13.1
|
||||||
|
asyncpg==0.29.0
|
||||||
|
pydantic==2.7.1
|
||||||
|
pydantic-settings==2.2.1
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
python-multipart==0.0.9
|
||||||
|
greenlet==3.0.3
|
||||||
15
docs/branching-strategy.md
Normal file
15
docs/branching-strategy.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Branching-Strategie
|
||||||
|
|
||||||
|
```
|
||||||
|
main
|
||||||
|
└── develop
|
||||||
|
├── feature/<name>
|
||||||
|
├── fix/<name>
|
||||||
|
└── chore/<name>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `main` – stabiler Produktionsstand, nur Merges aus `develop`
|
||||||
|
- `develop` – Integrationsstand, Basis für alle Feature-Branches
|
||||||
|
- `feature/*` – neue Features, Basis: `develop`
|
||||||
|
- `fix/*` – Bugfixes, Basis: `develop`
|
||||||
|
- `chore/*` – Wartung, Konfiguration, Basis: `develop`
|
||||||
@@ -13,17 +13,27 @@ Allgemein gültige Standards für die Arbeit in diesem Repository.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Git
|
## Git & Branching
|
||||||
|
|
||||||
### Branching
|
### Struktur
|
||||||
|
|
||||||
```
|
```
|
||||||
main – stabiler Produktionsstand
|
main
|
||||||
feature/<name> – neue Features
|
└── develop
|
||||||
fix/<name> – Bugfixes
|
├── feature/<name> – neue Features
|
||||||
chore/<name> – Wartung, Abhängigkeiten, Konfiguration
|
├── fix/<name> – Bugfixes / kleine Fixes
|
||||||
|
└── debug/<name> – Debugging / Fehleranalyse
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Regeln
|
||||||
|
|
||||||
|
0. **Branch-Wechsel erfolgen selbstständig** – Claude wechselt eigenständig in den jeweils passenden Branch, ohne nachzufragen. Dabei gelten alle übrigen Regeln uneingeschränkt.
|
||||||
|
1. **Nie direkt nach `main` pushen oder mergen.** Änderungen in `main` kommen ausschließlich über eine Pull-Request aus `develop`.
|
||||||
|
2. Jede Änderung findet in einem eigenen `feature/` oder `fix/` Branch unterhalb von `develop` statt.
|
||||||
|
3. In `develop` mergen erst, wenn die Arbeit abgeschlossen ist und alle Tests erfolgreich waren.
|
||||||
|
4. Eine PR nach `main` wird nur auf explizite Anweisung des Nutzers geöffnet.
|
||||||
|
5. Vor jedem Merge nach `develop` und vor jeder PR nach `main` werden alle wesentlichen Dokumente (README.md, CHANGELOG.md, docs/) geprüft und ggf. aktualisiert.
|
||||||
|
|
||||||
### Commit-Messages
|
### Commit-Messages
|
||||||
|
|
||||||
Format: `<type>: <short description>` (max. 72 Zeichen)
|
Format: `<type>: <short description>` (max. 72 Zeichen)
|
||||||
@@ -37,7 +47,31 @@ Format: `<type>: <short description>` (max. 72 Zeichen)
|
|||||||
| `docs` | Nur Dokumentation |
|
| `docs` | Nur Dokumentation |
|
||||||
| `chore` | Build, Dependencies, Konfiguration |
|
| `chore` | Build, Dependencies, Konfiguration |
|
||||||
|
|
||||||
Beispiel: `feat: add watering schedule to plant detail view`
|
**Nach jedem Commit wird sofort gepusht.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Versionierung
|
||||||
|
|
||||||
|
Schema: `MAJOR.MINOR.PATCH`
|
||||||
|
|
||||||
|
| Teil | Bedeutung | Wechsel |
|
||||||
|
|---|---|---|
|
||||||
|
| `MAJOR` | Hauptrelease | Nur auf explizite Anweisung des Nutzers |
|
||||||
|
| `MINOR` | Größere Updates, neue Features | Bei jeder Feature-Erweiterung oder größerem Umbau |
|
||||||
|
| `PATCH` | Kleine Fixes, Minimalkorrekturen | Bei Bugfixes, kleinen Ergänzungen, Mini-Umbauten |
|
||||||
|
|
||||||
|
- Die aktuelle Version steht in [CHANGELOG.md](../CHANGELOG.md) und in der Datei `VERSION`
|
||||||
|
- Nach **jeder** Änderung wird die Versionsnummer eigenständig erhöht und in beiden Dateien notiert
|
||||||
|
- Die Versionsnummer wird im Commit-Message-Footer vermerkt: `Version: x.y.z`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Projektstruktur-Dokumentation
|
||||||
|
|
||||||
|
- Alle Funktionen/Module werden kurz in [docs/project-structure.md](project-structure.md) dokumentiert
|
||||||
|
- Ziel: Token-Effizienz in zukünftigen Conversations – nicht alles neu einlesen müssen
|
||||||
|
- Bei jeder Änderung an Funktionen/Modulen wird die Dokumentation synchron aktualisiert
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -56,6 +90,7 @@ Beispiel: `feat: add watering schedule to plant detail view`
|
|||||||
- Integrationstests an Systemgrenzen (API, Datenbank)
|
- Integrationstests an Systemgrenzen (API, Datenbank)
|
||||||
- Keine Mocks für die Datenbank in Integrationstests
|
- Keine Mocks für die Datenbank in Integrationstests
|
||||||
- Testdatei liegt neben der zu testenden Datei oder in einem `__tests__`/`tests`-Verzeichnis auf gleicher Ebene
|
- Testdatei liegt neben der zu testenden Datei oder in einem `__tests__`/`tests`-Verzeichnis auf gleicher Ebene
|
||||||
|
- **In `develop` wird erst gemergt, wenn alle Tests grün sind**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,59 @@
|
|||||||
# Projektstruktur
|
# Projektstruktur & Modulreferenz
|
||||||
|
|
||||||
> Dieses Dokument beschreibt die Verzeichnisstruktur und Architektur des Gartenmanagers.
|
> **Token-Sparmaßnahme:** Dieses Dokument ist die erste Anlaufstelle.
|
||||||
> Bei strukturellen Änderungen bitte hier aktualisieren.
|
> Vor dem Öffnen von Quellcode hier nachschlagen.
|
||||||
|
> Bei jeder Änderung an Funktionen, Modulen oder der Verzeichnisstruktur sofort aktualisieren.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Verzeichnisübersicht
|
## Verzeichnisstruktur
|
||||||
|
|
||||||
```
|
```
|
||||||
gartenmanager/
|
gartenmanager/
|
||||||
├── CLAUDE.md # Guidance für Claude Code
|
├── .claude/ # Claude-Tooling (kein Projektcode)
|
||||||
|
│ ├── scripts/
|
||||||
|
│ │ ├── bump.sh # Version bumpen + commit + push
|
||||||
|
│ │ └── new-feature.sh # Feature-Branch erstellen
|
||||||
|
│ └── session-context.md # Sessionstart-Kontext
|
||||||
|
├── .gitea/
|
||||||
|
│ └── PULL_REQUEST_TEMPLATE.md
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── development-standards.md # Allgemeine Entwicklungsstandards
|
│ ├── branching-strategy.md
|
||||||
│ └── project-structure.md # Dieses Dokument
|
│ ├── development-standards.md
|
||||||
│
|
│ └── project-structure.md # dieses Dokument
|
||||||
│ (weitere Verzeichnisse entstehen mit dem Projekt)
|
├── .gitattributes
|
||||||
|
├── CHANGELOG.md
|
||||||
|
├── CLAUDE.md
|
||||||
|
├── README.md
|
||||||
|
└── VERSION
|
||||||
```
|
```
|
||||||
|
|
||||||
> Sobald der Techstack feststeht, wird diese Struktur hier dokumentiert.
|
> Sobald Quellcode-Verzeichnisse entstehen, hier ergänzen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architektur
|
## Modulübersicht
|
||||||
|
|
||||||
> Noch festzulegen. Typische Optionen für dieses Projekt:
|
> Noch kein Anwendungscode vorhanden. Sobald Module/Komponenten entstehen:
|
||||||
>
|
>
|
||||||
> - **Monolith** (z. B. Full-Stack-Framework wie Laravel, Django, Rails)
|
> ```
|
||||||
> - **Frontend + Backend getrennt** (z. B. Vue/React + REST/GraphQL API)
|
> Modulname | Datei(en) | Zweck | Exportierte Funktionen
|
||||||
> - **Mobile App** (z. B. Flutter, React Native)
|
> ```
|
||||||
|
>
|
||||||
|
> **Format pro Funktion:**
|
||||||
|
> `funktionsname(param: Typ): Rückgabetyp` – Ein-Satz-Beschreibung
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Domänenmodell (geplant)
|
## Domänenmodell
|
||||||
|
|
||||||
Die Kernkonzepte des Gartenmanagers:
|
| Entität | Felder (geplant) | Beziehungen |
|
||||||
|
|---|---|---|
|
||||||
| Konzept | Beschreibung |
|
| `Plant` | name, sowingStart, sowingEnd, waterInterval, spacing | gehört zu Bed |
|
||||||
|---|---|
|
| `Bed` | name, width, length, location | enthält viele Plants |
|
||||||
| `Plant` / Pflanze | Eine Pflanzenart mit Eigenschaften (Aussaatzeit, Abstand, Wasserbedarf …) |
|
| `SowingCalendar` | year, plantId, sowDate, plantDate | referenziert Plant |
|
||||||
| `Bed` / Beet | Ein physischer Gartenbereich, dem Pflanzen zugeordnet werden |
|
| `Task` | title, dueDate, done, bedId? | optional zu Bed |
|
||||||
| `SowingCalendar` / Aussaatkalender | Zeitplan für Aussaat und Pflanzung je Pflanze und Jahr |
|
| `WateringSchedule` | bedId/plantId, intervalDays, lastWatered | referenziert Bed oder Plant |
|
||||||
| `Task` / Aufgabe | Eine zu erledigende Gartenarbeit mit Fälligkeitsdatum |
|
|
||||||
| `WateringSchedule` / Bewässerungsplan | Regelmäßige Bewässerungsintervalle je Beet oder Pflanze |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -51,6 +63,18 @@ Die Kernkonzepte des Gartenmanagers:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Schnittstellen
|
## Schnittstellen / API
|
||||||
|
|
||||||
> Noch festzulegen.
|
> Noch festzulegen. Hier Endpunkte mit Kurzbeschreibung eintragen:
|
||||||
|
>
|
||||||
|
> ```
|
||||||
|
> GET /api/plants – alle Pflanzen
|
||||||
|
> POST /api/plants – neue Pflanze anlegen
|
||||||
|
> ...
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
> Relevante Umgebungsvariablen und Konfigurationsdateien hier auflisten.
|
||||||
|
|||||||
Reference in New Issue
Block a user