Skip to content

Latest commit

 

History

174 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kontor

Kontor consolidates personal finance data - transactions, portfolios, and market data - and uses a GenAI layer to surface personalised, actionable insights. Stock and ETF data is sourced from Twelve Data; the AI component uses Retrieval-Augmented Generation (RAG) over the user's own financial data and curated financial news.


Tech Stack

Layer Technology
Client React 19, TypeScript 6, Vite 8, Tailwind CSS 4, shadcn/ui
Server Java 25, Spring Boot 4, Gradle
News Service Java 25, Spring Boot 4, RabbitMQ
AI Service Python 3.14, FastAPI, uv
Linting/Formatting Biome (client), Spotless + Checkstyle + Error Prone (server), Ruff (AI)

Setup

Prerequisites

Running the full stack locally needs only Docker (Docker Desktop or Docker Engine) with Compose v2. The rest of the toolchain (Node, Java 25, uv) is required only for the non-Docker Local Development workflow.

Configuration

Create your local environment file from the tracked template:

cp .env.example .env

The defaults in .env match the Compose defaults, so the stack runs as-is. Update .env to change the Postgres credentials, Keycloak database credentials, or the shared local Keycloak dev-user password — and to set the API keys described below.

The AI service accepts any hosted OpenAI-compatible provider through AI_API_KEY, AI_BASE_URL, AI_CHAT_MODEL, and AI_EMBEDDING_MODEL. The local and Kubernetes defaults point at the TUM course gateway and therefore require TUM network or eduVPN access; Azure defaults to OpenAI. When AI_API_KEY is missing, the service automatically falls back to a local LLM (see Local LLM (offline AI) below). If no local LLM is configured either (LOCAL_LLM_BASE_URL empty), recommendation calls return 503; if a local LLM is configured but not running, they return 502 ("Local LLM is not reachable").

Live stock/ETF market data (quotes, price history, the holdings chart) is sourced from Twelve Data via a TWELVE_DATA_API_KEY in .env. See Market data (Twelve Data) below for how to get a free key and what the free-tier rate limit means for the app.

Hosted news ingest uses the same provider key and base URL as chat, with an independently configurable AI_EMBEDDING_MODEL.

The Compose Keycloak service is for local development only. It builds the custom Keycloak image from infra/keycloak/theme/ (Keycloak with the Kontor login theme baked in), runs start-dev, imports infra/keycloak/realms/kontor-realm.json, and stores Keycloak state in the keycloak_postgres_data Docker volume. The realm import is skipped once the realm already exists, so delete that volume if you need to re-apply the import from scratch. The first docker compose up --build builds the theme image (Node + Maven), which takes a few minutes; subsequent runs use the cached layer.

Local Keycloak users live in infra/keycloak/realms/kontor-users-0.json. Add another object to the users array to create more local accounts with their own realmRoles and attributes. The sample users share the KEYCLOAK_DEV_USERS_PASSWORD environment placeholder so adding users does not require more Compose variables.

docker compose up --build
Service URL
Client http://localhost:5173
Server (core) http://localhost:8080
AI Service http://localhost:8000
News Service http://localhost:8082
Keycloak http://localhost:8081
RabbitMQ management http://localhost:15672

The news aggregator and its RabbitMQ broker are documented in news/README.md; each service directory has its own README (client/, core/, ai/).

The client redirects to Keycloak for login. Sign in with a seeded local user — username dev, password dev (the password comes from KEYCLOAK_DEV_USERS_PASSWORD, default dev). Two more seeded users exist: analyst (regular user) and admin-user (also holds the kontor-admin role required to trigger news-aggregation runs). Add or edit users in infra/keycloak/realms/kontor-users-0.json.

The dev user comes pre-loaded with example data (several months of transactions and a multi-asset portfolio), so the dashboard is populated on first login. To try the CSV import flow yourself, upload the sample file at resources/example-data/transaction-csv.example.csv. Note that an import replaces the signed-in user's existing transactions, so it overwrites the seeded data.

