9.3 KiB
| name | description |
|---|---|
| backend | 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 isserver.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:runto apply migrations fromdatabase/migrations/.node ace db:seedfor seeders indatabase/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/(notutils). 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 outsideapp/(e.g.providers/socket_io_provider.ts) reach intoapp/with relative paths (../app/services/line_connection.js). Match the convention of the file you're editing. - All ESM imports keep the
.jsextension even when importing.tssource — that's required by AdonisJS's NodeNext resolution.import { foo } from './bar.js'resolves to./bar.tsat build time. - Comments mix Vietnamese and English. Preserve existing language in nearby code.
- Error handling style: existing code wraps risky blocks in
try/catchandconsole.logs the error rather than rethrowing. Match it for socket handlers — an unhandled throw inside asocket.onhandler 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'))inready()).node ace migration:runwon't open the socket port — don't "fix" this. - It runs an independent
http.createServer()onSOCKET_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)
- Add
socket.on('your_event', async (data) => { ... })inside theconnectionhandler insocket_io_provider.ts. - Validate
stationIdis active when the event triggers hardware:const ok = await checkStationActive(stationId); if (!ok) return;. - Route hardware actions through
handleLineOperationorhandleStationOperation. They auto-reconnect a dropped TCP session and arm the idle-timeout interval. Never calllineConn.writeCommand(...)directly from a socket handler. - To respond to the requesting socket only, use
io.to(socket.id).emit('event_response', payload). To broadcast (e.g. ticket updates), useio.emit(...). - Cleanup: if your event creates any
setInterval/setTimeout, store it in the existingintervalMap/intervalKeepConnectdictionaries keyed bylineId/ip— not local closure variables — so it can be cleared on disconnect/reconnect.
Example shape (matches get_line_history):
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 forclear line N,show line, etc.line_connection.ts— individual serial line. Owns scenario execution (runScript), DPELP flow, IOS/license loading, physical-port testing (PhysicalPortTestfromphysical_test_service.ts), AI log classification (usesPromptAimodel + axios).apc_connection.ts— APC PDU outlet control.switch_connection.ts— managed switch port + PoE control.
When extending one of these:
- Mutate
this.configrather than creating parallel state — the WebSocket layer readslineMap.get(id)?.configdirectly. - Append to
this.config.outputand 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)fromapp/ultils/helper.ts— that writes the system log file understorage/system_logs/. - Always provide a
disconnect()method, even if it just callsthis.client.destroy().setTimeoutConnectin the provider calls it.
The two interval lifecycles (don't forget the second one)
Every long-lived TCP session needs both:
- Idle disconnect —
setTimeoutConnect(id, conn, 8h_default)in the provider. Stored inintervalMap[id]. Resets on each operation throughhandleLineOperation. - Keep-alive —
keepConnectAPC(40s ENTER) /keepConnectStation(120s\r\n). Stored inintervalKeepConnect[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— fulllineMapsnapshot (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 viaLineConnection.addHistory, read viaLineConnection.getHistoryor the privategetHistoryinWebSocketIo. Auto-pruned to last 96h on eachaddHistory.
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>thennode 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
BaseModelfrom@adonisjs/lucid/ormand use@column()decorators. Seeapp/models/line.tsas the reference. database/seeders/index.tsis wired up; add new seeders alongsideprompt_ai_seeder.tsand 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
ultilstypo. - Don't open a second Socket.IO server or HTTP server in another provider.
- Don't
throwfrom asocket.onhandler — wrap intry/catchandconsole.log. - Don't bypass
handleLineOperation/handleStationOperationfor 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.outputwithout truncating — runaway buffers crash Redis snapshots. - Don't import without the
.jsextension; NodeNext ESM will fail at runtime. - Don't edit a previously-applied migration; add a new one.