Skip to content

Development

Prerequisites

  • Python >= 3.12 and uv
  • task (go-task)
  • PostgreSQL at localhost:15432 — to run the service. The test suite needs no database; see Testing.

Environment Variables

Variable Default Description
DATABASE_URL postgresql+asyncpg://...host.docker.internal:15432/celine_webapp PostgreSQL async URL
DATABASE_ECHO false Log SQL statements
DIGITAL_TWIN_API_URL http://host.docker.internal:8002 Digital Twin service URL
NUDGING_API_URL http://host.docker.internal:8016 nudging-tool service URL
FLEXIBILITY_API_URL http://host.docker.internal:8017 flexibility-api service URL
REC_REGISTRY_URL http://host.docker.internal:8004 rec-registry service URL
SMART_METER_API_URL Optional smart meter API URL
NUDGING_INGEST_SCOPE nudging.ingest OAuth2 scope for nudging ingest calls
POLICY_VERSION 2024-01-01 Current terms version string
JWT_HEADER_NAME x-auth-request-access-token Header carrying the bearer token
CORS_ORIGINS ["http://localhost:5173"] Allowed CORS origins
CELINE_OIDC_* (from celine-sdk defaults) OIDC settings — issuer, JWKS URI, audience

Data sharing

Off by default. The section is hidden in the app and the routes answer 404 until the dataspace is deployed, because a sharing screen that cannot record a decision is worse than no screen.

Variable Default Description
DATA_SHARING_ENABLED false Master switch for the whole surface
IDENTITY_REGISTRY_URL http://host.docker.internal:30005 Resolves the member's DID and credential
DS_CONNECTOR_URL http://host.docker.internal:30001 Holds the consent records
DS_PROVENANCE_URL http://host.docker.internal:30000 Serves the member's own history
DS_NS_URL Vocabulary namespace
DS_RESOLVE_CLIENT_ID svc-celine-webapp Service account, used only to resolve a credential
DS_RESOLVE_CLIENT_SECRET Secret for the above

The routes stay off unless DATA_SHARING_ENABLED is true and both IDENTITY_REGISTRY_URL and DS_CONNECTOR_URL are set.

Backend Setup

uv sync
uv run alembic upgrade head
task run
# Listens on http://localhost:8014

Taskfile Commands

Command Description
task setup uv sync
task test Run the test suite
task run Start dev server on port 8014
task debug Start with debugger
task migrate Apply all pending migrations
task migrate:create Create a new Alembic migration
task release Run semantic-release

Frontend

The participant frontend is in celine-frontend apps/webapp. See that repository for frontend setup instructions.

Calling the API locally

In local dev the BFF reads X-Auth-Request-Access-Token, the header oauth2-proxy injects in deployment. Authorization: Bearer <token> works as a fallback.

curl http://localhost:8014/api/me \
  -H "X-Auth-Request-Access-Token: <token>"

The token must be a real one from Keycloak. It is verified in full here — the signing key is resolved from the realm's JWKS by the token's kid, the signature is checked, and exp and iss are enforced. There is no unverified path and no development bypass, so a hand-assembled or alg: none token will be rejected.

A 401 accompanied by this log line is the usual result, and it means the token, not the network:

Failed to fetch signing key from …/protocol/openid-connect/certs:
  Unable to find a signing key that matches: "None"

CLI

celine-webapp-export-feedback   # Export user feedback data

Database Migrations

uv run alembic upgrade head
uv run alembic revision --autogenerate -m "add preference column"

Testing

task test              # the whole suite
task test -- -x -q     # arguments pass through to pytest

The suite needs nothing running — no PostgreSQL, no Keycloak, no upstream service. It runs on a clean checkout and in CI, and a test that needs a live service does not belong in it.

That is achieved at two seams, and both matter when you write a test:

  • Identity is real; only the JWKS fetch is faked. Tests sign genuine RS256 tokens with a throwaway key, and the service verifies them exactly as it does in production — signature, expiry, issuer. Nothing overrides the authentication dependency, so a test cannot accidentally prove that a token this service should reject is accepted.
  • The database is SQLite, one file per test. Production is PostgreSQL, so a schema change is not verified by a green run alone — exercise the migration separately.

What the suite does and does not tell you

This service is a backend-for-frontend: most of what it does is fan out to the Digital Twin, rec-registry, flexibility-api and nudging-tool through celine-sdk, then compose the results. Those four are replaced by fakes in tests/fakes.py, which reproduce the shape the SDK returned when they were written.

So a green suite says the composition logic is correct against that shape. tests/test_sdk_contract.py checks the fakes against the installed celine-sdk models, which catches drift in the package — but not that a deployed upstream serves what its own SDK describes. A celine-sdk version bump can still change this service's behaviour with no file here changing. Treat an SDK bump as a change to this service rather than as dependency maintenance.

Layout

File Covers
tests/test_auth_boundary.py what is accepted as an identity, and what is rejected
tests/test_overview_fanout.py /api/overview — aggregation, trend building, degradation
tests/test_gamification_fanout.py /api/gamification — season scoring and its fallback
tests/test_nudging_fanout.py /api/settings and /api/notifications
tests/test_sdk_contract.py that the fakes still match the installed celine-sdk models
tests/test_data_sharing.py the data-sharing surface, dataspace stubbed
tests/test_api.py, tests/test_forecast.py pure mapping and window functions
tests/fakes.py the four upstream fakes

If you add a field to a fake, assert it in test_sdk_contract.py in the same change. The fakes are written by hand from reading route code, so an invented attribute will reproduce whatever you expect of it — that is not a hypothetical, it produced a fully reproducible and entirely wrong bug report the day the fakes were written.

Tests marked xfail with strict=True are known defects, pinned deliberately, each with its reason in the marker. Fixing one turns the run red with XPASS(strict) — the signal to remove the marker in the same change. There are none at present.

Project Layout

src/celine/webapp/
  main.py                # FastAPI app factory
  settings.py            # Pydantic settings
  routes.py              # Router registration
  cli.py                 # CLI (celine-webapp-export-feedback)
  api/
    user.py              # /api/me, /api/terms/accept
    overview.py          # /api/overview
    weather.py           # /api/weather
    forecast.py          # /api/forecast
    community.py         # /api/community
    suggestions.py       # /api/suggestions, /api/commitments
    gamification.py      # /api/gamification
    co2_settings.py      # /api/settings/co2
    settings_routes.py   # /api/settings
    notifications.py     # /api/notifications, webpush
    feedback.py          # /api/feedback
    data_sharing.py      # /api/data-sharing
    meta.py              # /health
    deps.py              # FastAPI dependencies — every outbound client is resolved here
    schemas.py           # Pydantic schemas
  services/
    data_sharing.py      # Dataspace calls (identity registry, connector, provenance)
  db/
    models.py            # SQLAlchemy ORM models
    session.py           # Async session management
    user_settings.py     # User settings helpers
alembic/                 # Database migrations
tests/                   # See Testing above