# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview SmartBotic is a workflow automation and execution platform with a microservices architecture. The backend is C++20 with gRPC communication, and the frontend is React/TypeScript. ## Build Commands ### C++ Backend The existing `build/` directory is configured with **Ninja**, not Make. Running `make` in it prints "Nothing to be done" and builds nothing at all - it does not fail, so an edit looks built when it has not been. Always build through cmake, which uses whichever generator the directory was configured with: ```bash # Everything cmake --build build -j8 # One target - much faster when only one service changed cmake --build build --target smartbotic-webserver -j8 cmake --build build --target smartbotic-runner -j8 ``` Configuring a build directory from scratch: ```bash # Debug cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug # Release cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release # With Address Sanitizer cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON ``` After building a service, restart it to actually run the new binary. Everything runs on **zeus**, which is Void Linux and uses **runit, not systemd**: ```bash sudo sv restart smartbotic-webserver smartbotic-runner sudo sv status smartbotic-webserver smartbotic-runner tail -f /var/log/smartbotic-webserver/current tail -f /var/log/smartbotic-runner/current ``` There is no `systemctl` on zeus at all - it answers "command not found", which reads like a typo rather than the wrong init system. The `systemd/user/*.service` units in this repo are leftovers from when the services ran on mulan. ### Tests ```bash ./scripts/run-node-tests.sh # every node, through a running webserver + runner ./scripts/run-cpp-tests.sh # pure C++ logic, compiled in the build image ``` Node tests need the services up. When the runner and webserver are on different hosts, a case whose NODE fetches the API needs `SMARTBOTIC_RUNNER_API` set to an address the runner can reach - see the `{{API}}` note under Relocating a service. ### Frontend (webui/) ```bash cd webui npm install npm run dev # Dev server (port 3000, proxies to localhost:8090) npm run build # Production build npm run lint # ESLint ``` ## Running Services The database is **not** built from this repo - it is the standalone upstream `smartbotic-database` daemon. It runs on zeus as the Docker container `smartbotic-db` (moved there 2026-08-09; it and the SD.cpp REST API could not share mulan's memory), so it is neither an APT package nor a system service on the host any more. It is already up; the in-repo services connect to it: ```bash # 1. Database daemon - Docker container `smartbotic-db` on 9004. Not from this repo. docker ps --filter name=smartbotic-db # 2. WebServer (HTTP 8090, WebSocket 8091, gRPC 9012 + 9013) # 3. Runner (gRPC 9011) sudo sv status smartbotic-webserver smartbotic-runner ``` ### Relocating a service **No service may assume another is on the same host.** Every address a service needs is an env override, and both runit `run` scripts (`/etc/sv/smartbotic-*/run`) state all of them explicitly even where the value is the local default - so moving a service to another machine for load balancing or failover is an edit of one file, not a hunt through the code. | variable | service | what breaks when it is wrong | | --- | --- | --- | | `DATABASE_ADDRESS` | both | no persistence at all; fails loudly | | `WEBSERVER_ADDRESS` | runner | the runner never registers | | `NODE_SYNC_ADDRESS` | runner | no node updates reach it | | `CREDENTIAL_SERVICE_ADDRESS` | runner | credentials and workflow-control fail | | `ADVERTISE_ADDRESS` | runner | **fails silently** - the runner registers as `localhost:`, reports online, and every dispatch goes to the webserver's own host | | `NODES_PATH` | runner | no built-in nodes | | `WEBUI_PATH` | webserver | the UI 404s, the API still works | `config/*.json` is tracked in git and its defaults are all `localhost`, so it must not be edited per host. `config/*.local.json` is gitignored but nothing reads it - the mechanism is `${VAR:default}` substitution. **Tests must not assume co-location either.** A case whose node fetches the API writes `{{API}}`, which the harness substitutes from `SMARTBOTIC_RUNNER_API`. Three cases hardcoded `http://localhost:8090` and broke the day the runner moved. SD.cpp stays on mulan (`https://mulan:8077`) because that is where the GPU is. zeus needs its self-signed certificate in **two** stores - the trusted-certificate store for host `mulan` (for the runner) and the system CA store (for the test harness's Python probe). Both the database address and the project namespace are configurable everywhere, via `config/webserver.json` and `config/runner.json`: | Key | Default | Env override | | --- | --- | --- | | `database_address` | `localhost:9004` | `DATABASE_ADDRESS` | | `database_project` | `smartbotic-automation` | `DATABASE_PROJECT` | One `smartbotic-database` instance is shared by several projects. `database_project` is the multi-tenant namespace (upstream client >= 2.3): the client transparently sends every collection as `:`, so our data stays isolated from other services on the same daemon. Never set it to `default` - that is the shared back-compat namespace where projects collide. ## Architecture ### Two In-Repo Services + External Database 1. **WebServer Service** (src/webserver/) - HTTP REST API, WebSocket for real-time updates, JWT auth, runner registry 2. **Runner Service** (src/runner/) - Workflow execution engine with QuickJS for JavaScript node evaluation Both persist through the external **smartbotic-database** daemon (upstream repo: `ssh://git@git.smartbotics.ai:10022/fszontagh/smartbotic-database.git`), consumed as the `libsmartbotic-db-client-dev` APT package. `cmake/FindPackages.cmake` locates it via `find_package(smartbotic-db-client CONFIG REQUIRED)` → target `smartbotic::db-client`. `lib/storage/storage_client.{hpp,cpp}` is a thin PIMPL adapter over `smartbotic::database::Client`, so callers never touch the upstream API directly. ### Data Flow ``` WebUI (React) → HTTP/WebSocket → WebServer → gRPC → smartbotic-database (external) ↓ gRPC → Runner(s) → executes workflows ``` ### Key Directories - `lib/` - Shared libraries (common utilities, config loader, logging, storage client) - `proto/` - Protocol Buffer definitions for gRPC services - `src/` - Microservice implementations - `nodes/` - Built-in workflow node definitions (JavaScript modules) - `webui/` - React frontend - `config/` - Runtime JSON configuration files ### Node System Workflow nodes are JavaScript modules in `nodes/` with this interface: ```javascript module.exports = { configSchema: { /* JSON Schema */ }, inputSchema: { /* JSON Schema */ }, outputSchema: { /* JSON Schema */ }, execute: async (config, input, context) => { /* returns output */ } } ``` Nodes are loaded by the Runner service and executed in QuickJS. Hot-reload is supported. **See [docs/nodes.md](docs/nodes.md) for complete node creation guide.** **See [docs/node-roadmap.md](docs/node-roadmap.md) for nodes worth building next.** ### Migrating Nodes to Database After creating or modifying nodes in `nodes/`, migrate them to the running database: ```bash # 1. Login to get JWT token TOKEN=$(curl -s http://localhost:8090/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"username": "admin", "password": "admin"}' | jq -r '.accessToken') # 2. Migrate nodes from filesystem to database curl -X POST http://localhost:8090/api/v1/nodes/migrate \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"nodesPath": "./nodes"}' # 3. Verify nodes are loaded curl -s http://localhost:8090/api/v1/nodes \ -H "Authorization: Bearer $TOKEN" | jq '.nodes[] | "\(.id) - \(.name)"' ``` Runners automatically receive node updates via gRPC streaming - no restart required. ### Configuration Services load JSON configs from `config/` directory: - `webserver.json` - HTTP port, JWT settings, runner load balancing, `database_address` - `runner.json` - Runner ID, max concurrent executions, hot reload settings, `database_address` The database daemon has its own config at `/etc/smartbotic-database/config.json`, owned by the upstream package - not by this repo. Environment variables can override config values using `${VAR_NAME:default}` syntax. ## C++ Standards - C++20 required (strict compliance, no extensions) - Compiler warnings: Wall, Wextra, Wpedantic - Key libraries: gRPC, Protobuf, cpp-httplib, QuickJS, spdlog, nlohmann/json, bcrypt ## Frontend Stack - React 18.2 + TypeScript + Vite - State: Zustand + React Query - UI: TailwindCSS + Lucide icons - Workflow editor: ReactFlow