Skip to content

Features

Terms Acceptance

Terms acceptance is enforced at the frontend layout level. On every page load, the frontend calls GET /api/me. If the response has terms_required: true, the user is redirected to a terms page. Acceptance is persisted via POST /api/terms/accept.

With a legal host (LEGAL_BASE_URL) and a known community, the gate is per document of the member's own community (celine/webapp/legal.py): the terms and the privacy notice, read from <LEGAL_BASE_URL>/<community>/current.json. /api/me returns legal_documents (each with its url, version, title and required), and the frontend posts back the documents and versions it showed. Each acceptance is a row of its own (community, document, version, language, url, sha256). A member is asked again for one document when its version changes, without being asked about the other. The version recorded is the server's; a stale page gets 409. Before the host has ever answered, a document is accepted without a version, and the date decides against the version's published_at once it is known. current.json is refreshed every five minutes, and the last good copy is kept while the host is down.

The terms' clauses. When the terms' text carries a specific approval of its onerous clauses (declarations.<lang>.specific_approval in current.json), the gate asks it as a document of its own, terms_clauses: the terms' url, version and sha256, its own row. It is a separate act (Art. 1341(2) c.c.): the frontend shows it as its own unticked box, worded by the text (declaration), and POST /api/terms/accept refuses with 422 a request that does not name it while it is required. A document whose text carries a general declaration (the terms' declarations.<lang>.general) has it in declaration too, shown as its box's words. No words, no box: without them terms_clauses is not asked. A member who accepted the terms before the box existed is asked the clauses alone.

Accepted once, at application. On a miss — a document the member has no current acceptance of — and with ONBOARDING_API_URL set, the gate asks onboarding (GET /api/me/acceptances, the member's own token) what the member accepted when they applied. An acceptance whose version and sha256 match the text served now, in any of its languages, is copied as a row with source onboarding and the submission id in source_ref, and is not asked. Anything else is asked: another version or text, no sha256, onboarding refusing, down or unset, or no application. Onboarding's answer is kept for five minutes per member, so a deployment without onboarding does not wait on it at every request.

Otherwise (no legal host, or the member's community is unknown) the gate is the deployment-wide POLICY_VERSION, as before: raising it asks every member again.

Overview and Energy Data

The overview page displays energy data fetched from the Digital Twin service: - Community-level production and consumption totals - Participant-level meter readings - Incentive calculations (Italian REC rules: GSE incentives)

Data is fetched server-side by the BFF, which authenticates with the Digital Twin using the user's access token.

Weather and Forecast

  • GET /api/weather returns current weather conditions for the user's community location via the Digital Twin.
  • GET /api/forecast returns energy production/consumption forecasts.

Flexibility Suggestions

The suggestions system integrates with the flexibility-api and Digital Twin to present energy flexibility opportunities:

  1. GET /api/suggestions lists active flexibility windows with available actions.
  2. Users can accept or reject suggestions via POST /api/suggestions/{id}/respond, which creates commitments in the flexibility-api.
  3. Users can schedule reminders via POST /api/suggestions/{id}/remind, which delegates to the nudging-tool via the Digital Twin.
  4. Active commitments can be cancelled via DELETE /api/commitments/{id}.

Gamification

GET /api/gamification aggregates data from multiple services: - Season points from the Digital Twin (rec_points_leaderboard, rec_participant_points), not from the flexibility-api — its settlement figure omits the baseline comparison and inflates the value - Badges awarded for achievements, stored locally - The member's own anonymous season ranking, from the Digital Twin

Points and the level ladder are scoped to the current season, so the ladder resets each season. Where the season data is unavailable — an older Digital Twin, or a device not yet in the fleet — the endpoint falls back to an all-time total and shows no ranking at all. The response does not say which mode produced it.

GET /api/gamification/history returns the user's commitment history from the flexibility-api, with settled reward points cross-referenced against the Digital Twin's figures.

CO2 Reporting

GET /api/settings/co2 returns carbon emission factors and configuration.

Community

GET /api/community returns community metadata from the rec-registry, including name, description, areas, and links.

The legal links (terms_url, privacy_url, statute_url, regulations_url) follow one rule (celine/webapp/legal.py): 1. the community's own registry link, when it is set; 2. otherwise, when LEGAL_BASE_URL is set, the legal host's address for that document, <LEGAL_BASE_URL>/<community>/<slot>/ (slots terms, privacy, statute, regulations); 3. otherwise none.

Notifications

The notification system proxies the nudging-tool: - List notifications (no filtering is exposed through this API) - Mark individual or all notifications as read - Enable/disable notifications per user

Web Push (VAPID)

Setup flow:

  1. Frontend requests the VAPID public key from GET /api/notifications/webpush/vapid-public-key.
  2. Browser subscribes using the Web Push API (PushManager.subscribe).
  3. The subscription endpoint is registered via POST /api/notifications/webpush/subscribe.
  4. The BFF stores the subscription in the nudging-tool service.
  5. Nudging events trigger push deliveries through the registered endpoint.

Actual push delivery is handled by the nudging-tool service, not the BFF.

Feedback

Users can submit feedback via POST /api/feedback. Each entry is bound to the REC resolved from the authenticated participant's registry membership and may include a screenshot. Authorized REC managers can review the same entries, retrieve their screenshots, and advance them through new, seen, and resolved; the manager response omits stored identity and network diagnostics. Feedback data can also be exported using the celine-webapp-export-feedback CLI tool.

Data Sharing

A member's own decisions about sharing their energy data, at GET /api/data-sharing and POST /api/data-sharing/{offer_id}, with their history at /api/data-sharing/history.

Off by default (DATA_SHARING_ENABLED): while the dataspace is undeployed the routes answer 404 and the app hides the section, because a sharing screen that cannot record a decision is worse than no screen.

The work happens in onboarding. It owns the member's dataspace identity, resolves their credential, merges the offers their community publishes against the decisions the connector holds, and serves their history. These routes forward the member's own token to it and pass the answer back, because the browser talks only to this service.

The offers a member sees are read from the published vocabulary on each request rather than from a local copy — two copies of the text somebody agrees to is how the thing displayed and the thing recorded drift apart. An offer may carry text, the community's own wording per locale, which onboarding attaches only when it was written for the offer's current version (onboarding's docs/data-sharing.md). It is forwarded unchanged; the page shows it in the member's language and falls back to the generic label.

Withdrawal is the point of the surface. The onboarding wizard can only grant, so without these routes a consent could be given and never taken back. Every call is made with the member's own credential: it is their act, and an administrator cannot perform it for them. This service holds no credential and no service account of its own.

Being asked is this service's half. GET /api/data-sharing carries asked and review_due, so the app can show a first-run sequence to somebody who has never been asked and a "review your settings" reminder to somebody whose decision has gone stale (DATA_SHARING_REVIEW_AFTER_DAYS, 180 by default). A dismissal is recorded through POST /api/data-sharing/seen under the data-sharing key of the table every in-app tour uses, together with the offers (ids and versions) that were on offer; a decision records the same. The reminder is also due, whatever the date, when an offer is new or its version moved since the member was shown it — here, or in the onboarding form, which onboarding reports as presented_version. An offer shown at its current version and not granted was declined, and is not asked about again.

Settings

GET/PUT /api/settings presents one settings object assembled from two owners:

  • This service stores display preferences — simple_mode, font_scale — and web push enablement.
  • The nudging-tool owns the notification limit, the email channel and the per-kind catalogue.

The split is invisible to the app, which reads and writes a single object. Some notification kinds are marked non-editable and cannot be switched off by a member.