Market data (Twelve Data)

Live stock/ETF market data — quotes, price history, and the holdings chart — comes from Twelve Data. You need a free TWELVE_DATA_API_KEY in .env for it to work; the free tier is enough for local development.

How to get an API key:

  1. Create a free account at https://twelvedata.com/pricing (the free plan needs no card).

  2. After signing in, open your API dashboard — it shows your unique API key.

  3. Copy the key into the root .env file:

    TWELVE_DATA_API_KEY=your-key-here
  4. (Re)start the stack so core picks up the key:

    docker compose up --build

The holdings chart on the dashboard should now render live prices.

Rate limit. The free tier caps how often you can call the API (currently 8 API credits per minute and 800 requests per day). When you exceed it, Twelve Data responds with 429; core passes that through as a 429, and the holdings chart dialog shows a "Live market data paused" notice with the provider's rate-limit message instead of the chart. Wait for the limit window to reset and reopen the chart — no key change is needed.

No key configured. When TWELVE_DATA_API_KEY is empty, market-data endpoints return 502 with a message explaining the key is missing, and the chart shows that message instead of prices. The rest of the app works normally.

Local LLM (offline AI)

When AI_API_KEY is empty, the AI service falls back to a local Ollama model so the AI features keep working without hosted-provider access. The fallback is opt-in: LOCAL_LLM_BASE_URL is unset by default, so a bare advisor run (no Compose) returns 503 until you point it at a local model. Compose sets it explicitly; hosted Helm and Azure deployments require an API key. There are two ways to provide it.

Native Ollama (recommended on macOS). A native Ollama uses the Mac GPU and is much faster than a container:

brew install ollama        # or download the app from ollama.com
ollama pull llama3.2       # one-time model download
ollama pull nomic-embed-text # one-time news embedding model download
ollama serve               # start this yourself and keep it running

The default docker compose up reaches it via host.docker.internal:11434. No extra flags needed.

Containerised Ollama (no host setup). Layer the opt-in override file — it runs Ollama in a container, pulls the model once, and points the AI service at it:

docker compose -f docker-compose.yml -f docker-compose.local-llm.yml up --build

Note: containerised Ollama on macOS is CPU-only (Docker Desktop can't pass through Apple's Metal GPU), so it is noticeably slower than the native option above.

Override the model with LOCAL_LLM_MODEL (default llama3.2). Trust boundary: in fallback mode your portfolio data is sent to whatever listens on LOCAL_LLM_BASE_URL. Set LOCAL_LLM_BASE_URL= (empty) in .env to disable the fallback entirely.

Local Development

Client (client/)

cd client
npm install
npm run dev
Task Command
Type check npm run typecheck
Unit tests npm test
Unit tests (watch) npm run test:watch
Unit tests (coverage) npm run test:coverage
E2E tests npm run e2e
Lint npm run lint
Lint (autofix) npm run lint:fix
Format (autofix) npm run format
Build npm run build

Server (core/)

cd core
./gradlew build
Task Command
Test ./gradlew test
Compile check ./gradlew compileJava
Format check ./gradlew spotlessCheck
Format fix ./gradlew spotlessApply
Lint ./gradlew checkstyleMain checkstyleTest

AI Service (ai/)

Requires uv.

cd ai
uv sync
uv run uvicorn advisor.main:app --reload
Task Command
Install deps uv sync
Dev server uv run uvicorn advisor.main:app --reload
Test uv run pytest
Type check uv run ty check
Format check uv run ruff format --check .
Format fix uv run ruff format .
Lint uv run ruff check .
Lint (autofix) uv run ruff check --fix .

Generated API Client

The client's TypeScript types and Zod schemas are generated from each backend's OpenAPI spec — never hand-written. After changing a route or its request/response models, regenerate the spec and the client, then commit the result:

