diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5134dbe --- /dev/null +++ b/Makefile @@ -0,0 +1,75 @@ +# Root developer entrypoint. Wraps the per-service tooling (Gradle, pnpm, pytest, docker +# compose) that already exists in each subdirectory - see docs/cicd.md for what CI itself runs. +# +# Unlike CI, these targets don't pass --no-daemon to Gradle: locally you want the daemon kept +# warm between runs, you just don't want a stale one in an ephemeral CI container. +.DEFAULT_GOAL := help + +SPRING_SERVICES := organization member event finance feedback letter + +.PHONY: help +help: ## Show this help message + @echo "Usage: make " + @echo "" + @grep -E '^[a-zA-Z0-9_-]+:.*## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' + +## --- Docker Compose lifecycle ----------------------------------------------------------- +## Always cd into infra/ first so docker-compose.override.yml auto-merges (matching README's +## "Running Locally" section) - never pass -f infra/docker-compose.yml directly, that skips it. + +.PHONY: up +up: ## Start the local stack (docker compose up -d --build) + cd infra && docker compose up -d --build + +.PHONY: down +down: ## Stop the local stack, keep the database volume + cd infra && docker compose down + +.PHONY: down-v +down-v: ## Stop the local stack and delete all volumes (wipes the database) + cd infra && docker compose down -v + +.PHONY: logs +logs: ## Follow logs for all running containers + cd infra && docker compose logs -f + +.PHONY: ps +ps: ## List running containers and their status + cd infra && docker compose ps + +## --- Test / Lint / Build ---------------------------------------------------------------- +## No per-service targets on purpose - these always run against everything. + +.PHONY: test +test: ## Run every test suite (Spring x6, GenAI, web-client) + @for s in $(SPRING_SERVICES); do \ + echo "==> test: spring-$$s"; \ + (cd services/spring-$$s && ./gradlew test) || exit 1; \ + done + @echo "==> test: py-genai-helper" + cd services/py-genai-helper && pytest -q + @echo "==> test: web-client" + cd web-client && pnpm test:coverage + +.PHONY: lint +lint: ## Run every linter (Checkstyle, ruff, ESLint + typecheck) + @for s in $(SPRING_SERVICES); do \ + echo "==> lint: spring-$$s"; \ + (cd services/spring-$$s && ./gradlew checkstyleMain) || exit 1; \ + done + @echo "==> lint: py-genai-helper" + cd services/py-genai-helper && ruff check . + @echo "==> lint: web-client" + cd web-client && pnpm typecheck && pnpm lint + +.PHONY: build +build: ## Build every service (Spring x6 + web-client) + @for s in $(SPRING_SERVICES); do \ + echo "==> build: spring-$$s"; \ + (cd services/spring-$$s && ./gradlew build) || exit 1; \ + done + @echo "==> build: web-client" + cd web-client && pnpm build + +.PHONY: verify +verify: lint test build ## Run lint, test, and build for everything - mirrors what CI checks per PR diff --git a/README.md b/README.md index 2e6751b..75de55a 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,9 @@ All five schema-owning Spring services share a single **PostgreSQL 15** instance | Feedback | `feedback` | `feedback_user` | | Finance | `finance` | `finance_user` | -Schemas and users are created at DB init time by [`infra/postgres/init-db.sh`](infra/postgres/init-db.sh). Each service runs its own **Flyway** migrations on startup (`V1__create_tables.sql`, `V2__add_foreign_keys.sql` for cross-schema references, granted via `ALTER DEFAULT PRIVILEGES`). The letter service has no database; the GenAI service persists RAG documents in a Chroma vector store instead of PostgreSQL. +Schemas and users are created at DB init time by [`infra/postgres/init-db.sh`](infra/postgres/init-db.sh). Each service runs its own **Flyway** migrations on startup (`V1__create_tables.sql`, `V2__add_foreign_keys.sql` for cross-schema references, granted via `ALTER DEFAULT PRIVILEGES`). The letter service has no database of its own. + +The GenAI service also uses this same Postgres instance: it owns a sixth schema, `reports` (`reports_user`), where generated member/team report text is persisted (created idempotently at startup — Python has no Flyway). Separately, it persists RAG documents in a Chroma vector store, not PostgreSQL. ## Authentication (Keycloak) @@ -114,6 +116,18 @@ pre-commit install --hook-type pre-push Auto-fixing hooks modify files and abort the commit so you can re-stage. Bypass only in emergencies (`git commit --no-verify` / `git push --no-verify`) — CI still gates. Full hook config: [`.pre-commit-config.yaml`](.pre-commit-config.yaml). +The root [`Makefile`](Makefile) wraps the per-service tooling (Gradle, pnpm, pytest, docker compose) so you don't need to remember each one's exact invocation or working directory. Run `make help` for the full list: + +```bash +make up # start the local stack (see Running Locally below) +make test # run every test suite (Spring x6, GenAI, web-client) +make lint # run every linter (Checkstyle, ruff, ESLint + typecheck) +make build # build every service (Spring x6 + web-client) +make verify # lint + test + build for everything — mirrors what CI checks per PR +``` + +`test`, `lint`, and `build` always run against everything — there are no per-service targets, so a full check is always exactly one command. + ## Running Locally ```bash @@ -122,6 +136,8 @@ cp .env.example .env # first time only — local-dev secrets, gitignored docker compose up -d --build ``` +Equivalent shortcut from the repo root: `make up` (also `make down`, `make down-v`, `make logs`, `make ps`) — see [Developer Setup](#developer-setup) above. Both do exactly the same thing: the Makefile just `cd`s into `infra/` first, same as the manual steps. + This auto-merges [`infra/docker-compose.override.yml`](infra/docker-compose.override.yml), which strips TLS/Let's-Encrypt/Host-routing so everything is reachable on plain HTTP: | URL | Service | diff --git a/docs/architecture.md b/docs/architecture.md index 3b0bef8..586441f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,12 @@ # Architecture -This is the detailed companion to the [README's Architecture section](../README.md#architecture): per-service responsibilities, who calls whom, and how a request is authenticated end to end. There is deliberately no UML diagram here — see [`docs/outdated/`](outdated/) for why the previous ones were retired. +This is the detailed companion to the [README's Architecture section](../README.md#architecture): per-service responsibilities, who calls whom, and how a request is authenticated end to end. The diagrams below reflect the current six-service system; the PNGs under [`docs/outdated/`](outdated/) model an earlier three-service topology and are kept only as historical record. Diagram sources live in [`docs/diagrams/`](diagrams/) as Mermaid files — see [`docs/diagrams/README.md`](diagrams/README.md) to regenerate them after an architecture change. + +## Subsystem Decomposition + +Depicts the Kubernetes (Rancher/RKE2) deployment specifically, since it's the one continuously-live, gradeable cluster target — see [Environment differences](#environment-differences) below for how Compose/VM differ. `oauth2-proxy` only gates the web-client's cookie session; every backend service, Keycloak, api-docs, and Grafana are reached directly through the ingress and each does its own auth (Bearer-JWT check, or in Grafana's case its own separate Keycloak OAuth client) — there is no single shared gatekeeper. Note also the two independent PostgreSQL instances: `app-db` (schema-per-service, described below) and a separate `keycloak-db` that only Keycloak uses. + +![Subsystem decomposition diagram](diagrams/subsystem-decomposition.png) ## Subsystems @@ -31,11 +37,12 @@ A Python 3.12 / Flask service using LangChain. Unlike the Spring services, it is - It calls **feedback-service** (`GET /feedback`, Bearer-forwarded) to pull the data it summarizes when generating a member/team report. - It calls **either OpenAI or a local Ollama instance** to run inference, selected per-request via the `uselocal` field on the report-generation endpoints (not a global config flag) — the web client exposes this as the "Use local LLM" toggle on the helper page. -- RAG question-answering persists uploaded documents in a **Chroma** vector store (a PVC-backed directory in Kubernetes, a bind-mounted volume elsewhere) rather than PostgreSQL. +- RAG question-answering persists uploaded documents in a **Chroma** vector store (a PVC-backed directory in Kubernetes, a bind-mounted volume elsewhere), not PostgreSQL. +- Generated report text *is* persisted in PostgreSQL, though: the service owns a sixth schema, `reports` (`reports_user`), in the same instance as the Spring services, with its tables created idempotently at startup since Python has no Flyway. ### Database — PostgreSQL -Single instance, schema-per-service, documented in the [README's Database section](../README.md#database). No service reads another service's schema directly; cross-schema foreign keys exist (e.g. `event.events.creator_id → member.members.id`) but are enforced by the database, not queried across services. +Single instance, schema-per-service, documented in the [README's Database section](../README.md#database) — five schemas for the five Spring services that own one, plus the `reports` schema owned by the GenAI service (see above). Every service user is also granted a shared, read-only `reader` role (`infra/postgres/init-db.sh`) that can `SELECT` across all schemas but never write outside its own; this backs a handful of small, explicitly-documented read-only lookups — e.g. `event-service`'s `MemberEntity`, `letter-service`'s `TransactionEntity`, and the GenAI service's own member/team display-name lookups — used only to resolve a name or balance for display, never to write, and never as a substitute for calling the owning service's API. Cross-schema foreign keys (e.g. `event.events.creator_id → member.members.id`) are enforced by the database independently of this. ### Proxy & Auth — Traefik / nginx ingress + Keycloak @@ -43,6 +50,18 @@ Single instance, schema-per-service, documented in the [README's Database sectio - **Kubernetes**: the cluster's own nginx ingress does the prefix-stripping and routing instead of Traefik; TLS is handled at the cluster edge. - **Keycloak** (realm `devops`) is the single OIDC provider in every environment. Four confidential/public clients exist: `devops-client` (public, PKCE — the React app), `traefik-forward-auth` (confidential — gates the browser session at the proxy), `grafana` (confidential — Grafana's own admin-only OAuth login), and `org-role-sync` (confidential, service account only, no browser flow — organization-service and member-service's Admin REST API client, see above). None of the three confidential clients' secrets is hardcoded: each is templated as a `__PLACEHOLDER__` in `infra/keycloak/realm-config.json`, substituted at container start (Compose/VM) or chart render (Helm) from the matching GitHub secret — see [docs/cicd.md](cicd.md). +## Use Cases + +Route access is role-gated client-side (`web-client/src/app/navPolicy.ts`) and re-checked server-side by each service. Four roles exist: `member` (trainee), `trainer` (coach), `director`, `admin`. All four can additionally view the dashboard, browse events & sessions, view sports & teams, and manage their own profile — those baseline pages are omitted from the diagram below since every role shares them; only the role-differentiating capabilities are shown. Admin's capabilities are exactly the union of Coach's and Director's, with nothing unique of its own, so it's shown as generalizing both (`«is-a»`) rather than duplicating every edge. + +![Use case diagram](diagrams/use-case.png) + +## Analysis Object Model + +Modeled from the client's perspective — its `Role` type (`web-client/src/types.ts`) and each feature's role-gated actions — rather than the server's join-table implementation (`DirectorEntity`/`TrainerEntity`/`TraineeEntity`, each scoped to one sport or team). A person's role is a single flat attribute from the client's standpoint, not a per-scope assignment, so `Trainee`/`Coach`/`Director`/`Admin` are modeled as specializations of `Member` to group each role's distinct capabilities clearly. The two relationships that genuinely are per-scope — a coach coaches specific teams, a director directs specific sports — are kept as separate associations rather than folded into the role itself. Feedback, balances, letters, event attendance, and generated reports are all about a `Trainee` specifically (coaches and directors act on them, but are never themselves the subject), so those associations sit on `Trainee` rather than the abstract `Member` — matching the client's own `ReportKind`-discriminated `Report` type, member and team reports are modeled as one `Report` class rather than two near-identical ones. + +![Analysis object model diagram](diagrams/analysis-object-model.png) + ## Request lifecycle (example: loading the members page) 1. Browser requests `/` → proxy's forward-auth middleware checks for a session cookie; none yet → redirected through Keycloak's login page → cookie set on success. diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md new file mode 100644 index 0000000..0e0a700 --- /dev/null +++ b/docs/diagrams/README.md @@ -0,0 +1,39 @@ +# Diagrams + +Mermaid sources for the diagrams embedded in [`docs/architecture.md`](../architecture.md). Each `.mmd` file has a matching `.png` rendered from it — edit the `.mmd`, then regenerate the PNG, never the other way around. + +| Source | Rendered | Diagram | +|---|---|---| +| `subsystem-decomposition.mmd` | `subsystem-decomposition.png` | Subsystem Decomposition | +| `use-case.mmd` | `use-case.png` | Use Case | +| `analysis-object-model.mmd` | `analysis-object-model.png` | Analysis Object Model | + +## Regenerating + +```bash +cd docs/diagrams +npx -y @mermaid-js/mermaid-cli -i .mmd -o .png -b white -w 1400 -s 2 +``` + +Requires Node/npx (no global install needed — `npx -y` fetches `@mermaid-js/mermaid-cli` on demand and runs it headlessly via Puppeteer). + +## Manual label fix in `analysis-object-model.png` + +Mermaid's `classDiagram` layout has no `linkStyle`/spacing controls (unlike flowchart), so the auto-placed `belongs to` label originally overlapped the `Coach` box. This was fixed by hand-shifting that one label in the rendered SVG rather than in the `.mmd` — running the command above regenerates a technically-correct but visually-regressed PNG (label back on top of `Coach`). To reproduce the fix after a content change: + +```bash +cd docs/diagrams +npx -y @mermaid-js/mermaid-cli -i analysis-object-model.mmd -o /tmp/aom.svg -b white +python3 -c " +svg = open('/tmp/aom.svg').read() +old = '> + +UUID id + +String firstName + +String lastName + +String email + +String phoneNumber + +String address + +Date birthday + +Date joiningDate + +String information + +Role role + +updateProfile() + } + class Trainee { + +requestAIReport() + } + class Coach { + +createEvent() + +submitFeedback() + +manageMembers() + +generateLetter() + +requestAIReport() + } + class Director { + +manageTeamRoster() + +manageBalances() + +manageMembers() + +generateLetter() + +createEvent() + } + class Admin { + +manageSportsAndTeams() + +manageRoles() + } + + Member <|-- Trainee + Member <|-- Coach + Member <|-- Director + Member <|-- Admin + + class Sport { + +UUID id + +String name + +String description + } + class Team { + +UUID id + +String name + +String description + +String address + } + class Event { + +UUID id + +String name + +String description + +Instant startTime + +Instant endTime + +createEvent() + } + class Feedback { + +UUID id + +Integer rating + +String feedback + +Instant createdAt + +submitFeedback() + } + class Transaction { + +UUID id + +int amountCents + +String title + +String description + +Instant createdAt + +recordTransaction() + } + class Letter { + +String subject + +String content + +generatePdf() + +sendMail() + } + class Report { + +UUID id + +String kind + +String text + +Instant createdAt + } + + Director "*" --> "*" Sport : directs + Sport "1" --> "*" Team : offers + Coach "*" --> "*" Team : coaches + Trainee "*" --> "*" Team : belongs to + Team "1" --> "*" Report : subject of + Coach "1" --> "*" Feedback : writes + Event "1" --> "*" Feedback : about + Trainee "1" --> "*" Feedback : receives + Event "*" --> "*" Trainee : attendance + Trainee "1" --> "*" Transaction : owns + Trainee "1" --> "*" Letter : recipient of + Trainee "1" --> "*" Report : subject of diff --git a/docs/diagrams/analysis-object-model.png b/docs/diagrams/analysis-object-model.png new file mode 100644 index 0000000..ea95add Binary files /dev/null and b/docs/diagrams/analysis-object-model.png differ diff --git a/docs/diagrams/subsystem-decomposition.mmd b/docs/diagrams/subsystem-decomposition.mmd new file mode 100644 index 0000000..f99667d --- /dev/null +++ b/docs/diagrams/subsystem-decomposition.mmd @@ -0,0 +1,53 @@ +flowchart TB + ING["nginx Ingress · TLS via cert-manager
ge83mom-devops26.stud.k8s.aet.cit.tum.de"] + OAP["oauth2-proxy
cookie session gate"] + WC["web-client"] + DOCS["api-docs (swagger-ui)
open, no auth"] + + subgraph Backend["Backend services — own Bearer-JWT check, HPA 1-2 replicas"] + ORG["organization-service"] + MEM["member-service"] + EVT["event-service"] + FIN["finance-service"] + FBK["feedback-service"] + LET["letter-service"] + GAI["py-genai-helper
single replica — RWO PVC"] + end + + subgraph Auth["Auth"] + KC["Keycloak
realm: devops"] + KCDB[("keycloak-db")] + end + + APPDB[("app-db
schema per service + reports")] + + subgraph Inference["Inference"] + OLL["Ollama"] + OAI["OpenAI API"] + VEC[("Chroma PVC")] + end + + subgraph Observability["Observability — same namespace"] + PROM["Prometheus"] + GRAF["Grafana
own Keycloak OAuth"] + LOKI["Loki"] + ALLOY["Alloy"] + end + + ING -->|"/"| OAP --> WC + ING -->|"/api/v1/…"| Backend + ING -->|"/auth"| KC + ING -->|"/docs"| DOCS + ING -->|"/dashboard"| GRAF + + Backend --> APPDB + GAI --> OLL + GAI --> OAI + GAI --> VEC + KC --> KCDB + + ALLOY --> LOKI + PROM -.->|scrapes| Backend + PROM -.->|scrapes| KC + GRAF --> PROM + GRAF --> LOKI diff --git a/docs/diagrams/subsystem-decomposition.png b/docs/diagrams/subsystem-decomposition.png new file mode 100644 index 0000000..0b7e2d6 Binary files /dev/null and b/docs/diagrams/subsystem-decomposition.png differ diff --git a/docs/diagrams/use-case.mmd b/docs/diagrams/use-case.mmd new file mode 100644 index 0000000..b53f59c --- /dev/null +++ b/docs/diagrams/use-case.mmd @@ -0,0 +1,30 @@ +%%{init: {'flowchart': {'nodeSpacing': 90, 'rankSpacing': 130}}}%% +flowchart LR + Member(["Member"]) + Coach(["Coach"]) + Director(["Director"]) + Admin(["Admin"]) + + subgraph System["Sports Club Management System"] + UC6(("View balance & transactions")) + UC5(("Submit / view feedback")) + UC7(("Generate AI progress report")) + UC8(("Manage member profiles")) + UC9(("Generate & send letters")) + UC10(("Create & manage events")) + UC11(("Manage sports & teams")) + end + UC7a(("Select cloud or local LLM")) + + Member --> UC6 & UC5 & UC7 + Coach --> UC5 & UC7 & UC8 & UC9 & UC10 + Director --> UC8 & UC9 & UC10 & UC11 & UC6 + + Admin -.->|"«is-a»"| Coach + Admin -.->|"«is-a»"| Director + + UC7 -.->|"«include»"| UC7a + + linkStyle 0,1,2 stroke:#0072B2,stroke-width:2px + linkStyle 3,4,5,6,7 stroke:#009E73,stroke-width:2px + linkStyle 8,9,10,11,12 stroke:#E69F00,stroke-width:2px diff --git a/docs/diagrams/use-case.png b/docs/diagrams/use-case.png new file mode 100644 index 0000000..303a636 Binary files /dev/null and b/docs/diagrams/use-case.png differ diff --git a/docs/problem-statement.md b/docs/problem-statement.md index 6851901..7b72269 100644 --- a/docs/problem-statement.md +++ b/docs/problem-statement.md @@ -5,7 +5,7 @@ Our application provides a centralized and hierarchical club management system t The intended users, on the other hand, are the club organizers, who can use the app for organizational purposes as an all-in-one solution, reducing overhead by automating billing and maintaining a structured view of the organization. On the other hand, the members of the club have a more organized overview of training sessions and events, as well as personalized feedback for improvements. Additionally, the application allows trainers and mentors to document the progress of trainees easily and provide proper guidance. -This system uses GenAI as a personal assistant. It analyzes data for the user, unstructured trainer notes, attendance records, and member profiles to tailor a progress report that can be used to track successes and facilitate further improvement. +This system uses GenAI as a personal assistant. It analyzes trainer feedback, augmented with relevant club reference material, to tailor a progress report that can be used to track successes and facilitate further improvement. ## Scenarios