Skip to main content
The bridge ships a built-in operator console at /console — a single page served by the service itself, no separate frontend deployment. Open it, paste the process API key (kept for the browser tab only, in sessionStorage), and connect.

Finding your way

The console is one application shell in Engrate’s design language: a sidebar on the left with every page in a named group, the page itself on the right, and a top bar with the service state, the Engrate environment, Start/Stop and the key.
  • Jump anywhere with ⌘/Ctrl+K (or Springe zu … at the bottom of the sidebar): type a few letters, Enter opens the page.
  • Light and dark. The console follows the system until Darstellung switches it; the choice is remembered per browser.
  • Every screen. On a desktop the sidebar can be folded to an icon rail (the button next to the name); on a tablet it is the rail, and the menu button expands it over the page; on a phone it slides in from the left. Pages with a form and a result — Zuordnung, PARTIN, Bestellungen — show both side by side where there is room and stacked where there is not.
  • The last page, the folded sidebar and the theme are remembered per browser; the Marktrollen view per tab.
The page loads nothing from anywhere else: its two typefaces (Work Sans and IBM Plex Mono, the Latin subsets) are served by the service under /console/static/, and the content security policy allows exactly that.

Fast by design

A load of the console costs round trips, not bytes - so the service is built to need as few as possible (console_assets.py):
  • Addressed by content. The two scripts and the three fonts are /console/static/<name>.<digest>.<ext>, cached for a year as immutable. The page itself has a fixed URL and is revalidated: an ETag and Cache-Control: no-cache, so a browser that has it asks once and gets a 304. A repeat load is one small round trip for the page and nothing else; a release changes the digests and the browser fetches exactly what changed (/console/app.js, the script’s fixed name, still answers, revalidating). The page holds no secret - the key is typed into it - which is why it may be cached at all.
  • Compressed once. Page and scripts are hashed and compressed while the process starts - zstd where the interpreter has it (Python 3.14), gzip always - and served by Accept-Encoding (the page: 107 KB → 24 KB gzip; the script 192 KB → 53 KB). Fonts are Brotli inside and left alone. The JSON the console fetches is gzipped on the way out as well, so the 900-row views travel at a fraction of their size; the event stream is never compressed or buffered.
  • The first paint is the right one. A 400-byte boot script in the head, before any paint, puts the stored theme and page on <html>; every page but the overview starts hidden in the markup; the console script is fetched from the head while the page still parses and runs, deferred, once it is parsed. Nothing flashes the system scheme or the overview first.
  • Live means live. The event stream no longer reads the log every two seconds: an emit wakes every open stream, from whichever thread emits, and the event is in the browser within a frame. An idle stream costs nothing but a keep-alive every fifteen seconds.
  • No layout shift. The two faces the first paint needs are preloaded, and each has a fallback face (Arial / Liberation Sans, Courier New / Liberation Mono) with size-adjust and the ascent, descent and line-gap overrides measured from the font files, so text set before a font arrives occupies the same lines and widths.
  • Cheap frames. Surfaces are solid and bordered, not blurred or shadowed - a traced page switch spent a third of its time re-rasterising a blurred top bar and the shadow under a full-page card - and pages swap in the same frame. Events arrive in bursts (the snapshot on connect is hundreds at once) and are written in one frame: one fragment, one trim, one scroll. Rows of the long lists are laid out and painted only when scrolled to (content-visibility); a filter typed into re-renders its list once per painted frame, not once per keystroke. The status poll pauses while the tab is hidden, runs at once when it is shown again, asks for the delivery days only while the overview shows, and redraws nothing whose data did not change. A list being refreshed keeps its rows, dimmed, until the new ones arrive. The registry of a thousand Netzbetreiber loads with the first page that needs it, not with every connect.

What it does today

Live event stream

Every pipeline action — forecast received, positions aggregated, CSV published, schedules submitted, orchestration errors, config changes, start/stop — appears live via Server-Sent Events, with history on load.

Runtime configuration

Change sFTP host/credentials, Engrate settings, home control area, active areas, EIC identifiers and more — validated, applied without a restart, persisted across restarts, secrets write-only.

Start / Stop

Stop pauses the orchestrator (scheduled runs become no-ops); manual process steps stay available as an operator override. State survives restarts and is visible in the status pill.

Delivery-day overview