cd core && ./gradlew generateOpenApiDocs   # core spec  → core/docs/openapi.yml
cd ai   && uv run export-openapi           # AI spec    → ai/docs/openapi.json
cd client && npm run generate:api          # both specs → client/src/network/generated*

CI's openapi-sync workflow fails if the committed files are out of sync.


Deployment

The production stack runs on the TUM Kubernetes cluster and is served at:

Environment URL
App https://kontor.live
Auth (Keycloak) https://auth.kontor.live
Grafana https://grafana.kontor.live

Unlike the local stack, prod is not pre-seeded with example data — new accounts start empty, so import a CSV (e.g. the sample at resources/example-data/transaction-csv.example.csv) to populate the dashboard.

The Kontor stack ships as a single Helm chart in deploy/helm/kontor/ that bundles the client, core, the news aggregator, the AI service, their Postgres instances (core's and the AI service's with pgvector), a RabbitMQ broker (subchart), and an optional Keycloak. The chart README documents environment overlays (values-prod.yaml, values-pr.template.yaml), required secrets, and the install / upgrade flow.

Deployment is CI-driven: a push to main releases and deploys prod into the team-3m namespace, and adding the deploy:preview label to a PR stands up an ephemeral preview in team-3m-pr-<N> (torn down when the label is removed or the PR closes). Manually triggering the CI/CD workflow redeploys the latest version without a code change.

There is also a standalone single-VM deployment on Azure (Terraform + Docker Compose + Traefik), served at https://azure.kontor.live and documented in deploy/azure/README.md.

