### 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 }, }); ```