Per-day pipeline state (aggregated / published / submitted), including whether a step is stale because newer input arrived.
Fälle panel. The inbox: the bridge’s exceptions as cases with an outcome, in six views - see Cases; on every open case a tool to propose and the proposals waiting for their people - see Tools. Agenten panel. Every routine of the orchestrator as a process agent: its triggers, skills, autonomy and runs, switchable and editable - see Process Agents. Marktpartner panel — order. The partner list can be ordered two ways. Status, dann Name (the default) puts rejections first, then the ladder down to “kein Kontakt”. Letzter Kontakt – neueste zuerst orders each control area by the newest contact on either channel, in either direction — EDIFACT sent or received, e-mail sent or answered — and puts partners without any contact last; every row shows that date and names the contact on hover. The order is fetched from the server (?sort=kontakt), which decides “newest” on instants across sources with different offsets; the browser never re-sorts the strings. The choice is remembered per browser. Fahrplan/Ist panel. A delivery day’s schedule against what the market measured, from GET /v1/process/fahrplan-ist, for Germany or one Regelzone:
  • Chart: the Fahrplan and the Ist step by step per quarter-hour. Orange areas are short (more consumed than scheduled), blue areas long. The checkbox adds the net series of each Kategorie: A from the grid operator, monthly; B from the ÜNB, monthly; C from the ÜNB, daily.
  • Which Ist: the same one as the app’s Plan gegen Ist. It is Kategorie A plus B, or A plus C until B has arrived.
  • Which Fahrplan: what the TSO confirmed, where a confirmation is in. A trade series the TSO changed moves the Regelzone’s Fahrplan by the change, and a note under the chart names the report and the amount (see Fahrplanbestätigung).
  • Costs: below the chart, the reBAP from the BIKO’s PRICAT 27001 (EUR/MWh) and the cost per quarter-hour, which is the deviation energy times the reBAP. Orange is to pay, blue a credit. The reBAP is one price for all of Germany, so a Regelzone without its own price list takes another’s. Where no price list has a quarter-hour, the reBAP published on netztransparenz.de stands in, then the reBAP rebuilt from the market data, then the TSOs’ AEP-Schätzer (operational, every 15 minutes), then the reBAP forecast; on the last three the cost reads “geschätzt”, and the hint says which quarter-hours came from where.
  • reBAP forecast (GET /v1/process/rebap?day=, default tomorrow): per quarter-hour the mean of the same wall-clock quarter-hour over the latest 28 days with a published reBAP before the day, working days apart from weekends and nationwide holidays (all days while fewer than 4 of the kind are there), with P10 and P90. The history comes from netztransparenz.de: the orchestrator reads the quality-assured data points (reBAP, AEP modules 1-3, VoAA, NRV-Saldo) of the missing days every 6 hours and the operational ones (AEP-Schätzer, NRV-Saldo, ID AEP, spot price) every hour; POST /v1/process/rebap/aktualisieren?tage= does both at once. It needs NETZTRANSPARENZ_CLIENT_ID and NETZTRANSPARENZ_CLIENT_SECRET, or, on a Mandant, the Zentrale, whose history it copies instead.
  • reBAP rebuild (GET /v1/process/rebap/pruefung?von=&bis=): the TSOs’ model description since 01.11.2023 - maximum of modules 1-3 when the NRV-Saldo is short, minimum when long, module 2 at zero. Module 2 (ID AEP plus a minimum gap of 25 %, at least 10 €/MWh, full from |NRV-Saldo| 500 MW) is rebuilt and checked per quarter-hour against the published one, and the published modules combined against the published reBAP. Module 1 is the ENTSO-E price of the activated balancing energy (A84, realised) in the balance’s direction - up when short, down when long - and the VoAA where nothing was activated in that direction. Where mFRR was activated in that direction too, module 1 mixes both by the platforms’ satisfied demand, which is not published: such a quarter-hour is not rebuilt (the check counts it as “nicht berechnet”, the AEP-Schätzer stands in); module 3 the published one, else the scarcity component of the operational NRV-Saldo. The whole reBAP rebuilt from these comes right after the published one, before the AEP-Schätzer; the check compares module 1 and the whole reBAP too. Needs ENTSOE_API_TOKEN.
  • Kalkulation: a table for the delivery day and the month up to it (GET /v1/process/kalkulation?von&bis, at most 31 days), in kWh, € and ct/kWh:
    • Spot bought and sold: the Fahrplan on the day-ahead market at the spot price (ENTSO-E, DE-LU, quarter-hourly; netztransparenz’s later where ENTSO-E has none). Net consumption is bought, net production sold.
    • Ausgleichsenergie: kWh short and long, and the cost (above); “erwartet” while the reBAP is rebuilt or estimated.
    • Total cost: both together.
    • Delivered to customers: the Ist, the Fahrplan where it has not arrived, and its value at the quarter-hour’s spot price.
    • Mehrkosten (total cost minus that value) and the Aufschlag per kWh that covers them. Customers’ cost per kWh is their spot price plus this Aufschlag. The Mehrkosten equal the deviation times reBAP minus spot.
    • With the customers’ contractual Aufschlag set (KUNDEN_AUFSCHLAG_CT_KWH, console group “Kalkulation”; the tariff is the quarter-hour spot price plus it): their revenue (spot value plus Aufschlag) and the margin (revenue minus total cost), in € and ct/kWh.
    • Germany is the sum of the Regelzonen, each with the Ist it has.
  • Bilanzierungsgebiete: a table of the Ist per area, from Kategorie A (Z97) and Kategorie B per area (Z99).
  • Incomplete days: until the Ist is complete, the sums cover only the quarter-hours compared, and the cost reads “bisher”.
  • Hover: shows every value of one quarter-hour.
