The operator console is the screen of the people who run a multi-tenant deployment. Operators are not tenant users: their accounts live in the control-plane database, they sign in through their own route, and their tokens are refused by every tenant route. The server side is server/operators.py and server/routes/vendor.py; the page is webapp/operator.html with webapp/js/operator.js. This page explains accounts, sign-in, the routes, the cross-country fleet view, and the audit trail.
In short¶
- The console API exists only in multi-tenant mode, under
/api/v1/vendor(src/cluster_erp/server/app.py:1440-1450,src/cluster_erp/server/routes/vendor.py:68-70). - Operator accounts live in
control_operators; the first one is created at start-up fromvendor_admin_usernameandvendor_admin_passwordwhen none exists (src/cluster_erp/core/tenancy.py:132-154,src/cluster_erp/server/operators.py:56-75). - Sign-in requires the password and, with
vendor_require_totpon, an authenticator code (src/cluster_erp/server/operators.py:114-175,src/cluster_erp/config/settings.py:131). - Operator tokens carry the audience
vendorand last a fixed number of hours; tenant tokens and operator tokens are refused at each other's routes (src/cluster_erp/server/operators.py:1-12,src/cluster_erp/server/operators.py:209-235). - Every change an operator makes to a customer is written to
control_operator_actions(src/cluster_erp/server/operators.py:238-257).
How it works¶
Accounts¶
| Command | Effect | Source |
|---|---|---|
python -m cluster_erp.server.operators add <username> |
Create an operator | src/cluster_erp/server/operators.py:78-90 |
python -m cluster_erp.server.operators list |
List operators | src/cluster_erp/server/operators.py:323-366 |
python -m cluster_erp.server.operators disable <username> |
Deactivate an operator | src/cluster_erp/server/operators.py:93-103 |
python -m cluster_erp.server.operators totp-enroll <username> |
Issue the authenticator seed | src/cluster_erp/server/operators.py:178-195, src/cluster_erp/server/operators.py:352-353 |
The seed is stored sealed; see second factor and secret storage.
Sign-in and requests¶
POST /api/v1/vendor/login takes username, password, and an optional otp_code (src/cluster_erp/server/routes/vendor.py:101-138). authenticate returns one of five outcomes: ok, invalid, locked, totp_required, or totp_not_enrolled; an unknown name and a wrong password give the same answer (src/cluster_erp/server/operators.py:104-128). A wrong code counts as a failed attempt, and repeated failures lock the account for a period (src/cluster_erp/server/operators.py:110-111, src/cluster_erp/server/operators.py:147-152). Every other route depends on require_operator, which reads a Bearer token with the vendor audience and loads the active operator (src/cluster_erp/server/routes/vendor.py:84-96).
The setting vendor_allowed_ips limits the console API to listed addresses or networks; it applies before sign-in (src/cluster_erp/server/routes/vendor.py:38-70, src/cluster_erp/config/settings.py:134). When it is empty, the server logs a warning at start-up (src/cluster_erp/server/app.py:129-134).
Routes¶
Fifteen routes, counted from the @router decorators in server/routes/vendor.py:
| Route | Purpose | Source |
|---|---|---|
POST /login |
Operator sign-in | src/cluster_erp/server/routes/vendor.py:113 |
GET /me |
The signed-in operator | src/cluster_erp/server/routes/vendor.py:141 |
GET /_meta |
Column and action metadata for the page | src/cluster_erp/server/routes/vendor.py:149 |
GET /overview |
Counts across tenants | src/cluster_erp/server/routes/vendor.py:332 |
GET /tenants |
Tenants with membership, provisioning state, region, schema revision | src/cluster_erp/server/routes/vendor.py:394 |
GET /fleet |
Minimised tenant list for the cross-country view | src/cluster_erp/server/routes/vendor.py:456 |
GET /tenants/{key} |
One tenant with database size and latest backup | src/cluster_erp/server/routes/vendor.py:497 |
GET /signups |
The signup queue | src/cluster_erp/server/routes/vendor.py:540 |
GET /actions |
The audit trail | src/cluster_erp/server/routes/vendor.py:570 |
POST /tenants/{key}/membership/extend |
Extend the paid period | src/cluster_erp/server/routes/vendor.py:607 |
POST /tenants/{key}/membership/suspend |
Suspend membership and tenant | src/cluster_erp/server/routes/vendor.py:626 |
POST /tenants/{key}/membership/activate |
Reactivate | src/cluster_erp/server/routes/vendor.py:639 |
POST /tenants/{key}/refresh-seeds |
Refresh the tenant's seed catalogue in the background | src/cluster_erp/server/routes/vendor.py:652 |
POST /tenants/{key}/backup |
Take a backup in the background | src/cluster_erp/server/routes/vendor.py:690 |
GET /tenants/{key}/backups |
List the tenant's backups | src/cluster_erp/server/routes/vendor.py:740 |
One console across countries¶
The page can show the tenants of several country nodes together. The page itself asks every node listed in its metadata; there is no central copy. The /fleet route returns only the fields in _FLEET_FIELDS, and the server allows cross-origin calls to that one route only from the origins in console_origins, without credentials (src/cluster_erp/server/app.py:1354-1389, src/cluster_erp/server/routes/vendor.py:443-494, src/cluster_erp/config/settings.py:115-122). Each read of the fleet is recorded as an action (src/cluster_erp/server/routes/vendor.py:478-488).
The page¶
webapp/js/operator.js renders four views (overview, tenants, signup queue, audit trail) from GET /vendor/_meta and knows only column kinds, never specific columns (webapp/js/operator.js:1-8). The operator's token is kept in the browser tab's session storage, and signing out clears the tokens of every country node (webapp/js/operator.js:17-52). All page text comes from the operator keys of the language files, for example Operator Console (webapp/i18n/en.json key operator.title). The web app folder is served under /app (src/cluster_erp/server/app.py:1633-1643).
Why it is built this way¶
- Operators and tenant users are separate worlds so that they cannot be confused by accident or on purpose (
src/cluster_erp/server/operators.py:1-8). - Every operator action is recorded so that a change to a customer is never anonymous (
src/cluster_erp/server/operators.py:10-12). - The cross-country view runs in the browser because a customer's data stays in that customer's country, with no central copy (
src/cluster_erp/server/app.py:1354-1361). - Screens are metadata so that a new column or action is one server-side line (
src/cluster_erp/server/routes/vendor.py:6-8).
Rules and boundaries¶
- Membership actions share code with the command line in
server/membership.py(src/cluster_erp/server/operators.py:260-320). See multi-tenancy. - The control-plane retention pass deletes old operator actions and anonymises long-disabled operator accounts after
control_actions_retention_years(src/cluster_erp/server/control_retention.py:137-159). - The e2e suite is
web-client/_e2e_operator_console.mjs.
Interaction with other parts¶
- Signup fills the signup queue.
- Backups and restore and tenant lifecycle explain the background work the console starts.
Limits and open points¶
- Operator tokens have no revocation list in code;
read_tokenchecks signature, audience, type, and expiry, andrequire_operatorchecks that the account is active (src/cluster_erp/server/operators.py:222-235,src/cluster_erp/server/routes/vendor.py:84-96). - Operator roles or per-operator permissions are not determined from code; every active operator reaches every route.
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| operator | Operator |
A person who runs the deployment, with an account on the control plane. | operatör | src/cluster_erp/core/tenancy.py:132 |
| vendor audience | VENDOR_AUDIENCE |
The token audience that separates operator tokens from tenant tokens. | satıcı hedef kitlesi | src/cluster_erp/server/operators.py:49 |
| operator action | OperatorAction |
One audit record of what an operator did to which tenant. | operatör eylemi | src/cluster_erp/core/tenancy.py:157 |
| fleet view | vendor_fleet |
The minimised tenant list that the console merges across country nodes. | filo görünümü | src/cluster_erp/server/routes/vendor.py:457 |
Related pages¶
- Identity, access, tenancy, privacy, and licensing
- Multi-tenancy
- Second factor and secret storage
- System endpoints
Verified against commit 92f8051 on 2026-11-13.