9.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Repo layout
This is a monorepo with two independent Node packages:
BACKEND/— AdonisJS 6 (TypeScript, ESM) HTTP API + Socket.IO server. Talks to MySQL via Lucid ORM and uses Redis for shared/persisted state. Connects to physical lab equipment (Cisco-style consoles, APC PDUs, network switches) over raw TCP/Telnet.FRONTEND/— Vite + React 19 + Mantine 8 SPA. Authenticates locally and connects to the backend's REST API and Socket.IO server.
There is no workspace tool — each side has its own package.json, node_modules, lockfile, and ESLint/TS config. Run npm commands from inside the relevant directory.
Common commands
Backend (cd BACKEND)
npm run dev— starts AdonisJS vianode ace serve --hmr(HTTP + Socket.IO providers boot together).npm run build—node ace build.npm start—node bin/server.js(production entry; the Socket.IO provider'sready()only boots the WS server when the entry isserver.js).npm test—node ace test(Japa). Suites are configured inadonisrc.ts:unit(tests/unit/**/*.spec.ts) andfunctional(tests/functional/**/*.spec.ts).npm run lint/npm run format/npm run typecheck.node ace migration:run— apply MySQL migrations fromdatabase/migrations/.node ace db:seed— run seeders indatabase/seeders/(e.g.prompt_ai_seeder.ts).- Run a single test:
node ace test --files "tests/unit/foo.spec.ts"(Japa filter;--tagsand--matchalso work).
Frontend (cd FRONTEND)
npm run dev— Vite dev server (default port 5173).npm run build—tsc -b && vite build.npm run lint— flat-config ESLint.npm run preview— preview the production build.
Setup / environment
- Both sides use
.env(copy from.env.example). Backend readsSOCKET_PORT(default 8989),FRONTEND_URL(CORS origin), MySQL/Redis creds, Zulip stream/topic,LINK_WIKI,TIME_ZONE. Frontend readsVITE_BACKEND_URLandVITE_SOCKET_SERVER. - A local Redis server is required (the README documents
apt install redis-serverfor Linux hosts).
Architecture — the parts that span files
Backend: HTTP API is thin; the real engine is the Socket.IO provider
BACKEND/providers/socket_io_provider.ts is registered in adonisrc.ts and only boots its WebSocket server when the process entry is server.js (so ace commands like migrations don't open the socket port). It exposes a static SocketIoProvider.io for the rest of the app.
Inside WebSocketIo.boot() it creates a separate http.createServer() listening on SOCKET_PORT (independent of the AdonisJS HTTP port) and registers a single big connection handler with ~30 socket events (connect_lines, write_command_line_from_web, run_scenario, open_cli/close_cli, control_apc, control_switch, run_all_dpelp, load_ios_router, etc.). When extending behavior, the pattern is: add a new socket.on(...) block and route it through handleLineOperation / handleStationOperation, which auto-reconnect the underlying TCP session if it has dropped.
The provider keeps in-memory maps that ARE the live system state:
lineMap: Map<lineId, LineConnection>stationMap: Map<stationId, StationConnection>apcsControl: Map<apcIp, APCController>switchControl: Map<switchIp, SwitchController>userConnecting: Map<userId, ...>
Every 10s saveState() serializes a slimmed lineMap (status forced to disconnected, output truncated to last 5000 chars, CLI ownership cleared) into Redis under key socket_state. On boot, restoreState() rehydrates it. Per-line history is stored in Redis sorted sets keyed station:{stationId}:line:{lineId}:history.
setTimeoutConnect(lineId, conn, 8h) arms an idle-disconnect interval per connection; keepConnectAPC / keepConnectStation arm the inverse — periodic \r\n / ENTER keep-alives. Keep these paired when adding new connection types or sessions silently die / outlive their usefulness.
Backend: app/services/ is a hierarchy of TCP session classes
Each class wraps a net.Socket to a piece of lab hardware and exposes a config object that the Socket.IO layer mutates and broadcasts:
station_connection.ts— top-level Cisco station console (used to issueclear line N,show line, etc.).line_connection.ts— individual serial line on a station; the heart of test orchestration. Owns scenario execution (runScript), DPELP flow, IOS/license loading (loadIosRouter,loadIosSwitch,loadLicenseRouter,loadLicenseSwitch), physical port testing (PhysicalPortTestfromphysical_test_service.ts), and AI log classification (usesPromptAimodel + axios calls).apc_connection.ts— APC PDU outlet control (on/off/restart/reconnect,navigateToOutlets).switch_connection.ts— managed switch port + PoE control.
When a line emits data, line_connection.ts parses it through TextFSM-style templates (app/ultils/templates/) and helper.ts parsers, and emits Socket.IO events (line_output, data_textfsm, running_scenario, feature_tested, summary_tested, test_port_physical, etc.) that the frontend subscribes to in App.tsx's big useEffect.
Backend: routes and AdonisJS subpath imports
start/routes.ts defines REST endpoints grouped by resource (api/stations, api/lines, api/logs, api/users, api/models, api/scenarios, api/auth, api/ticket, api/brands, api/categories, api/config-ram, api/keywords, api/prompt-ai, atc/health-check, api/ios, api/license). Controllers live under app/controllers/, models under app/models/.
The project relies heavily on AdonisJS subpath imports declared in package.json#imports — always use these, not relative paths:
#controllers/*,#models/*,#services/*,#middleware/*,#start/*,#config/*,#exceptions/*,#validators/*,#providers/*,#policies/*,#abilities/*,#database/*,#tests/*,#mails/*,#listeners/*,#events/*.
Note: socket_io_provider.ts itself uses relative paths to reach into app/ (e.g. '../app/services/line_connection.js') because it lives outside app/. Match the convention of the file you're editing.
Frontend: single-page app driven by Socket.IO events
src/main.tsx→src/App.tsxis the entire mounted tree.Main(default export) wrapsAppinMantineProvider+SocketProviderand gates onlocalStorage.user; if absent, it rendersPageLogininstead.src/context/SocketContext.tsxcreates a singlesocket.io-clientinstance authed with{ userId, userName }and provides it viauseSocket(). URL comes fromVITE_SOCKET_SERVER(defaulthttp://localhost:8989/).App.tsxkeeps the canonicalstations: TStation[]state. Most socket events arrive as line-level deltas and are funneled throughupdateValueLineStation(lineId, updates, stationId), which fans out toselectedLine/selectedLinesin lockstep.line_outputis special: its text is buffered inlineBuffersRef(aMap<lineId, string>) and flushed every ~50ms viaflushBuffersto keep terminal updates from causing per-character React renders.- Large log payloads arrive as
response_content_logchunks ({ fileId, chunkIndex, totalChunks, chunk }) and are reassembled into a single string before being shown.
UI is built with Mantine 8 (@mantine/core, @mantine/dates, @mantine/form, @mantine/hooks, @mantine/notifications), @dnd-kit/* for the draggable station tabs (DragTabs.tsx), @tabler/icons-react, and xterm (+ @xterm/addon-fit) for the terminal modal.
Conventions worth knowing before editing
- Folder name typos are intentional (or at least entrenched): backend uses
app/ultils/(helpers, types, templates) and frontend usessrc/untils/. Imports across the codebase reference these names; do not rename them as a "cleanup". - Two TCP-session lifecycles to remember: idle-timeout (
setTimeoutConnect, default 8h) and keep-alive (keepConnectAPC40s,keepConnectStation120s). When you add a new long-lived connection, wire both — otherwise it'll either disconnect silently or hold a slot forever. - State that survives restarts lives in Redis, not memory:
socket_state(fulllineMapsnapshot) andstation:{id}:line:{id}:historysorted sets. If you add per-line state that should persist across restarts, extendsaveState/restoreState. - Hot-reload boundaries (
hotHook.boundariesinBACKEND/package.json) are limited to controllers and middleware. Editing a service or provider file may require a full restart for changes to apply. - Comments and string literals frequently mix Vietnamese and English — preserve existing language when modifying nearby code.
- Auth is custom + minimal: routes under
api/auth/loginandapi/auth/registerare public;start/kernel.tsregisters anauthnamed middleware via@adonisjs/auth, but most routes instart/routes.tsare not currently behind it. The frontend gates onlocalStorage.useronly.