Bestellungen panel. The BKV’s three MaBiS orders — the aggregation level RZ (17207), the Bilanzkreiszuordnungsliste (17203) and the Clearingliste BAS (17204). Pick the order, the Regelzone and the kind; only the fields that order needs are shown, and empty ones take the server’s defaults (the ÜNB or BIKO from the BDEW register, the main balance group, the activated BAS). Vorschau renders the ORDERS and names the receiver and, for a level change, the send deadline; Senden previews again and asks for confirmation with exactly that before anything goes out. Below, every order sent with its CONTRL/APERAK and the answer (UTILMD 55067/55070 or the ORDRSP refusal) and a status chip. Zeitreihen panel. Which of our Zuordnungsermächtigungen the market has turned into sum time series, per Regelzone: the BIKO on top, below it the ÜNB and every Netzbetreiber with their Bilanzierungsgebiete — each series under the party responsible for aggregating it — a chip per Zeitreihentyp, dimmed until activated, red with ! for an activation we never asked for, filled once MSCONS values arrived — click a filled chip for the time-series chart. Where the customer has confirmed customers, a badge shows how many and from when, and every series they need that is not active by their first supply start is hatched red — including Bilanzierungsgebiete we never granted a ZOE. On top, the Bilanzkreise: the BKV with its EIC and MP-ID, its main balance group and, below it, each Unterbilanzkreis with EIC, Regelzonen, Zeitreihentypen and start on a line of their own (a disabled one struck through with the reason). Small role icons — BKV, Hauptbilanzkreis, Unterbilanzkreis, BIKO, ÜNB, Netzbetreiber, explained in the legend under the summary — mark the rows; every node of an Unterbilanzkreis carries its icon, EIC and name (the name gives way in a narrow column, the EIC never), each Regelzone’s head the EICs of the Bilanzkreise in it (one not valid yet dashed, with its start), a node holding only another Bilanzkreis’ activations that one’s EIC in a dashed tag without icon, and the filter matches EICs — the activations’ too. See Zuordnungsermächtigung – Activations.

The console API

Everything the page does goes through JSON endpoints (scope: process), so it can equally be scripted:

Forecast qualities (tab Prognosen)

One row per delivery day from a week back to the end of the forecast horizon, with a badge per stored quality: The active version comes first; a faded badge is a version that was replaced and is kept for comparison. Per row: consumption of the active version (production in the tooltip), the confirmed customers in supply, the CSV and schedule state, and — where the customer’s forecast replaced a backup — how far the backup was off in daily energy. A click opens the quarter-hour chart with every version of the day on top of each other.

Marktrollen (button in the header)

The whole window instead of the event stream and the panels: who n:lead deals with, on which contract, for which customers, and which sum time series the market has really activated. Betrieb switches back.
  • Head: the BKV (MP-ID, EIC, the Lieferant’s MP-ID) with its Bilanzkreise (Haupt- and Unterbilanzkreis), and one card per Regelzone with its customers, the series active against those expected and the Bilanzkreisvertrag with its ÜNB.
  • Tree: Regelzone → BIKO (its aggregates on Regelzone level) and ÜNB → Netzbetreiber → Bilanzierungsgebiet. Per row the contracts, the confirmed customers by group (iMSys ¼h, RLM, SLP, TLP), every Zeitreihentyp in the colours of the Zeitreihen tab, and aktiv / erwartet as a bar. nur offene Punkte keeps the rows with a missing series, an unexpected activation or a missing contract.
  • Details: a click on a row shows its master data, the customers by group, the series - and the contracts with a form to record them.
Read from GET /v1/process/marktrollen (the Zeitreihen tree with contracts, customer groups and counts per node); nothing is recorded by reading.

Verträge

