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/weatherreturns current weather conditions for the user's community location via the Digital Twin.GET /api/forecastreturns energy production/consumption forecasts.
Flexibility Suggestions
The suggestions system integrates with the flexibility-api and Digital Twin to present energy flexibility opportunities:
GET /api/suggestionslists active flexibility windows with available actions.- Users can accept or reject suggestions via
POST /api/suggestions/{id}/respond, which creates commitments in the flexibility-api. - Users can schedule reminders via
POST /api/suggestions/{id}/remind, which delegates to the nudging-tool via the Digital Twin. - 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:
- Frontend requests the VAPID public key from
GET /api/notifications/webpush/vapid-public-key. - Browser subscribes using the Web Push API (
PushManager.subscribe). - The subscription endpoint is registered via
POST /api/notifications/webpush/subscribe. - The BFF stores the subscription in the nudging-tool service.
- 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.