diff --git a/.claude/skills/backend/SKILL.md b/.claude/skills/backend/SKILL.md deleted file mode 100644 index 03b18ff..0000000 --- a/.claude/skills/backend/SKILL.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -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. diff --git a/.claude/skills/frontend/SKILL.md b/.claude/skills/frontend/SKILL.md deleted file mode 100644 index 28f4dda..0000000 --- a/.claude/skills/frontend/SKILL.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: frontend -description: Use when adding/modifying anything in FRONTEND/ — new components, modals, socket events, state, routing, Mantine UI, terminal/xterm work. Covers conventions, the App.tsx state-fanout pattern, the SocketContext, the line-buffering renderer, and the modal stacking rules. ---- - -# FRONTEND skill - -Scope: everything under `FRONTEND/` (React 19 + Vite + TypeScript + Mantine 8 + socket.io-client + xterm). - -Run from `FRONTEND/`: -- `npm run dev` (Vite, port 5173) -- `npm run build` (`tsc -b && vite build`) -- `npm run lint` (flat-config ESLint) - -Env: `VITE_BACKEND_URL` (REST), `VITE_SOCKET_SERVER` (Socket.IO; default `http://localhost:8989/`). - -## Conventions to preserve - -- **Folder typo is intentional:** `src/untils/` (not `utils`). Imports across the app reference `../untils/...` — do not rename. -- **Mantine 8 only.** Don't reach for raw HTML form elements or other UI kits when a Mantine component exists. Same for `@tabler/icons-react` for icons. -- **Comments mix Vietnamese and English.** Preserve existing language in nearby code. -- **`localStorage.user`** is the single source of truth for the logged-in user. The shape is parsed JSON `{ id, userName, email, ... }`. Use `isJsonString` from `untils/helper` before parsing. -- **`apiUrl` pattern:** every file that calls REST does `const apiUrl = import.meta.env.VITE_BACKEND_URL;` at the top, then `axios.get(apiUrl + "api/...")`. Match it. - -## App.tsx is the canonical store - -`src/App.tsx` holds `stations: TStation[]` and is the only place that owns line-level state. Everything else reads from props. - -When you handle a new socket event that updates a line, **always go through `updateValueLineStation(lineId, updates, stationId)`** — it fans the update out to `selectedLine` and `selectedLines` in lockstep. Manually calling `setStations` will desync the terminal modal from the cards. - -`line_output` is the exception: it's batched in `lineBuffersRef: Map` and flushed every ~50ms by `flushBuffers`. If you add a new high-frequency stream (anything that fires more than ~10 Hz), reuse this batch path — don't dispatch per event, you'll cause per-character React renders that lock up the UI. - -## SocketContext is the only place that creates a socket - -`src/context/SocketContext.tsx` instantiates one `socket.io-client` connection authed with `{ userId, userName }` and exposes it via `useSocket()`. **Never call `io(...)` from a component.** New components consume the socket via `useSocket()` or accept a `socket: Socket | null` prop from a parent that already has it. - -When subscribing in a component, the rules are: -1. Always `socket.off("event", handler)` in the effect cleanup with the same handler reference. -2. Don't subscribe to events that `App.tsx` already listens to — `App.tsx`'s handler will fire too and you'll get double updates. Filter inside your handler instead (e.g. by `stationId`/`lineId`). -3. Re-subscribe when the relevant ID prop changes — include `stationId`/`lineId` in the dependency array. - -## Adding a new socket round-trip (FE side) - -Pattern, copy-pasted from `ModalLineHistory.tsx`: - -```tsx -useEffect(() => { - if (!socket || !opened) return; - const handler = (data: { stationId: number; lineId: number; ...: ... }) => { - if (data?.stationId !== stationId || data?.lineId !== lineId) return; - // setState(...) - }; - socket.on("event_name", handler); - return () => { socket.off("event_name", handler); }; -}, [socket, opened, stationId, lineId]); - -useEffect(() => { - if (!socket || !opened || !stationId || !lineId) return; - socket.emit("event_name_request", { stationId, lineId }); -}, [socket, opened, stationId, lineId]); -``` - -The filter `data.stationId !== stationId || data.lineId !== lineId` is mandatory — the backend broadcasts to the requesting socket, but a single socket can have multiple modals open. - -## Modals: prefer `` from Mantine - -Look at `ModalLineHistory.tsx` for the canonical small modal, `ModalTerminal.tsx` for the large multi-panel one. - -- **Stacking:** the terminal modal uses `zIndex={95}`. Modals opened from inside it must use a higher `zIndex` (use `1000000` to stay above any portaled content). -- **Close-guarding:** when adding a new inner modal opened from `ModalTerminal`, add its `open` flag to the close-guard around line `560` of `ModalTerminal.tsx` so Escape on the inner modal doesn't bubble up and close the terminal too: - ```ts - if (openSelectIos || openLineHistory || /* your flag */ || ...) return; - ``` -- **Don't roll a custom backdrop.** The previous version of `ModalLineHistory.tsx` used a fixed-position `
` overlay — that's been replaced with ``. Don't reintroduce the pattern. - -## Types live in `src/untils/types.ts` - -`TLine`, `TStation`, `TUser`, `IScenario`, `TBrands`, `TCategories`, `TextFSM`, etc. all come from there. Backend can send the same field in two casings — `lineNumber` vs `line_number`, `stationId` vs `station_id`, `apcName` vs `apc_name`. **Always read both:** `line.lineNumber || line.line_number`. When passing into a new socket emit, normalize to one form. - -## Terminal (xterm) - -`src/components/TerminalXTerm.tsx` is the only consumer of `xterm` + `@xterm/addon-fit`. It receives `content` (latest delta), `initContent` (full buffer for hydration), `loadingClearTerminal`, and `isClearKeepScrollBack` from `ModalTerminal`. The `[CLEAR_TERMINAL_SCROLL_BACK]` sentinel string in the buffer is the contract for "clear scrollback but keep history" — if you add a new clear mode, follow the same sentinel approach rather than adding a new prop. - -## DnD-Kit station tabs - -`src/components/DragTabs.tsx` owns the draggable station header. If you add a new tab-level action, plug it in here, not in `App.tsx`. - -## Don't - -- Don't fix the `untils` typo. -- Don't add a new socket connection. -- Don't bypass `updateValueLineStation` to mutate `stations` directly when a line update arrives. -- Don't add a per-message render — batch via `lineBuffersRef` if frequency is high. -- Don't mock UI/UX changes only with type-checks; for any visible change, run `npm run dev` and walk through the affected flow in the browser before reporting done. diff --git a/.gitignore b/.gitignore index 5c16e0b..e4248a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ # Secret cục bộ — chứa token xác thực, KHÔNG commit. config.local.js +.claude \ No newline at end of file