For a manual install, copy deploy/helm/kontor/secrets.example.yaml to secrets.yaml and fill in all REPLACE_ME values. Create the hosted-provider Secret out-of-band so the API key is not retained in Helm release history. The course gateway URL and model defaults live in values.yaml (and the deploy workflow's vars.AI_* || fallback expressions), not in values-prod.yaml; change ai.baseUrl, ai.chatModel, and ai.embeddingModel to use another OpenAI-compatible provider:

kubectl create secret generic kontor-ai-provider \
  -n team-3m \
  --from-literal=AI_API_KEY="$AI_API_KEY"

helm upgrade --install kontor ./deploy/helm/kontor \
  -n team-3m \
  -f deploy/helm/kontor/values-prod.yaml \
  -f deploy/helm/kontor/secrets.yaml \
  --set-string ai.existingSecret=kontor-ai-provider

PR previews run hosted AI too (it is a requirement). The k8s-preview GitHub environment supplies its own AI_API_KEY; use a separate, budget-capped key there so pull-request-built code never holds the production credential.

Keycloak Login Theme

Keycloak ships as a custom image with the Kontor login theme baked in, rather than stock Keycloak plus a sibling jar. The theme source is vendored under infra/keycloak/theme/ — a minimal Keycloakify project whose Docker build assembles the theme jar and copies it into /opt/keycloak/providers/. CI builds and pushes it to ghcr.io/aet-devops26/team-3m/kontor-keycloak; the realm selects it via loginTheme: kontor. See the vendored README for how to edit the theme.


Observability

A single shared LGTM stack (Loki, Grafana, Tempo, Prometheus) with a Grafana Alloy OTLP gateway serves all environments (prod and every PR preview). It has its own chart (deploy/helm/observability/), namespace (team-3m-monitoring), and workflow_dispatch workflow (.github/workflows/observability.yml) — PR deploy/teardown never touches it. The core, news, and AI services push traces/metrics via OTel, the client ships browser RUM via Grafana Faro, and pod logs are collected into Loki. Alloy also scrapes Keycloak, the RabbitMQ broker (rabbitmq_prometheus), and the AI database's postgres_exporter (pgvector embedding store). Every signal is tagged with deployment_environment, so one Grafana variable switches between environments.

The same configuration runs locally as a compose overlay:

docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d

Grafana is then available at http://localhost:3001.


Top Level Architecture

Component Diagram

Use Case Diagram

Use Case Diagram

Analysis Object Model

Analysis Object Model

Deployment Diagram

Deployment Diagram


AI Agent Setup

This project is configured for AI coding agents (Claude Code, Codex). Rules, skills, and MCP servers ensure agents follow consistent standards across the codebase.

Project Instructions

CLAUDE.md is the top-level instruction file for Claude Code. It is automatically loaded at the start of every agent session and provides the agent with project context, commands, and coding rules. AGENTS.md serves the same purpose for Codex. For more information, see the Claude Code docs on CLAUDE.md.

Rules

Coding rules live in .claude/rules/ and are automatically loaded based on the files being edited. They enforce consistent style, testing, security, and architectural patterns. For more information, see the Claude Code Docs.

.claude/rules/
├── common/              # Apply to all code
│   ├── coding-style.md  # Immutability, KISS/DRY/YAGNI, naming, file organization
│   ├── testing.md       # TDD workflow, AAA pattern, 80%+ coverage
│   ├── patterns.md      # Repository pattern, API envelope, service classes
│   └── code-review.md   # Review checklist, security triggers, severity levels
├── java/                # Apply to *.java, pom.xml, build.gradle*
│   ├── coding-style.md  # Records, sealed classes, Optional, modern Java (16+)
│   ├── patterns.md      # Constructor injection, Spring conventions, Flyway, jOOQ
│   └── testing.md       # JUnit Jupiter 5, AssertJ, Mockito, Testcontainers
├── python/              # Apply to *.py, pyproject.toml
│   ├── coding-style.md  # Ruff, type hints, PEP 8 naming, modern Python (3.13+)
│   ├── patterns.md      # FastAPI routers, pydantic models, uv workflow
│   └── testing.md       # pytest with asyncio, httpx ASGITransport
└── typescript/          # Apply to *.ts, *.tsx, *.js, *.jsx
    ├── coding-style.md  # Biome, kebab-case files, React patterns, Zod, shadcn/ui
    ├── patterns.md      # Custom hooks, data fetching, repository pattern
    ├── shadcn-components.md  # shadcn/ui component usage
    └── testing.md       # Vitest for unit tests, Playwright for E2E
  • Common rules apply to all code. Language-specific rules extend them — they don't replace them.
  • When common and language-specific rules conflict, the language-specific rule takes precedence.

MCP Servers

MCP (Model Context Protocol) servers provide tool integrations for AI coding agents. They are configured in two places to support both Claude Code and Codex:

File Agent
.mcp.json Claude Code (Anthropic)
.codex/config.toml Codex (OpenAI)

Both files define the same servers — keep them in sync when adding or removing MCP servers.

Skills

Skills extend AI coding agents with reusable instructions. They live in .claude/skills/ (Claude Code) and .agents/skills/ (Codex) and are tracked via skills-lock.json.

# Add a skill
npx skills add shadcn/ui

# List installed skills
npx skills list

# Update skills
npx skills update

# Remove a skill
npx skills remove <skill-name>

Only skills prefixed with local_ (e.g. .agents/skills/local_conventional-commit/) are tracked in git. Externally synced skills are ignored via .gitignore.

Creating a local skill

Local skills are project-specific skills that live in the repository. To create one:

  1. Create a directory in .agents/skills/ with a local_ prefix (e.g. .agents/skills/local_my-skill/).

  2. Add a SKILL.md file with frontmatter and instructions:

    ---
    name: my-skill
    description: Short description of when and how the skill should be triggered.
    ---
    
    # My Skill
    
    Instructions for the agent...
  3. Run ./install-skills.sh to symlink the skill into .claude/skills/.

The local_ prefix ensures the skill is tracked in git and not overwritten by external skill updates.

Install skills

To install skills from the lock file and symlink all agent skills (including local ones) to Claude Code:

./install-skills.sh

About

Repository for team 3M

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages