--- name: backend description: Use when adding/modifying anything in BACKEND/ — REST controllers, models, migrations, socket events, TCP session classes (line/station/APC/switch), Redis state, AdonisJS providers/middleware. Covers the socket_io_provider architecture, the handleLineOperation pattern, Redis history keys, idle-timeout vs keep-alive intervals, and the import alias rules. --- # BACKEND skill Scope: everything under `BACKEND/` (AdonisJS 6, TypeScript, ESM, MySQL via Lucid, Redis, Socket.IO, raw TCP/Telnet to lab hardware). Run from `BACKEND/`: - `npm run dev` — `node ace serve --hmr` (HTTP + Socket.IO providers boot together). - `npm start` — `node bin/server.js` (production; the Socket.IO provider only opens its WS port when entry is `server.js`). - `npm test` — Japa. Single test: `node ace test --files "tests/unit/foo.spec.ts"`. - `npm run typecheck` / `npm run lint` / `npm run format`. - `node ace migration:run` to apply migrations from `database/migrations/`. - `node ace db:seed` for seeders in `database/seeders/`. Env: `SOCKET_PORT` (default 8989), `FRONTEND_URL` (CORS origin), MySQL/Redis creds, `ZULIP_STREAM_AU/US`, `ZULIP_TOPIC_AU`, `LINK_WIKI`, `TIME_ZONE`. Local Redis is required. ## Conventions to preserve - **Folder typo is intentional:** `app/ultils/` (not `utils`). Imports across the project reference `../ultils/...` and `#models/...`-style aliases. Do not rename. - **Always use AdonisJS subpath imports** declared in `package.json#imports`: `#controllers/*`, `#models/*`, `#services/*`, `#middleware/*`, `#start/*`, `#config/*`, `#exceptions/*`, `#validators/*`, `#providers/*`, `#policies/*`, `#abilities/*`, `#database/*`, `#tests/*`. **Exception:** files outside `app/` (e.g. `providers/socket_io_provider.ts`) reach into `app/` with **relative paths** (`../app/services/line_connection.js`). Match the convention of the file you're editing. - **All ESM imports keep the `.js` extension** even when importing `.ts` source — that's required by AdonisJS's NodeNext resolution. `import { foo } from './bar.js'` resolves to `./bar.ts` at build time. - **Comments mix Vietnamese and English.** Preserve existing language in nearby code. - **Error handling style:** existing code wraps risky blocks in `try/catch` and `console.log`s the error rather than rethrowing. Match it for socket handlers — an unhandled throw inside a `socket.on` handler crashes the whole event loop. ## `providers/socket_io_provider.ts` is the engine The HTTP API (controllers in `app/controllers/`) is intentionally thin. Real behavior lives in `WebSocketIo` inside `socket_io_provider.ts`. Key invariants: - The class only boots its WS server when the process entry is `server.js` (`if (process.argv[1].includes('server.js'))` in `ready()`). `node ace migration:run` won't open the socket port — don't "fix" this. - It runs an **independent** `http.createServer()` on `SOCKET_PORT`, separate from the AdonisJS HTTP port. - `SocketIoProvider.io` (static) is the way other modules reach the socket server post-boot. In-memory maps that ARE the live system state: - `lineMap: Map` - `stationMap: Map` - `apcsControl: Map` - `switchControl: Map` - `userConnecting: Map` `saveState()` runs every 10s and serializes a slimmed `lineMap` (status forced to `disconnected`, output truncated to last 5000 chars, CLI ownership cleared) into Redis under key `socket_state`. `restoreState()` rehydrates on boot. **If you add per-line state that should survive restart, extend both methods.** ## Adding a new socket event (the standard pattern) 1. Add `socket.on('your_event', async (data) => { ... })` inside the `connection` handler in `socket_io_provider.ts`. 2. **Validate `stationId` is active** when the event triggers hardware: `const ok = await checkStationActive(stationId); if (!ok) return;`. 3. **Route hardware actions through `handleLineOperation` or `handleStationOperation`.** They auto-reconnect a dropped TCP session and arm the idle-timeout interval. Never call `lineConn.writeCommand(...)` directly from a socket handler. 4. To respond to the **requesting socket only**, use `io.to(socket.id).emit('event_response', payload)`. To broadcast (e.g. ticket updates), use `io.emit(...)`. 5. Cleanup: if your event creates any `setInterval`/`setTimeout`, store it in the existing `intervalMap` / `intervalKeepConnect` dictionaries keyed by `lineId`/`ip` — not local closure variables — so it can be cleared on disconnect/reconnect. Example shape (matches `get_line_history`): ```ts socket.on('your_event', async (data) => { const stationId = Number(data?.stationId) const lineId = Number(data?.lineId) if (!stationId || !lineId) { return io.to(socket.id).emit('your_event_response', { ok: false }) } const result = await this.someHelper(stationId, lineId) io.to(socket.id).emit('your_event_response', { stationId, lineId, result }) }) ``` ## TCP session classes (`app/services/`) Hierarchy: each class wraps `net.Socket` to a piece of lab hardware and exposes a mutable `config` object that the Socket.IO layer broadcasts. - `station_connection.ts` — top-level Cisco station console. Used for `clear line N`, `show line`, etc. - `line_connection.ts` — individual serial line. Owns scenario execution (`runScript`), DPELP flow, IOS/license loading, physical-port testing (`PhysicalPortTest` from `physical_test_service.ts`), AI log classification (uses `PromptAi` model + axios). - `apc_connection.ts` — APC PDU outlet control. - `switch_connection.ts` — managed switch port + PoE control. When extending one of these: - Mutate `this.config` rather than creating parallel state — the WebSocket layer reads `lineMap.get(id)?.config` directly. - Append to `this.config.output` and slice to the last 5000 chars (`config.output = config.output.slice(-5000)`). Unbounded buffers blow memory and break Redis snapshots. - For long output streams, also call `appendLog(cleanData(message), id, name, ip, lineNumber)` from `app/ultils/helper.ts` — that writes the system log file under `storage/system_logs/`. - Always provide a `disconnect()` method, even if it just calls `this.client.destroy()`. `setTimeoutConnect` in the provider calls it. ## The two interval lifecycles (don't forget the second one) Every long-lived TCP session needs both: 1. **Idle disconnect** — `setTimeoutConnect(id, conn, 8h_default)` in the provider. Stored in `intervalMap[id]`. Resets on each operation through `handleLineOperation`. 2. **Keep-alive** — `keepConnectAPC` (40s ENTER) / `keepConnectStation` (120s `\r\n`). Stored in `intervalKeepConnect[ip|id]`. If you add a new connection type and skip the keep-alive, sessions die silently after the firewall/device idle window. If you skip the idle disconnect, dead sockets pile up. ## Redis keys in use - `socket_state` — full `lineMap` snapshot (slimmed). Written every 10s. - `station:{stationId}:line:{lineId}:history` — sorted set, score = timestamp ms, value = JSON `{ pid, vid, sn, scenario, id, number, stationId, timestamp }`. Write via `LineConnection.addHistory`, read via `LineConnection.getHistory` or the private `getHistory` in `WebSocketIo`. Auto-pruned to last 96h on each `addHistory`. When adding new Redis keys, follow the colon-namespaced convention and document them here. ## Routes (`start/routes.ts`) REST is 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`. Verb pattern is `GET /` for list, `POST create`, `POST update`, `POST delete` (or `DELETE delete/:id` for tickets). Match this when adding a new resource. Auth is mostly absent — routes are not currently behind the `auth` named middleware. Don't assume `request.user` is present unless you've added the middleware to that route group. ## Migrations & models - New table: `node ace make:migration ` then `node ace make:model `. Migrations are timestamped; never edit a migration that's been run on a shared environment — write a new one. - Models extend `BaseModel` from `@adonisjs/lucid/orm` and use `@column()` decorators. See `app/models/line.ts` as the reference. - `database/seeders/index.ts` is wired up; add new seeders alongside `prompt_ai_seeder.ts` and import them there. ## Hot-reload boundaries `hotHook.boundaries` in `package.json` is `app/controllers/**/*.ts` and `app/middleware/*.ts`. Editing a service, model, provider, or `start/*.ts` requires a full restart (`Ctrl+C`, `npm run dev` again) — HMR will not pick up the change. ## Don't - Don't fix the `ultils` typo. - Don't open a second Socket.IO server or HTTP server in another provider. - Don't `throw` from a `socket.on` handler — wrap in `try/catch` and `console.log`. - Don't bypass `handleLineOperation`/`handleStationOperation` for hardware ops; you'll skip the auto-reconnect and idle-timer reset. - Don't store long-lived intervals in local closure variables; put them in `intervalMap`/`intervalKeepConnect`. - Don't append to `config.output` without truncating — runaway buffers crash Redis snapshots. - Don't import without the `.js` extension; NodeNext ESM will fail at runtime. - Don't edit a previously-applied migration; add a new one.