listing_ebay/.claude/skills/backend/SKILL.md

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 devnode ace serve --hmr (HTTP + Socket.IO providers boot together).
  • npm startnode 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.logs 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):

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 disconnectsetTimeoutConnect(id, conn, 8h_default) in the provider. Stored in intervalMap[id]. Resets on each operation through handleLineOperation.
  2. Keep-alivekeepConnectAPC (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.