### Install and Run Operator UI with NPM
Source: https://github.com/citrineos/citrineos-operator-ui/blob/main/README.MD
Installs dependencies and starts the development server with hot reload for local development without Docker.
```bash
npm install
npm run dev
```
--------------------------------
### Typical Development Workflow
Source: https://github.com/citrineos/citrineos-operator-ui/blob/main/README.MD
A common workflow for developing the Operator UI, involving building CitrineOS Core, installing UI dependencies, and starting the development server.
```bash
cd citrineos-core
npm run build
```
```bash
cd citrineos-operator-ui
npm install
```
```bash
npm run dev
```
--------------------------------
### Start EVerest Simulator
Source: https://github.com/citrineos/citrineos-operator-ui/blob/main/README.MD
Starts the EVerest simulator, which is necessary for bringing a charging station online. This command should be run from the 'citrineos-core/Server' directory.
```bash
cd citrineos-core/Server
npm run start-everest
```
--------------------------------
### Build and Preview Production Application
Source: https://github.com/citrineos/citrineos-operator-ui/blob/main/README.MD
Creates a production build of the application and starts a local server for previewing, typically at http://localhost:3000.
```bash
npm run build
npm run preview
```
--------------------------------
### Running the Application with Docker
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Commands to start the CitrineOS Operator UI using Docker Compose for development or to build the image manually.
```bash
# Start with Docker Compose (connects to CitrineOS Core)
docker compose up -d
# Build image manually
docker build -t citrineos-operator-ui .
# Local development with separate core instance
cp docker-compose-local.yml docker-compose.override.yml
docker compose up -d
```
--------------------------------
### Run Operator UI in Development Mode
Source: https://github.com/citrineos/citrineos-operator-ui/blob/main/README.MD
Starts the application with hot reload enabled, accessible at http://localhost:3000 by default.
```bash
npm run dev
```
--------------------------------
### Perform GET, POST, and DELETE requests using BaseRestClient
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Examples of making GET, POST, and DELETE requests to the CitrineOS Core API using the BaseRestClient. POST requests can include complex payloads for OCPP commands.
```typescript
// GET request
const status = await client201.get('/stations/cp001/status', {});
// POST request — send OCPP command to station
const result = await client201.post(
'/remotestart',
{},
{
identifier: 'cp001',
tenantId: '1',
remoteStartRequest: {
idToken: { idToken: 'DEADBEEF', type: 'ISO14443' },
evseId: 1,
},
},
);
// DELETE request
await dataClient.del('/locations/5', {});
```
--------------------------------
### Running the Application with NPM
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
NPM commands for installing dependencies, running the development server with hot reload, building for production, and performing linting or cleaning build artifacts.
```bash
npm install
npm run dev # http://localhost:3000 with hot reload
npm run build # production build
npm run start # start production server
npm run lint # ESLint check
npm run lint-fix # auto-fix + Prettier
npm run fresh # clean all build artifacts and node_modules
npm run fi # fresh + npm install
```
--------------------------------
### OCPP 1.6 Command Registry and Usage
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Imports and uses functions from the OCPP 1.6 command registry to get command keys and specific command details. Includes an example of rendering the Modals component.
```typescript
// src/lib/client/components/modals/1.6/commands.registry.ts
import {
OCPP1_6_COMMANDS_REGISTRY,
getOCPP16CommandKeys,
getOCPP16Command,
} from '@lib/client/components/modals/1.6/commands.registry';
// Available commands:
// 'Change Availability', 'Data Transfer', 'Change Configuration',
// 'Get Configuration', 'Get Diagnostics', 'Trigger Message', 'Update Firmware'
const commandKeys = getOCPP16CommandKeys();
// ['Change Availability', 'Data Transfer', 'Change Configuration', ...]
const cmd = getOCPP16Command('Trigger Message');
// { displayName: 'Trigger Message', modalType: ModalComponentType.triggerMessage16 }
// Render the command modals panel (inside a charging station detail page):
import { Modals } from '@lib/client/components/modals';
```
--------------------------------
### Health Check API Endpoint
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Example of how to call the GET /api/health endpoint to check the application's liveness status. No authentication is required.
```bash
curl http://localhost:3000/api/health
# Response: {"status":"ok"}
```
--------------------------------
### Run CitrineOS Operator UI with Docker
Source: https://github.com/citrineos/citrineos-operator-ui/blob/main/README.MD
Starts the Operator UI container, connecting it to Hasura and Core services. Assumes CitrineOS Core is already running via Docker.
```bash
docker compose up -d
```
--------------------------------
### Generic Authentication Login Flow
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Example of how to perform a login using the generic authentication provider. This involves sending a POST request to the credentials callback with username and password.
```typescript
// src/app/api/auth/[...nextauth]/options.ts
// Configure in .env.local:
// NEXT_PUBLIC_AUTH_PROVIDER=generic
// NEXT_PUBLIC_ADMIN_EMAIL=admin@citrineos.com
// ADMIN_PASSWORD=CitrineOS!
// Login via POST /api/auth/callback/credentials
const response = await fetch('/api/auth/callback/credentials', {
method: 'POST',
body: new URLSearchParams({
username: 'admin@citrineos.com',
password: 'CitrineOS!',
csrfToken: '',
}),
});
// Successful login redirects to /overview
```
--------------------------------
### Fetch charging station status counts using GraphQL
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Example of fetching the count of online and offline charging stations using a GraphQL client. This query is useful for monitoring the status of charging infrastructure.
```typescript
// src/lib/queries/charging.stations.ts
import {
CHARGING_STATIONS_LIST_QUERY,
CHARGING_STATIONS_GET_QUERY,
CHARGING_STATIONS_CREATE_MUTATION,
CHARGING_STATIONS_EDIT_MUTATION,
CHARGING_STATIONS_DELETE_MUTATION,
CHARGING_STATIONS_STATUS_COUNT_QUERY,
FAULTED_CHARGING_STATIONS_LIST_QUERY,
CHARGING_STATION_ONLINE_STATUS_QUERY,
} from '@lib/queries/charging.stations';
// List with pagination, filters, related location/evses/transactions:
// Variables: { offset, limit, order_by, where: ChargingStations_bool_exp }
// Status count (online vs offline):
// Returns: { online: { aggregate: { count } }, offline: { aggregate: { count } } }
// Faulted stations filter:
// where: { LatestStatusNotifications: { StatusNotification: { connectorStatus: { _eq: "Faulted" } } } }
// Example: fetch online/offline counts via Apollo or graphql-request
import { GraphQLClient } from 'graphql-request';
const client = new GraphQLClient('http://localhost:8090/v1/graphql', {
headers: { Authorization: 'Bearer ' },
});
const { online, offline } = await client.request(CHARGING_STATIONS_STATUS_COUNT_QUERY);
console.log(`Online: ${online.aggregate.count}, Offline: ${offline.aggregate.count}`);
```
--------------------------------
### Protocol-Aware Remote Start Transaction Modal
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Renders the RemoteStartTransactionModal, which automatically routes to the correct OCPP 1.6 or 2.0.1 form based on the station's protocol.
```typescript
// Remote Start — automatically routes to OCPP 1.6 or 2.0.1 form based on station.protocol
import { RemoteStartTransactionModal } from '@lib/client/components/modals/remote-start/remote.start.modal';
// station.protocol === 'ocpp1.6' → renders OCPP1_6_RemoteStart
// station.protocol === 'ocpp2.0.1' → renders OCPP2_0_1_RemoteStart
// station.protocol === 'ocpp2.1' → renders OCPP2_0_1_RemoteStart
// Same pattern for Reset and Remote Stop:
import { ResetModal } from '@lib/client/components/modals/reset/reset.modal';
import { RemoteStopTransactionModal } from '@lib/client/components/modals/remote-stop/remote.stop.modal';
```
--------------------------------
### GraphQL Queries for Charging Stations
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Core GraphQL operations for the `ChargingStations` resource, including listing, getting, creating, editing, deleting, and status counts.
```APIDOC
## GraphQL Queries — Charging Stations
Core GraphQL operations for the `ChargingStations` resource.
```typescript
// src/lib/queries/charging.stations.ts
import {
CHARGING_STATIONS_LIST_QUERY,
CHARGING_STATIONS_GET_QUERY,
CHARGING_STATIONS_CREATE_MUTATION,
CHARGING_STATIONS_EDIT_MUTATION,
CHARGING_STATIONS_DELETE_MUTATION,
CHARGING_STATIONS_STATUS_COUNT_QUERY,
FAULTED_CHARGING_STATIONS_LIST_QUERY,
CHARGING_STATION_ONLINE_STATUS_QUERY,
} from '@lib/queries/charging.stations';
// List with pagination, filters, related location/evses/transactions:
// Variables: { offset, limit, order_by, where: ChargingStations_bool_exp }
// Status count (online vs offline):
// Returns: { online: { aggregate: { count } }, offline: { aggregate: { count } } }
// Faulted stations filter:
// where: { LatestStatusNotifications: { StatusNotification: { connectorStatus: { _eq: "Faulted" } } } }
// Example: fetch online/offline counts via Apollo or graphql-request
import { GraphQLClient } from 'graphql-request';
const client = new GraphQLClient('http://localhost:8090/v1/graphql', {
headers: { Authorization: 'Bearer ' },
});
const { online, offline } = await client.request(CHARGING_STATIONS_STATUS_COUNT_QUERY);
console.log(`Online: ${online.aggregate.count}, Offline: ${offline.aggregate.count}`);
```
```
--------------------------------
### Create Tariff Pricing Structure
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Define pricing for charging sessions using `useCreate` and `TARIFF_CREATE_MUTATION`. Supports per-kWh, per-minute, and per-session rates, along with fees and taxes.
```typescript
// src/lib/queries/tariffs.ts
import {
TARIFF_LIST_QUERY,
TARIFF_GET_QUERY,
TARIFF_CREATE_MUTATION,
TARIFF_EDIT_MUTATION,
TARIFF_DELETE_MUTATION,
GET_CHARGING_STATIONS_FOR_TARIFF,
GET_TRANSACTIONS_FOR_TARIFF,
} from '@lib/queries/tariffs';
// Create a tariff:
const { mutate } = useCreate();
mutate({
resource: 'Tariffs',
values: {
currency: 'USD',
pricePerKwh: 0.35,
pricePerMin: 0.05,
pricePerSession: 1.00,
authorizationAmount: 5.00,
paymentFee: 0.10,
taxRate: 0.08,
tariffAltText: [{ language: 'en', text: '$0.35/kWh + $0.05/min' }],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
meta: { gqlMutation: TARIFF_CREATE_MUTATION },
});
```
--------------------------------
### Linking Local CitrineOS Core
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Steps to link a local development instance of citrineos-core to the operator UI. Requires building the core package and then linking it.
```bash
# In citrineos-core/00_Base:
npm run build
npm link
# In citrineos-operator-ui:
npm link @citrineos/base@1.9.0
```
--------------------------------
### Link Base Package for Local Development
Source: https://github.com/citrineos/citrineos-operator-ui/blob/main/README.MD
Links the base package of CitrineOS Core into the local environment. This command should be run from the '00_Base' directory within citrineos-core.
```bash
cd 00_Base
npm link
```
--------------------------------
### Build CitrineOS Core for Local Linking
Source: https://github.com/citrineos/citrineos-operator-ui/blob/main/README.MD
Builds the CitrineOS Core project. This is a prerequisite for linking the core package into the Operator UI.
```bash
cd citrineos-core
npm run build
```
--------------------------------
### Real-Time Live Provider with GraphQL WebSocket
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Utilizes `graphql-ws` for real-time Hasura events, enabling automatic updates for Refine resources when data changes. Shows how to use `useList` with `liveMode: 'auto'` and manual subscriptions with `useSubscription`.
```typescript
// src/lib/providers/live-provider/index.ts
// Configured via NEXT_PUBLIC_WS_URL
// Refine's useList automatically uses the live provider when liveMode is set:
import { useList } from '@refinedev/core';
const { data } = useList({
resource: 'ChargingStations',
meta: { gqlQuery: CHARGING_STATIONS_LIST_QUERY },
liveMode: 'auto', // re-fetches when subscription fires
});
// Manual subscription via Refine's useSubscription:
import { useSubscription } from '@refinedev/core';
useSubscription({
channel: 'ChargingStations',
types: ['created', 'updated', 'deleted'],
onLiveEvent: (event) => {
console.log('Live event:', event.type, event.payload);
},
});
```
--------------------------------
### File Upload Server Action via Presigned URL
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Enables file uploads to AWS S3 or GCP using presigned URLs, typically for charging station images. Requires `ALLOW_IMAGE_UPLOAD=true` to be enabled.
```typescript
// src/lib/server/actions/file/uploadFileViaPresignedUrl.ts
import { uploadFileViaPresignedUrl } from '@lib/server/actions/file/uploadFileViaPresignedUrl';
// Upload a station image:
const fileInput = document.getElementById('uploadInput') as HTMLInputElement;
const file = fileInput.files?.[0];
if (file) {
const result = await uploadFileViaPresignedUrl(
file,
`charging-stations/images/${stationId}`, // S3 key / GCS object name
);
if (result.success) {
console.log('Uploaded to key:', result.data);
} else {
console.error(result.error); // 'Image upload is disabled' or upload error
}
}
// Fetch a file (config JSON or image) by key:
import { fetchFileAction } from '@lib/server/actions/file/fetchFileAction';
import { BucketType } from '@lib/utils/enums';
const configResult = await fetchFileAction('config.json', BucketType.CORE);
// Returns file contents from the core bucket (config.json)
```
--------------------------------
### Server Actions with Authentication Guards
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Wraps Next.js Server Actions with authentication guards like `authedAction`, `authedActionWithRole`, and `authedActionForTenant` to enforce access control. Shows how to consume action results, checking for success or specific error codes.
```typescript
// src/lib/utils/action-guard.ts
import { authedAction, authedActionWithRole, authedActionForTenant } from '@lib/utils/action-guard';
// Basic: requires valid session
export async function myAction(): Promise> {
return authedAction(async (session) => {
// session.accessToken, session.user.roles, session.user.tenantId
return 'ok';
});
}
// Role-gated: returns FORBIDDEN if role missing
export async function adminAction(): Promise> {
return authedActionWithRole('admin', async (session) => {
return 'admin-only data';
});
}
// Tenant-scoped: returns FORBIDDEN if tenantId mismatch
export async function tenantAction(tenantId: string) {
return authedActionForTenant(tenantId, async (session) => {
return 'tenant data';
});
}
// Consuming action results:
const result = await myAction();
if (result.success) {
console.log(result.data);
} else {
// result.code: 'UNAUTHORIZED' | 'FORBIDDEN' | 'ERROR'
console.error(result.error, result.code);
}
```
--------------------------------
### Initialize BaseRestClient for different OCPP versions and Data API
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Instantiate BaseRestClient for specific OCPP versions (e.g., 2.0.1, 1.6) or for the Data API (null version). The client automatically handles Bearer tokens and OpenTelemetry counters.
```typescript
// src/lib/utils/BaseRestClient.ts
import { BaseRestClient } from '@lib/utils/BaseRestClient';
import { OCPPVersion } from '@citrineos/base';
// OCPP 2.0.1 message API: base URL = {CITRINE_CORE_URL}/ocpp/2.0.1
const client201 = new BaseRestClient(OCPPVersion.OCPP2_0_1);
// OCPP 1.6 message API: base URL = {CITRINE_CORE_URL}/ocpp/1.6
const client16 = new BaseRestClient(OCPPVersion.OCPP1_6);
// Data API (no OCPP version): base URL = {CITRINE_CORE_URL}/data
const dataClient = new BaseRestClient(null);
```
--------------------------------
### Create and use AccessControlProvider for role-based permissions
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Implement role-based access control using Refine's `AccessControlProvider`. This involves defining `getPermissions` and `getUserRole` functions. The provider can be used with Refine's `CanAccess` component or programmatically with `useCan`.
```typescript
// src/lib/providers/access-control-provider/index.ts
import { createAccessProvider } from '@lib/providers/access-control-provider';
import { ActionType, ResourceType } from '@lib/utils/access.types';
const accessProvider = createAccessProvider({
getPermissions: async () => {
// Return user permissions object
return { role: 'admin' };
},
getUserRole: async (permissions) => {
return (permissions as any)?.role;
},
});
// ResourceType enum values:
// ResourceType.CHARGING_STATIONS, LOCATIONS, TRANSACTIONS,
// AUTHORIZATIONS, TARIFFS, PARTNERS, EVSES, CONNECTORS, ...
// ActionType enum values:
// ActionType.LIST, SHOW, CREATE, EDIT, DELETE, ACCESS, COMMAND
// Usage in React with Refine's CanAccess component:
import { CanAccess } from '@refinedev/core';
}
>
// Programmatic check:
import { useCan } from '@refinedev/core';
const { data: canEdit } = useCan({
resource: ResourceType.CHARGING_STATIONS,
action: ActionType.EDIT,
params: { id: 42 },
});
```
--------------------------------
### Link Base Package into Operator UI
Source: https://github.com/citrineos/citrineos-operator-ui/blob/main/README.MD
Links the locally built base package into the Operator UI project. Replace '@X.X.X' with the actual version if necessary. Note that 'npm run fresh' or 'npm run fi' will remove this link.
```bash
cd citrineos-operator-ui
npm link @citrineos/base@X.X.X
```
--------------------------------
### Generic Authentication Provider
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
This section details how to configure and use the generic authentication provider, which relies on username and password credentials.
```APIDOC
## Generic Auth Provider (credentials-based)
### Description
The default provider validates username/password against `NEXT_PUBLIC_ADMIN_EMAIL` and `ADMIN_PASSWORD` environment variables.
### Configuration
Configure in `.env.local`:
```
NEXT_PUBLIC_AUTH_PROVIDER=generic
NEXT_PUBLIC_ADMIN_EMAIL=admin@citrineos.com
ADMIN_PASSWORD=CitrineOS!
```
### Login
Login via POST `/api/auth/callback/credentials`
### Request Example
```typescript
const response = await fetch('/api/auth/callback/credentials', {
method: 'POST',
body: new URLSearchParams({
username: 'admin@citrineos.com',
password: 'CitrineOS!',
csrfToken: '',
}),
});
// Successful login redirects to /overview
```
```
--------------------------------
### Tariffs CRUD Operations
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Create and manage per-kWh, per-minute, or per-session pricing structures for charging sessions.
```APIDOC
## Create Tariff
### Description
Creates a new pricing structure (tariff).
### Method
`useCreate` hook with `meta.gqlMutation: TARIFF_CREATE_MUTATION`
### Request Body Example
```json
{
"currency": "USD",
"pricePerKwh": 0.35,
"pricePerMin": 0.05,
"pricePerSession": 1.00,
"authorizationAmount": 5.00,
"paymentFee": 0.10,
"taxRate": 0.08,
"tariffAltText": [{"language": "en", "text": "$0.35/kWh + $0.05/min"}],
"createdAt": "",
"updatedAt": ""
}
```
### Response Example
(Details depend on `TARIFF_CREATE_MUTATION` response schema)
```
```APIDOC
## List Tariffs
### Description
Retrieves a list of available tariffs.
### Method
`useList` hook with `meta.gqlQuery: Tariff_LIST_QUERY`
### Response Example
(Details depend on `TARIFF_LIST_QUERY` response schema)
```
```APIDOC
## Get Tariff
### Description
Retrieves details for a specific tariff.
### Method
`useGet` hook with `meta.gqlQuery: Tariff_GET_QUERY`
### Response Example
(Details depend on `TARIFF_GET_QUERY` response schema)
```
```APIDOC
## Edit Tariff
### Description
Updates an existing tariff.
### Method
`useUpdate` hook with `meta.gqlMutation: TARIFF_EDIT_MUTATION`
### Request Body Example
(Fields to update, e.g., `pricePerKwh`, `pricePerMin`, etc.)
### Response Example
(Details depend on `TARIFF_EDIT_MUTATION` response schema)
```
```APIDOC
## Delete Tariff
### Description
Deletes a tariff.
### Method
`useDelete` hook with `meta.gqlMutation: TARIFF_DELETE_MUTATION`
### Response Example
(Details depend on `TARIFF_DELETE_MUTATION` response schema)
```
```APIDOC
## Get Charging Stations for Tariff
### Description
Retrieves charging stations associated with a specific tariff.
### Method
`useQuery` hook with `meta.gqlQuery: GET_CHARGING_STATIONS_FOR_TARIFF`
### Response Example
(Details depend on `GET_CHARGING_STATIONS_FOR_TARIFF` response schema)
```
```APIDOC
## Get Transactions for Tariff
### Description
Retrieves transactions associated with a specific tariff.
### Method
`useQuery` hook with `meta.gqlQuery: GET_TRANSACTIONS_FOR_TARIFF`
### Response Example
(Details depend on `GET_TRANSACTIONS_FOR_TARIFF` response schema)
```
--------------------------------
### Overview Dashboard Page Components
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Renders five real-time summary cards using Refine data hooks and Recharts. All cards use liveMode="auto" for real-time updates via the Hasura WebSocket live provider.
```typescript
// src/lib/client/pages/overview/index.tsx
import { Overview } from '@lib/client/pages/overview';
// Composed of:
// — pie chart of online vs. offline stations
// (uses CHARGING_STATIONS_STATUS_COUNT_QUERY)
// — gauge chart showing current charging activity
// — success rate from TRANSACTION_SUCCESS_RATE_QUERY
// — Google Maps with station cluster markers
// (uses GET_CHARGING_STATIONS_WITH_LOCATION_... query)
// — live list of active transactions
// (uses TRANSACTION_LIST_QUERY with isActive: true)
// All cards use liveMode="auto" via the Hasura WebSocket live provider
// for real-time updates without manual refresh.
```
--------------------------------
### Create RFID Authorization Token
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Use `useCreate` with `AUTHORIZATIONS_CREATE_MUTATION` to register a new RFID or other ID token for charging authorization. Specify `idToken`, `idTokenType`, and `status`.
```typescript
// src/lib/queries/authorizations.ts
import {
AUTHORIZATIONS_LIST_QUERY,
AUTHORIZATIONS_SHOW_QUERY,
AUTHORIZATIONS_CREATE_MUTATION,
AUTHORIZATIONS_EDIT_MUTATION,
AUTHORIZATIONS_DELETE_MUTATION,
} from '@lib/queries/authorizations';
// Create an RFID authorization token:
const { mutate: createAuth } = useCreate();
createAuth({
resource: 'Authorizations',
values: {
idToken: 'DEADBEEF',
idTokenType: 'ISO14443',
status: 'Accepted',
chargingPriority: 0,
concurrentTransaction: false,
realTimeAuth: false,
cacheExpiryDateTime: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
meta: { gqlMutation: AUTHORIZATIONS_CREATE_MUTATION },
});
```
--------------------------------
### Charging Stations List Component Features
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Provides a paginated, searchable, and sortable table of charging stations with persistent column visibility and URL-synced state. Features debounced search, column toggles, and guards for user permissions.
```typescript
// src/lib/client/pages/charging-stations/list/charging.stations.list.tsx
import { ChargingStationsList } from '@lib/client/pages/charging-stations/list/charging.stations.list';
// Features:
// - Debounced full-text search across station ID, location name/address
// - Column visibility toggle persisted per-resource via localStorage
// - URL-synced table state (pagination, sorting, filters) via nuqs
// - CanAccess guards: CREATE button hidden if user lacks create permission
// - Uses CHARGING_STATIONS_LIST_QUERY with ChargingStationClass transform
// Table state is synced to URL query params:
// ?ChargingStations={"pagination":{"current":1,"pageSize":20},"sorters":[...]}
// Search triggers filter:
// { operator: 'or', value: [
// { field: 'id', operator: 'contains', value: searchTerm },
// { field: 'location.name', operator: 'contains', value: searchTerm },
// ]}
```
--------------------------------
### Handle errors from BaseRestClient requests
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Demonstrates how to catch and handle potential errors, specifically `UnsuccessfulRequestException`, when making requests with BaseRestClient. This allows for inspecting the HTTP status and response data.
```typescript
// Error handling
try {
await client201.post('/reset', {}, { identifier: 'cp001', tenantId: '1', type: 'Immediate' });
} catch (err) {
// UnsuccessfulRequestException with response.status and response.data
if (err instanceof UnsuccessfulRequestException) {
console.error('HTTP error:', err.response.status);
}
}
```
--------------------------------
### Keycloak JWT Callback and Client-Side Session Reading
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Extracts Keycloak roles and tenantId from access tokens for use in sessions. Demonstrates how to access session data, including roles and tenantId, on the client-side using NextAuth.
```typescript
// src/app/api/auth/[...nextauth]/options.ts
// JWT callback extracts Keycloak roles and tenantId from the access token
// Session includes:
// session.accessToken — Bearer token for API requests
// session.user.roles — Keycloak client roles array
// session.user.tenantId — Tenant identifier from JWT claim
// session.keycloakLogoutUrl — URL for browser-level Keycloak logout
// Example: Reading session on client
import { useSession } from 'next-auth/react';
export function MyComponent() {
const { data: session } = useSession();
const roles = (session?.user as any)?.roles ?? [];
const tenantId = (session?.user as any)?.tenantId;
// ...
}
```
--------------------------------
### Environment Configuration Variables
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Required and optional environment variables for configuring the CitrineOS Operator UI. These cover API endpoints, authentication, mapping services, file storage, and more.
```bash
# .env.local — required variables
# GraphQL endpoint (Hasura)
NEXT_PUBLIC_API_URL=http://localhost:8090/v1/graphql
NEXT_PUBLIC_WS_URL=ws://localhost:8090/v1/graphql
# CitrineOS Core REST API
NEXT_PUBLIC_CITRINE_CORE_URL=http://localhost:8080
# Optional: Use Hasura admin secret (not recommended for production)
HASURA_ADMIN_SECRET=CitrineOS!
# Authentication: "generic" or "keycloak"
NEXT_PUBLIC_AUTH_PROVIDER=generic
NEXT_PUBLIC_ADMIN_EMAIL=admin@citrineos.com
ADMIN_PASSWORD=CitrineOS!
# Keycloak (required when NEXT_PUBLIC_AUTH_PROVIDER=keycloak)
NEXT_PUBLIC_KEYCLOAK_URL=https://keycloak.example.com
KEYCLOAK_SERVER_URL=http://keycloak:8080 # internal URL for server-side requests
KEYCLOAK_REALM=citrineos
KEYCLOAK_CLIENT_ID=citrineos-ui
KEYCLOAK_CLIENT_SECRET=your-client-secret
# Google Maps (for location address autocomplete & map display)
GOOGLE_MAPS_API_KEY=YOUR_GOOGLE_MAPS_API_KEY
GOOGLE_MAPS_ADDRESS_API_KEY=YOUR_GOOGLE_MAPS_API_KEY
NEXT_PUBLIC_DEFAULT_MAP_CENTER_LATITUDE=39.833333
NEXT_PUBLIC_DEFAULT_MAP_CENTER_LONGITUDE=-98.583333
# File storage: "s3" (default) or "gcp"
FILE_STORAGE_TYPE=s3
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_S3_BUCKET_NAME=citrineos-ui-bucket
AWS_S3_CORE_BUCKET_NAME=citrineos-core-bucket
# Optional
ALLOW_IMAGE_UPLOAD=false
NEXT_PUBLIC_METRICS_URL=http://localhost:4318/v1/metrics
NEXT_PUBLIC_BANNER_MESSAGE=OCPP 2.1 Now Available!
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=YOUR-SECRET-KEY-TO-ENCRYPT-JWT
```
--------------------------------
### OCPP 2.0.1 Command Registry Usage
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Imports and uses functions from the OCPP 2.0.1 command registry to retrieve all available command keys.
```typescript
// src/lib/client/components/modals/2.0.1/commands.registry.ts
import {
OCPP2_0_1_COMMANDS_REGISTRY,
getOCPP201CommandKeys,
getOCPP201Command,
} from '@lib/client/components/modals/2.0.1/commands.registry';
// Available commands:
// 'Certificate Signed', 'Change Availability', 'Clear Cache',
// 'Customer Information', 'Data Transfer', 'Delete Certificate',
// 'Delete Station Network Profiles', 'Get Base Report',
// 'Get Installed Certificate IDs', 'Get Logs', 'Get Transaction Status',
// 'Get Variables', 'Install Certificate', 'Set Network Profile',
// 'Set Variables', 'Trigger Message', 'Unlock Connector',
// 'Update Auth Password', 'Update Firmware'
const allCommands = getOCPP201CommandKeys();
```
--------------------------------
### Initialize and Increment OpenTelemetry Metrics
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Initializes OpenTelemetry telemetry export via OTLP HTTP and increments a request counter. Call initTelemetry once at app startup. incrementRequestCount is a no-op if telemetry is disabled or not initialized.
```typescript
// src/lib/utils/telemetry.ts
import { initTelemetry, incrementRequestCount } from '@lib/utils/telemetry';
// Initialize once at app startup (idempotent):
initTelemetry();
// Creates: MeterProvider → OTLPMetricExporter → exports every 60s to NEXT_PUBLIC_METRICS_URL
// Metric: requests_count (counter) — service name: citrineos-operator-ui
// Increment request counter with custom attributes:
incrementRequestCount({ url: '/api/stations', method: 'GET' });
// No-op if telemetry is disabled or not yet initialized
```
--------------------------------
### List Active Transactions for a Station
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Query active transactions for a specific station using `TRANSACTION_LIST_QUERY`. This is useful for monitoring real-time charging sessions. Requires `stationPkId` and optional filtering/ordering.
```typescript
// src/lib/queries/transactions.ts
import {
TRANSACTION_LIST_QUERY,
TRANSACTION_GET_QUERY,
TRANSACTION_EDIT_MUTATION,
TRANSACTION_SUCCESS_RATE_QUERY,
GET_TRANSACTION_LIST_FOR_STATION,
GET_TRANSACTIONS_FOR_AUTHORIZATION,
} from '@lib/queries/transactions';
// List active transactions for a specific station:
const variables = {
stationPkId: 1,
where: [{ isActive: { _eq: true } }],
order_by: { createdAt: 'desc' },
offset: 0,
limit: 10,
};
// Transaction success rate (used in Overview dashboard):
// Returns: { success: { aggregate: { count } }, total: { aggregate: { count } } }
// Toggle transaction active state:
const { mutate: updateTx } = useUpdate();
updateTx({
resource: 'Transactions',
id: 123,
values: { isActive: false, updatedAt: new Date().toISOString() },
meta: { gqlMutation: TRANSACTION_EDIT_MUTATION },
});
```
--------------------------------
### Address Autocomplete Server Action
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Uses the Google Places API for address autocomplete with strict country filtering. Requires an authenticated session and returns PlacePrediction objects.
```typescript
// src/lib/server/actions/map/autocompleteAddress.ts
import { autocompleteAddress } from '@lib/server/actions/map/autocompleteAddress';
// Server action (requires authenticated session):
const result = await autocompleteAddress('123 Main', 'US', 'session-token-xyz');
if (result.success) {
result.data.forEach((prediction) => {
console.log(prediction.place_id); // Google Place ID
console.log(prediction.description); // Full address text
console.log(prediction.structured_formatting?.main_text); // Street
console.log(prediction.structured_formatting?.secondary_text); // City/State
});
}
// Returns PlacePrediction[] filtered to street_address, premise, subpremise, route types
// Country restriction via includedRegionCodes (not IP-biased)
```
--------------------------------
### Use Translations in a Server Component with next-intl
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Shows how to fetch translations in a server component using `getTranslations` from `next-intl/server`. This method is asynchronous and requires awaiting the translation object.
```typescript
import {
getTranslations
} from "next-intl/server";
export async function ServerComponent() {
const t = await getTranslations('common');
return
{t('overview.chargerOnlineStatus')}
;
}
```
--------------------------------
### Use Translations in a Client Component with next-intl
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Demonstrates how to use the `useTranslate` hook from `@refinedev/core` to access translations in a client component. Ensure locale files are correctly placed in `public/locales/{locale}/`.
```typescript
import {
useTranslate
} from "@refinedev/core";
export function MyComponent() {
const translate = useTranslate();
return (
// Output: "Add Charging Station"
);
}
```
--------------------------------
### BaseRestClient Usage
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
The BaseRestClient simplifies making requests to CitrineOS Core Message and Data REST APIs. It handles token attachment and OpenTelemetry metrics.
```APIDOC
## BaseRestClient
`BaseRestClient` wraps Axios for calling CitrineOS Core Message and Data REST APIs. It automatically attaches Bearer tokens from the auth provider and increments OpenTelemetry request counters.
### Initialization
```typescript
// src/lib/utils/BaseRestClient.ts
import { BaseRestClient } from '@lib/utils/BaseRestClient';
import { OCPPVersion } from '@citrine/base';
// OCPP 2.0.1 message API: base URL = {CITRINE_CORE_URL}/ocpp/2.0.1
const client201 = new BaseRestClient(OCPPVersion.OCPP2_0_1);
// OCPP 1.6 message API: base URL = {CITRINE_CORE_URL}/ocpp/1.6
const client16 = new BaseRestClient(OCPPVersion.OCPP1_6);
// Data API (no OCPP version): base URL = {CITRINE_CORE_URL}/data
const dataClient = new BaseRestClient(null);
```
### HTTP Methods
#### GET request
```typescript
// GET request
const status = await client201.get('/stations/cp001/status', {});
```
#### POST request
```typescript
// POST request — send OCPP command to station
const result = await client201.post(
'/remotestart',
{},
{
identifier: 'cp001',
tenantId: '1',
remoteStartRequest: {
idToken: { idToken: 'DEADBEEF', type: 'ISO14443' },
evseId: 1,
},
},
);
```
#### DELETE request
```typescript
// DELETE request
await dataClient.del('/locations/5', {});
```
### Error Handling
```typescript
// Error handling
try {
await client201.post('/reset', {}, { identifier: 'cp001', tenantId: '1', type: 'Immediate' });
} catch (err) {
// UnsuccessfulRequestException with response.status and response.data
if (err instanceof UnsuccessfulRequestException) {
console.error('HTTP error:', err.response.status);
}
}
```
```
--------------------------------
### Transactions Queries and Mutations
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Query charging session transactions, including real-time data, and manage transaction states.
```APIDOC
## List Transactions
### Description
Retrieves a list of charging session transactions, with options for filtering and sorting.
### Method
`useList` hook with `meta.gqlQuery: TRANSACTION_LIST_QUERY`
### Parameters
#### Query Parameters
- **stationPkId** (integer) - Required - The primary key ID of the station.
- **where** (object) - Optional - Filtering conditions (e.g., `[{ isActive: { _eq: true } }]`).
- **order_by** (object) - Optional - Sorting order (e.g., `{ createdAt: 'desc' }`).
- **offset** (integer) - Optional - For pagination, the number of records to skip.
- **limit** (integer) - Optional - For pagination, the maximum number of records to return.
### Response Example
(Details depend on `TRANSACTION_LIST_QUERY` response schema)
```
```APIDOC
## Get Transaction
### Description
Retrieves details for a specific charging session transaction.
### Method
`useGet` hook with `meta.gqlQuery: TRANSACTION_GET_QUERY`
### Response Example
(Details depend on `TRANSACTION_GET_QUERY` response schema)
```
```APIDOC
## Edit Transaction
### Description
Updates an existing charging session transaction, for example, to toggle its active state.
### Method
`useUpdate` hook with `meta.gqlMutation: TRANSACTION_EDIT_MUTATION`
### Request Body Example
```json
{
"isActive": false,
"updatedAt": ""
}
```
### Response Example
(Details depend on `TRANSACTION_EDIT_MUTATION` response schema)
```
```APIDOC
## Transaction Success Rate
### Description
Calculates the success rate of charging transactions.
### Method
`useQuery` hook with `meta.gqlQuery: TRANSACTION_SUCCESS_RATE_QUERY`
### Response Example
```json
{
"success": {"aggregate": {"count": }},
"total": {"aggregate": {"count": }}
}
```
```
```APIDOC
## Get Transactions for Station
### Description
Retrieves transactions associated with a specific charging station.
### Method
`useQuery` hook with `meta.gqlQuery: GET_TRANSACTION_LIST_FOR_STATION`
### Response Example
(Details depend on `GET_TRANSACTION_LIST_FOR_STATION` response schema)
```
```APIDOC
## Get Transactions for Authorization
### Description
Retrieves transactions linked to a specific authorization token.
### Method
`useQuery` hook with `meta.gqlQuery: GET_TRANSACTIONS_FOR_AUTHORIZATION`
### Response Example
(Details depend on `GET_TRANSACTIONS_FOR_AUTHORIZATION` response schema)
```
--------------------------------
### Create EV Charging Location
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Use `useCreate` hook with `LOCATIONS_CREATE_MUTATION` to add a new charging location. Ensure all required fields like name, address, and coordinates are provided.
```typescript
// src/lib/queries/locations.ts
import {
LOCATIONS_LIST_QUERY,
LOCATIONS_GET_QUERY,
LOCATIONS_CREATE_MUTATION,
LOCATIONS_EDIT_MUTATION,
LOCATIONS_DELETE_MUTATION,
} from '@lib/queries/locations';
// Create a location via Refine useCreate:
const { mutate } = useCreate();
mutate({
resource: 'Locations',
values: {
name: 'Main Street Charging Hub',
address: '123 Main St',
city: 'Austin',
state: 'TX',
postalCode: '78701',
country: 'US',
coordinates: { type: 'Point', coordinates: [-97.7431, 30.2672] },
timeZone: 'America/Chicago',
parkingType: 'PARKING_LOT',
facilities: ['HOTEL', 'SHOPPING_CENTRE'],
openingHours: { twentyfourseven: true },
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
meta: { gqlMutation: LOCATIONS_CREATE_MUTATION },
});
```
--------------------------------
### Query OCPP Messages for a Station
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Retrieve raw OCPP messages for a specific station using `useList` and `GET_OCPP_MESSAGES_LIST_FOR_STATION`. Supports pagination and sorting by timestamp.
```typescript
// src/lib/queries/ocpp.messages.ts
import {
GET_OCPP_MESSAGES_LIST_FOR_STATION,
GET_OCPP_MESSAGES_FOR_TRANSACTION_LIST_QUERY,
} from '@lib/queries/ocpp.messages';
// All OCPP messages for a station (paginated):
// Variables: { stationPkId, where, order_by, offset, limit }
// Returns: { id, stationId, correlationId, origin, protocol, action, message, timestamp }
// Transaction-specific OCPP messages:
// Filters for StartTransaction, StopTransaction, MeterValues (OCPP 1.6)
// or GetTransactionStatus (OCPP 2.0.1) matching the transactionId in message JSON
const { data: messages } = useList({
resource: 'OCPPMessages',
meta: {
gqlQuery: GET_OCPP_MESSAGES_LIST_FOR_STATION,
gqlVariables: { stationPkId: 1 },
},
sorters: [{ field: 'timestamp', order: 'desc' }],
pagination: { current: 1, pageSize: 50 },
});
```
--------------------------------
### Charging Station Create/Edit Form
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
A form component for creating or editing charging station details, including location search, coordinate picking, and optional image upload. Supports Zod schema validation and uses specific mutations for create or edit operations.
```typescript
// src/lib/client/pages/charging-stations/upsert/charging.stations.upsert.tsx
import { ChargingStationUpsert } from '@lib/client/pages/charging-stations/upsert/charging.stations.upsert';
// Usage — create mode:
// Usage — edit mode:
// Form fields:
// - id (string, required) — must match the physical station/simulator ID (e.g. "cp001")
// - locationId (number, required) — combobox with debounced location search
// - floorLevel (string, optional)
// - parkingRestrictions (multi-select enum)
// - capabilities (multi-select enum: RESERVABLE, CREDIT_CARD_PAYABLE, etc.)
// - use16StatusNotification0 (checkbox) — OCPP 1.6 connector 0 status notifications
// - coordinates (lat/lng) — optional override; defaults to location coordinates
// - image (file upload) — stored in S3 at charging-stations/images/{stationId}
// Schema validated with Zod using ChargingStationSchema from @citrineos/base
// Mutation: CHARGING_STATIONS_CREATE_MUTATION or CHARGING_STATIONS_EDIT_MUTATION
```
--------------------------------
### Hasura GraphQL Data Provider Usage
Source: https://context7.com/citrineos/citrineos-operator-ui/llms.txt
Demonstrates how to use the Refine Hasura data provider for GraphQL queries and mutations. The provider automatically handles authentication by attaching Bearer tokens or the Hasura admin secret.
```typescript
// src/lib/providers/data-provider/index.ts
// Configured via NEXT_PUBLIC_API_URL
// Direct GraphQL query via the provider (used internally by Refine hooks):
import { useList, useOne, useCreate, useUpdate, useDelete } from '@refinedev/core';
// List charging stations
const { data, isLoading } = useList({
resource: 'ChargingStations',
meta: { gqlQuery: CHARGING_STATIONS_LIST_QUERY },
pagination: { current: 1, pageSize: 20 },
sorters: [{ field: 'updatedAt', order: 'desc' }],
filters: [{ field: 'isOnline', operator: 'eq', value: true }],
});
// Get single station by PK
const { data: station } = useOne({
resource: 'ChargingStations',
id: 42,
meta: { gqlQuery: CHARGING_STATIONS_GET_QUERY },
});
// Create a charging station
const { mutate: createStation } = useCreate();
createStation({
resource: 'ChargingStations',
values: {
id: 'cp001',
locationId: 1,
tenantId: '1',
floorLevel: 'G',
capabilities: ['RESERVABLE'],
parkingRestrictions: [],
use16StatusNotification0: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
meta: { gqlMutation: CHARGING_STATIONS_CREATE_MUTATION },
});
```