ATC_SIMPLE/.claude/skills/backend/SKILL.md

124 lines
9.3 KiB
Markdown

---
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<lineId, LineConnection>`
- `stationMap: Map<stationId, StationConnection>`
- `apcsControl: Map<apcIp, APCController>`
- `switchControl: Map<switchIp, SwitchController>`
- `userConnecting: Map<userId, ...>`
`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 <name>` then `node ace make:model <Name>`. 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.