What a contract says - its Fassungen - is in Verträge und Fassungen; the button Verträge des BKV & Prüfung in the BKV’s card shows the Lieferant’s and the trader’s contracts and today’s contract check. Each is angefragt, aktiv, gekuendigt - or fehlt where one is expected and nothing is recorded; an expected contract that is not active counts as offen. Where the Lieferant is the BKV, the Zuordnungsvereinbarung is a module of the Lieferantenrahmenvertrag (BK6-20-160): without an entry of its own it follows that contract. The Rahmenvertrag ledger (VNB-Kampagne) is what the Quittungs-Nachfass believes, and a Lieferantenrahmenvertrag there counts as active. Recording one here keeps it in step:
  • aktiv, from today or earlier: recorded there too, with the fixed reason “Verträge (Konsole)” - never the note, because that ledger’s events reach the shared Slack channel. Active from a later day: only here, until then.
  • gekuendigt: withdrawn there.
  • Entry deleted: withdrawn there only if this console put it there; one the VNB-Kampagne recorded stays.
  • Where the two disagree - the campaign sync changed it, or a contract is active from a later day - the contract shows ! and says why.
The notes and states of contracts stay in the bridge: they are not part of the Slack interaction log. If MakoFlow cannot be read, the view lists the contracts alone, and they can still be recorded. Recording a contract sends nothing to anyone.

How runtime config works

Console changes are kept as an overlay on top of the environment variables, persisted to runtime_config.json in the audit directory (the Azure Files share in production — so they survive restarts and scale-to-zero). The config panel marks overridden fields with a runtime badge. Environment variables remain the base; deleting the overlay file reverts everything to env values. The Lieferant tab also holds the document pool of the Lieferantenrahmenvertrag. Its files are uploaded there and take effect at once; they are not part of the overlay. API keys are deliberately not editable via the console — rotating them stays an infrastructure operation. See Security for how the stream token works and what the console records.

Events on disk

Events are appended to events.jsonl next to the audit trail — the stream you see in the console is the same file, so it doubles as a permanent operations log. events.meta.json beside it records the next event id and the log’s epoch. If the log is ever truncated, restored from a copy or replaced, ids still never repeat, and the epoch changes so the operator app starts its feed afresh. Leave the file alone; deleting it only gives up that guarantee until the next event.

Interaction log (Slack transparency channel)

GET /v1/process/protokoll?since=…&until=… returns every interaction of the bridge in a window of at most 31 days, oldest first, from four sources:
  • the event log, read in full (the console API only serves the latest 1,000 events);
  • the EDIFACT exchange in both directions: what each message carried (inhalt, e.g. 55062 Aktivierung SLS ab 01.11.26 · 11YN10001846-01S (EWE NETZ GmbH)), and for an acknowledgment its verdict, grund, bezug and what that message carried (bezug_inhalt);
  • the mail ledgers: hand-written mails with their full text, Quittungs- Nachfass, corrections, Stammdaten requests;
  • Erstkontakt mails, their replies and the Wiedervorlagen.
Each entry has ts (UTC), quelle, typ, richtung (aus/ein/ intern), kanal, mp_id, partner, titel, text, referenz and a stable id. The Slack channel shared with the customer is filled from it, each entry posted exactly once, so the customer’s team can follow and learn from every contact. Housekeeping events (API calls, restarts) are left out. A naive since/until is read as German legal time.

Posting to Slack

The bridge posts new interactions itself, through a Slack incoming webhook. The customer’s team is external to the workspace and reads along via Slack Connect; an incoming webhook of an app in the host workspace may post into such a shared channel.
  1. In Slack: create an app (“From scratch”) in the workspace, enable Incoming Webhooks and add one for the shared channel.
  2. In the console: Konfiguration → Slack-Protokoll, paste the URL into slack_protokoll_webhook_url (stored as a secret, never shown again), and set slack_protokoll_start to where the feed should begin.
  3. Check with POST /v1/process/protokoll/slack {"dry_run": true}, which returns the messages without posting.
From then on the orchestrator posts every slack_protokoll_intervall_minuten (default 60): one message per run, “Protokoll 10:04-10:58 · n Interaktionen”, one line per interaction, hand-written mails and call notes in full as a quote. Events that only repeat an EDIFACT or mail entry are left out. The ledger slack_protokoll keeps the high-water mark and moves only after Slack accepted a message, so an outage delays the log and loses nothing. An EDIFACT line says what the message carried, and its acknowledgments in the same run join it as A✅ (APERAK) or C✅ (CONTRL), a negative one with its reason:
An acknowledgment whose message was posted in an earlier run stands on its own line and names what it acknowledges, as far as the message is within the search window. Known cases: UTILMD 55062/55063 (Aktivierung/Deaktivierung MaBiS-ZP), 55071/55072 (ZOE, with Bilanzierungsgebiet, Zeitreihentypen and a sub-balance group by name), PARTIN by role, ORDERS by list and subscription, PRICAT 27001; any other case shows its PID. Phone calls and decisions are recorded with POST /v1/process/protokoll/notiz (text, mp_id, art such as Telefonat, person) and appear in the log and the channel.

Planned (not in this draft)

  • Configurable communication channels (sFTP, AS4, e-mail, API push)
  • Configurable order of pipeline steps
  • Message / file format configuration