ATC_SIMPLE/CLAUDE.md

91 lines
9.0 KiB
Markdown

# 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 via `node 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's `ready()` only boots the WS server when the entry is `server.js`).
- `npm test``node ace test` (Japa). Suites are configured in `adonisrc.ts`: `unit` (`tests/unit/**/*.spec.ts`) and `functional` (`tests/functional/**/*.spec.ts`).
- `npm run lint` / `npm run format` / `npm run typecheck`.
- `node ace migration:run` — apply MySQL migrations from `database/migrations/`.
- `node ace db:seed` — run seeders in `database/seeders/` (e.g. `prompt_ai_seeder.ts`).
- Run a single test: `node ace test --files "tests/unit/foo.spec.ts"` (Japa filter; `--tags` and `--match` also 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 reads `SOCKET_PORT` (default 8989), `FRONTEND_URL` (CORS origin), MySQL/Redis creds, Zulip stream/topic, `LINK_WIKI`, `TIME_ZONE`. Frontend reads `VITE_BACKEND_URL` and `VITE_SOCKET_SERVER`.
- A local Redis server is required (the README documents `apt install redis-server` for 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 issue `clear 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 (`PhysicalPortTest` from `physical_test_service.ts`), and AI log classification (uses `PromptAi` model + 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.tsx` is the entire mounted tree. `Main` (default export) wraps `App` in `MantineProvider` + `SocketProvider` and gates on `localStorage.user`; if absent, it renders `PageLogin` instead.
- `src/context/SocketContext.tsx` creates a single `socket.io-client` instance authed with `{ userId, userName }` and provides it via `useSocket()`. URL comes from `VITE_SOCKET_SERVER` (default `http://localhost:8989/`).
- `App.tsx` keeps the canonical `stations: TStation[]` state. Most socket events arrive as line-level deltas and are funneled through `updateValueLineStation(lineId, updates, stationId)`, which fans out to `selectedLine` / `selectedLines` in lockstep. `line_output` is special: its text is buffered in `lineBuffersRef` (a `Map<lineId, string>`) and flushed every ~50ms via `flushBuffers` to keep terminal updates from causing per-character React renders.
- Large log payloads arrive as `response_content_log` chunks (`{ 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 uses `src/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 (`keepConnectAPC` 40s, `keepConnectStation` 120s). 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` (full `lineMap` snapshot) and `station:{id}:line:{id}:history` sorted sets. If you add per-line state that should persist across restarts, extend `saveState` / `restoreState`.
- **Hot-reload boundaries** (`hotHook.boundaries` in `BACKEND/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/login` and `api/auth/register` are public; `start/kernel.ts` registers an `auth` named middleware via `@adonisjs/auth`, but most routes in `start/routes.ts` are not currently behind it. The frontend gates on `localStorage.user` only.