Upload files to "/"
This commit is contained in:
675
AGENTS.md
Normal file
675
AGENTS.md
Normal file
@@ -0,0 +1,675 @@
|
||||
# AGENTS.md — RackMapper
|
||||
|
||||
> **Project:** RackMapper — Web-based Network Rack Planner & Service Mapper
|
||||
> **Stack:** Node.js · React · TypeScript · SQLite · Prisma ORM · Docker
|
||||
> **Audience:** This file is the primary briefing for AI coding agents and senior devs onboarding to the project.
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
RackMapper is a two-module, dark-mode, full-stack web application for network infrastructure management:
|
||||
|
||||
1. **Rack Planner** — A visual, drag-and-drop rack diagram builder. Users populate standard 19" racks (42U default) with modular devices (switches, aggregate switches, modems, routers, NAS units, PDUs, patch panels, blanks, etc.), resize devices to span multiple U-spaces, and configure networking devices through interactive port modals with VLAN assignment. Multiple racks are displayed side-by-side when more than one rack exists.
|
||||
|
||||
2. **Service Mapper** — A node-graph canvas (React Flow) where users visually map service interactions, traffic flows, and dependencies between infrastructure components, applications, and external services. Device nodes can be auto-populated from Rack Module data and then freely repositioned on the canvas.
|
||||
|
||||
All data is persisted to SQLite via Prisma ORM with a migration-first workflow. The app is web-facing and protected by a single hardcoded admin account configured via Docker/Unraid environment variables.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Frontend | React 18 + TypeScript + Vite |
|
||||
| Styling | Tailwind CSS (dark-mode first, `darkMode: 'class'`) |
|
||||
| State Management | Zustand |
|
||||
| Node Graph (Service Mapper) | React Flow (`@xyflow/react` v12+) |
|
||||
| Drag & Drop (Rack Planner) | `@dnd-kit/core` + `@dnd-kit/sortable` |
|
||||
| Backend | Node.js + Express (REST API) |
|
||||
| ORM / DB | Prisma ORM + SQLite (`better-sqlite3`) |
|
||||
| Auth | JWT via `jsonwebtoken` + `bcryptjs`, `httpOnly` cookie strategy |
|
||||
| Export | `html-to-image` or `dom-to-svg` for PNG export |
|
||||
| Containerization | Docker (single container — Node serves Vite static build + API) |
|
||||
| Testing | Vitest + React Testing Library |
|
||||
| Linting/Formatting | ESLint + Prettier |
|
||||
|
||||
---
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── AGENTS.md ← this file
|
||||
├── README.md
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
├── .env.example
|
||||
├── prisma/
|
||||
│ ├── schema.prisma
|
||||
│ └── migrations/
|
||||
├── server/
|
||||
│ ├── index.ts ← Express entry point
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.ts ← POST /api/auth/login, POST /api/auth/logout
|
||||
│ │ ├── racks.ts
|
||||
│ │ ├── modules.ts
|
||||
│ │ ├── ports.ts
|
||||
│ │ ├── vlans.ts
|
||||
│ │ └── serviceMap.ts
|
||||
│ ├── middleware/
|
||||
│ │ ├── authMiddleware.ts ← JWT verification, applied to all /api/* except /api/auth
|
||||
│ │ └── errorHandler.ts
|
||||
│ ├── services/ ← Business logic, all Prisma queries live here
|
||||
│ │ ├── rackService.ts
|
||||
│ │ ├── moduleService.ts
|
||||
│ │ ├── portService.ts
|
||||
│ │ ├── vlanService.ts
|
||||
│ │ └── mapService.ts
|
||||
│ └── lib/
|
||||
│ └── prisma.ts ← PrismaClient singleton
|
||||
├── client/
|
||||
│ ├── src/
|
||||
│ │ ├── main.tsx
|
||||
│ │ ├── App.tsx ← Router root; / = login, /rack = planner, /map = mapper
|
||||
│ │ ├── store/
|
||||
│ │ │ ├── useAuthStore.ts
|
||||
│ │ │ ├── useRackStore.ts
|
||||
│ │ │ └── useMapStore.ts
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── auth/ ← LoginPage, ProtectedRoute
|
||||
│ │ │ ├── rack/ ← Rack Planner components
|
||||
│ │ │ ├── mapper/ ← Service Mapper components
|
||||
│ │ │ │ └── nodes/ ← Custom React Flow node components
|
||||
│ │ │ ├── modals/ ← All modal components
|
||||
│ │ │ └── ui/ ← Shared primitives (Button, Badge, Tooltip, etc.)
|
||||
│ │ ├── hooks/
|
||||
│ │ ├── api/
|
||||
│ │ │ └── client.ts ← ALL fetch calls go through here, never in components
|
||||
│ │ ├── types/
|
||||
│ │ │ └── index.ts ← Shared TypeScript interfaces
|
||||
│ │ └── lib/
|
||||
│ │ └── constants.ts ← Device types, U-height defaults, VLAN ranges
|
||||
│ └── index.html
|
||||
└── tests/
|
||||
├── unit/
|
||||
└── integration/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
### Strategy
|
||||
|
||||
- **Single admin account** — no registration UI, no user table in the database
|
||||
- Credentials are injected at runtime via Docker environment variables: `ADMIN_USERNAME` and `ADMIN_PASSWORD_HASH`
|
||||
- Password is stored as a `bcryptjs` hash (never plaintext) — generate the hash once with a seed script and store it in Unraid's Docker template as an environment variable
|
||||
- JWT issued on login, stored in an `httpOnly`, `SameSite=Strict`, `Secure` cookie — never `localStorage`
|
||||
- All `/api/*` routes except `/api/auth/login` are protected by `authMiddleware.ts`
|
||||
- Token expiry: `8h` (configurable via `JWT_EXPIRY` env var)
|
||||
|
||||
### Hash Generation Utility
|
||||
|
||||
Provide a one-time script at `scripts/hashPassword.ts`:
|
||||
```ts
|
||||
// Usage: npx ts-node scripts/hashPassword.ts mypassword
|
||||
import bcrypt from 'bcryptjs';
|
||||
const hash = await bcrypt.hash(process.argv[2], 12);
|
||||
console.log(hash); // paste this into ADMIN_PASSWORD_HASH env var
|
||||
```
|
||||
|
||||
### Auth Flow
|
||||
|
||||
```
|
||||
POST /api/auth/login { username, password }
|
||||
→ verify username === ADMIN_USERNAME
|
||||
→ bcrypt.compare(password, ADMIN_PASSWORD_HASH)
|
||||
→ sign JWT { sub: 'admin', iat, exp }
|
||||
→ Set-Cookie: token=<jwt>; HttpOnly; SameSite=Strict; Secure; Path=/
|
||||
→ 200 OK { success: true }
|
||||
|
||||
POST /api/auth/logout
|
||||
→ Clear cookie
|
||||
→ 200 OK
|
||||
|
||||
GET /api/auth/me
|
||||
→ Returns { authenticated: true } if valid cookie, else 401
|
||||
```
|
||||
|
||||
### Frontend Auth
|
||||
|
||||
- `ProtectedRoute` component wraps all app routes — redirects to `/login` if not authenticated
|
||||
- `useAuthStore` (Zustand) holds `{ isAuthenticated, loading }` state
|
||||
- On app mount, call `GET /api/auth/me` to verify session — show full-screen loader during check
|
||||
- Login page: centered card, dark-mode, RackMapper logo/wordmark, username + password fields, submit button, error toast on failure
|
||||
- No "forgot password" — admin resets by updating the Docker env var
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD_HASH=$2a$12$... # bcrypt hash, generated via scripts/hashPassword.ts
|
||||
JWT_SECRET=your-secret-here # min 32 chars, random
|
||||
JWT_EXPIRY=8h
|
||||
DATABASE_URL=file:./data/rackmapper.db
|
||||
PORT=3001
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema (Prisma)
|
||||
|
||||
```prisma
|
||||
model Rack {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
totalU Int @default(42)
|
||||
location String?
|
||||
displayOrder Int @default(0) // controls left-to-right order in side-by-side view
|
||||
modules Module[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Module {
|
||||
id String @id @default(cuid())
|
||||
rackId String
|
||||
rack Rack @relation(fields: [rackId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
type ModuleType
|
||||
uPosition Int // 1-indexed from top (U1 = topmost slot)
|
||||
uSize Int @default(1)
|
||||
manufacturer String?
|
||||
model String?
|
||||
ipAddress String?
|
||||
notes String?
|
||||
ports Port[]
|
||||
serviceNodes ServiceNode[] // reverse relation — nodes in maps that reference this module
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum ModuleType {
|
||||
SWITCH
|
||||
AGGREGATE_SWITCH
|
||||
MODEM
|
||||
ROUTER
|
||||
NAS
|
||||
PDU
|
||||
PATCH_PANEL
|
||||
SERVER
|
||||
FIREWALL
|
||||
AP
|
||||
BLANK
|
||||
OTHER
|
||||
}
|
||||
|
||||
model Port {
|
||||
id String @id @default(cuid())
|
||||
moduleId String
|
||||
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
||||
portNumber Int
|
||||
label String?
|
||||
portType PortType @default(ETHERNET)
|
||||
mode VlanMode @default(ACCESS)
|
||||
nativeVlan Int?
|
||||
vlans PortVlan[]
|
||||
notes String?
|
||||
}
|
||||
|
||||
enum PortType {
|
||||
ETHERNET
|
||||
SFP
|
||||
SFP_PLUS
|
||||
QSFP
|
||||
CONSOLE
|
||||
UPLINK
|
||||
}
|
||||
|
||||
enum VlanMode {
|
||||
ACCESS
|
||||
TRUNK
|
||||
HYBRID
|
||||
}
|
||||
|
||||
model Vlan {
|
||||
id String @id @default(cuid())
|
||||
vlanId Int @unique
|
||||
name String
|
||||
description String?
|
||||
color String? // hex color for UI display
|
||||
ports PortVlan[]
|
||||
}
|
||||
|
||||
model PortVlan {
|
||||
portId String
|
||||
port Port @relation(fields: [portId], references: [id], onDelete: Cascade)
|
||||
vlanId String
|
||||
vlan Vlan @relation(fields: [vlanId], references: [id], onDelete: Cascade)
|
||||
tagged Boolean @default(false)
|
||||
|
||||
@@id([portId, vlanId])
|
||||
}
|
||||
|
||||
// --- Service Mapper ---
|
||||
|
||||
model ServiceMap {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
nodes ServiceNode[]
|
||||
edges ServiceEdge[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ServiceNode {
|
||||
id String @id @default(cuid())
|
||||
mapId String
|
||||
map ServiceMap @relation(fields: [mapId], references: [id], onDelete: Cascade)
|
||||
label String
|
||||
nodeType NodeType
|
||||
positionX Float
|
||||
positionY Float
|
||||
metadata String? // JSON blob for arbitrary node-specific data
|
||||
color String?
|
||||
icon String?
|
||||
moduleId String? // optional link to a physical Rack Module
|
||||
module Module? @relation(fields: [moduleId], references: [id], onDelete: SetNull)
|
||||
sourceEdges ServiceEdge[] @relation("EdgeSource")
|
||||
targetEdges ServiceEdge[] @relation("EdgeTarget")
|
||||
}
|
||||
|
||||
enum NodeType {
|
||||
SERVICE
|
||||
DATABASE
|
||||
API
|
||||
DEVICE // links to a Module via moduleId
|
||||
EXTERNAL
|
||||
USER
|
||||
VLAN
|
||||
FIREWALL
|
||||
LOAD_BALANCER
|
||||
NOTE
|
||||
}
|
||||
|
||||
model ServiceEdge {
|
||||
id String @id @default(cuid())
|
||||
mapId String
|
||||
map ServiceMap @relation(fields: [mapId], references: [id], onDelete: Cascade)
|
||||
sourceId String
|
||||
source ServiceNode @relation("EdgeSource", fields: [sourceId], references: [id], onDelete: Cascade)
|
||||
targetId String
|
||||
target ServiceNode @relation("EdgeTarget", fields: [targetId], references: [id], onDelete: Cascade)
|
||||
label String?
|
||||
edgeType String @default("smoothstep")
|
||||
animated Boolean @default(false)
|
||||
metadata String?
|
||||
}
|
||||
```
|
||||
|
||||
**Migration workflow:**
|
||||
```bash
|
||||
npx prisma migrate dev --name <descriptive_name> # dev: creates + applies migration
|
||||
npx prisma migrate deploy # production/Docker: applies pending
|
||||
npx prisma generate # regenerate client after any schema change
|
||||
npx prisma studio # visual DB browser at localhost:5555
|
||||
```
|
||||
|
||||
> ⚠️ **Never** manually edit migration files after they are applied. Create a new migration for every schema change. Never use `prisma db push` — it skips migration history.
|
||||
|
||||
---
|
||||
|
||||
## Seed
|
||||
|
||||
`prisma/seed.ts` starts **blank** — no default VLANs, no sample racks. It should only:
|
||||
- Log a confirmation message that the DB is ready
|
||||
- Be a no-op safe to run multiple times
|
||||
|
||||
Do not pre-seed VLANs, racks, or modules unless the user explicitly requests it.
|
||||
|
||||
---
|
||||
|
||||
## API Routes
|
||||
|
||||
### Auth
|
||||
|
||||
| Method | Endpoint | Auth Required | Description |
|
||||
|---|---|---|---|
|
||||
| POST | `/api/auth/login` | No | Login, sets httpOnly cookie |
|
||||
| POST | `/api/auth/logout` | No | Clears cookie |
|
||||
| GET | `/api/auth/me` | Yes | Session check |
|
||||
|
||||
### Rack Planner
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/racks` | List all racks (ordered by `displayOrder`) |
|
||||
| POST | `/api/racks` | Create a rack |
|
||||
| GET | `/api/racks/:id` | Get rack with all modules and ports |
|
||||
| PUT | `/api/racks/:id` | Update rack metadata or displayOrder |
|
||||
| DELETE | `/api/racks/:id` | Delete rack (cascades) |
|
||||
| POST | `/api/racks/:id/modules` | Add a module to a rack |
|
||||
| PUT | `/api/modules/:id` | Update module (position, size, metadata) |
|
||||
| DELETE | `/api/modules/:id` | Remove a module |
|
||||
| GET | `/api/modules/:id/ports` | Get ports for a module |
|
||||
| PUT | `/api/ports/:id` | Update port config (VLAN, mode, label) |
|
||||
| GET | `/api/vlans` | List all VLANs |
|
||||
| POST | `/api/vlans` | Create a VLAN |
|
||||
| PUT | `/api/vlans/:id` | Update VLAN |
|
||||
| DELETE | `/api/vlans/:id` | Delete VLAN |
|
||||
|
||||
### Service Mapper
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/maps` | List all service maps |
|
||||
| POST | `/api/maps` | Create a new map |
|
||||
| GET | `/api/maps/:id` | Get full map (nodes + edges) |
|
||||
| PUT | `/api/maps/:id` | Update map metadata |
|
||||
| DELETE | `/api/maps/:id` | Delete map |
|
||||
| POST | `/api/maps/:id/nodes` | Add node |
|
||||
| POST | `/api/maps/:id/populate` | Auto-populate nodes from rack modules |
|
||||
| PUT | `/api/nodes/:id` | Update node (position, label, meta) |
|
||||
| DELETE | `/api/nodes/:id` | Remove node |
|
||||
| POST | `/api/maps/:id/edges` | Add edge |
|
||||
| PUT | `/api/edges/:id` | Update edge |
|
||||
| DELETE | `/api/edges/:id` | Remove edge |
|
||||
|
||||
---
|
||||
|
||||
## Module 1 — Rack Planner
|
||||
|
||||
### Visual Design
|
||||
|
||||
- Background: `#0f1117` (deep slate), rack column: `#1e2433` with silver/chrome border
|
||||
- Each rack rendered as a vertical U-slot grid; U1 is the **topmost slot**
|
||||
- Modules render as colored blocks spanning their `uSize`; color driven by `ModuleType`
|
||||
- Empty U-slots show faint dashed outline as drop targets
|
||||
- Each networking module shows: label, type badge, IP (if set), and a compact row of port indicator dots
|
||||
|
||||
### Multi-Rack Layout
|
||||
|
||||
- **Single rack:** centered full-height column
|
||||
- **2+ racks:** side-by-side horizontal flex layout, each rack same height, scrollable horizontally if > 3
|
||||
- Rack order controlled by `displayOrder` field; user can reorder via drag handle on rack header
|
||||
- "Add Rack" button always visible in the top toolbar
|
||||
|
||||
### Interaction Model
|
||||
|
||||
- **Add module:** sidebar device-type palette → drag to U-slot, or click slot to place with type selector
|
||||
- **Resize module:** drag handle at bottom edge → expands downward; validate no overlap before commit
|
||||
- **Move module:** drag to new U-position in same rack or different rack; validate collision
|
||||
- **Edit module:** click → slide-out panel (name, IP, manufacturer, model, notes, uSize)
|
||||
- **Delete module:** trash icon on hover → confirm tooltip
|
||||
|
||||
### Port Configuration Modal
|
||||
|
||||
Triggered by clicking any port indicator dot on applicable modules.
|
||||
|
||||
**Modal fields:**
|
||||
- Port number + label (editable)
|
||||
- Port type (read-only, set at module creation)
|
||||
- Mode: `Access` / `Trunk` / `Hybrid`
|
||||
- Native VLAN: dropdown (populated from VLAN list)
|
||||
- Tagged VLANs: multi-select (visible only in Trunk/Hybrid mode)
|
||||
- Notes field
|
||||
- Inline VLAN quick-create (add new VLAN ID + name without leaving modal)
|
||||
- Save / Cancel
|
||||
|
||||
**Devices with interactive ports:** `SWITCH`, `AGGREGATE_SWITCH`, `ROUTER`, `FIREWALL`, `PATCH_PANEL`, `AP`, `MODEM`
|
||||
**Devices without ports:** `NAS`, `PDU`, `BLANK` (SERVER optionally has ports)
|
||||
|
||||
### Port Count & U-Size Defaults (`constants.ts`)
|
||||
|
||||
```ts
|
||||
export const MODULE_PORT_DEFAULTS: Record<ModuleType, number> = {
|
||||
SWITCH: 24,
|
||||
AGGREGATE_SWITCH: 48,
|
||||
ROUTER: 4,
|
||||
FIREWALL: 8,
|
||||
PATCH_PANEL: 24,
|
||||
AP: 1,
|
||||
MODEM: 2,
|
||||
SERVER: 2,
|
||||
NAS: 0,
|
||||
PDU: 0,
|
||||
BLANK: 0,
|
||||
OTHER: 0,
|
||||
};
|
||||
|
||||
export const MODULE_U_DEFAULTS: Record<ModuleType, number> = {
|
||||
SWITCH: 1,
|
||||
AGGREGATE_SWITCH: 2,
|
||||
ROUTER: 1,
|
||||
FIREWALL: 1,
|
||||
PATCH_PANEL: 1,
|
||||
AP: 1,
|
||||
MODEM: 1,
|
||||
SERVER: 2,
|
||||
NAS: 4,
|
||||
PDU: 1,
|
||||
BLANK: 1,
|
||||
OTHER: 1,
|
||||
};
|
||||
```
|
||||
|
||||
Ports are **auto-generated** when a Module is created based on these defaults. Users should not need to manually add ports.
|
||||
|
||||
### PNG Export
|
||||
|
||||
- Export button in rack toolbar: captures the entire rack view (all side-by-side racks) as a single PNG
|
||||
- Use `html-to-image` (`toPng`) targeting the rack canvas container ref
|
||||
- Filename: `rackmapper-rack-<timestamp>.png`
|
||||
- Show a brief "Exporting..." toast during capture
|
||||
|
||||
---
|
||||
|
||||
## Module 2 — Service Mapper
|
||||
|
||||
### Library
|
||||
|
||||
Use **React Flow (`@xyflow/react` v12+)**. Do not substitute with D3, Cytoscape, or Dagre without approval.
|
||||
|
||||
### Device-to-Module Linking
|
||||
|
||||
- `DeviceNode` carries an optional `moduleId` field linking it to a physical `Module` in the rack
|
||||
- When `moduleId` is set, the node displays the module's name, type badge, and IP address pulled from the linked record
|
||||
- Clicking a linked `DeviceNode` opens a read-only info panel showing full module detail with a "View in Rack" button that navigates to `/rack` and highlights that module
|
||||
- Nodes are **fully draggable** regardless of module linkage — position on the canvas is independent of rack position
|
||||
|
||||
### Auto-Populate from Rack
|
||||
|
||||
`POST /api/maps/:id/populate` fetches all Modules across all racks and creates a `DeviceNode` for each one that doesn't already have a node in the map. Nodes are arranged in a grid layout (column per rack, rows per module in U-order). This is a one-way import — subsequent module additions must be manually added or re-triggered.
|
||||
|
||||
### Custom Node Types
|
||||
|
||||
Each `NodeType` has a custom React Flow node component in `client/src/components/mapper/nodes/`:
|
||||
|
||||
| Node Type | Component | Visual Style |
|
||||
|---|---|---|
|
||||
| `DEVICE` | `DeviceNode.tsx` | Rack icon; accent border color by ModuleType; shows IP if linked |
|
||||
| `SERVICE` | `ServiceNode.tsx` | Rounded card, colored left border, icon + label |
|
||||
| `DATABASE` | `DatabaseNode.tsx` | Cylinder icon, dark teal accent |
|
||||
| `API` | `ApiNode.tsx` | Badge style with optional method tag (REST/gRPC/WS) |
|
||||
| `EXTERNAL` | `ExternalNode.tsx` | Dashed border, cloud icon |
|
||||
| `VLAN` | `VlanNode.tsx` | Colored square matching VLAN color swatch |
|
||||
| `FIREWALL` | `FirewallNode.tsx` | Shield icon, red accent |
|
||||
| `LOAD_BALANCER` | `LBNode.tsx` | Scale/balance icon |
|
||||
| `USER` | `UserNode.tsx` | Person icon, neutral gray |
|
||||
| `NOTE` | `NoteNode.tsx` | Sticky note style, no handles, free text |
|
||||
|
||||
### Canvas Features
|
||||
|
||||
- Minimap (bottom-right, dark themed)
|
||||
- Controls: zoom in/out, fit view, lock toggle
|
||||
- Background: dot grid pattern (`#1e2433` dots)
|
||||
- Snap-to-grid: 15px
|
||||
- Multi-select: Shift+click or drag-select box
|
||||
- Edge types: `smoothstep` (default), `straight`, `bezier` — selectable per edge
|
||||
- Animated edges for "active traffic" flows (toggle per edge)
|
||||
- Right-click canvas → context menu: Add Node (type picker with icons)
|
||||
- Right-click node → Edit, Duplicate, Delete, Link to Module
|
||||
- Right-click edge → Edit Label, Change Type, Toggle Animation, Delete
|
||||
|
||||
### Persistence
|
||||
|
||||
Debounce all position/data changes by 500ms after drag end — PATCH to API. Never save on every pixel move. Use React Flow's `onNodesChange` and `onEdgesChange` callbacks.
|
||||
|
||||
### PNG Export
|
||||
|
||||
- "Export PNG" button in Service Mapper toolbar
|
||||
- Use `html-to-image` (`toPng`) on the React Flow container
|
||||
- Temporarily hide minimap and controls panel during capture, restore after
|
||||
- Filename: `rackmapper-map-<mapName>-<timestamp>.png`
|
||||
|
||||
---
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
### TypeScript
|
||||
|
||||
- `"strict": true` in tsconfig — no exceptions
|
||||
- No `any` — use `unknown` with type guards if necessary
|
||||
- All shared types in `client/src/types/index.ts` and `server/types/index.ts`
|
||||
- Prefer `interface` for object shapes, `type` for unions/aliases
|
||||
|
||||
### React
|
||||
|
||||
- Functional components only — no class components
|
||||
- Custom hooks for all non-trivial stateful logic (`use` prefix, in `/hooks`)
|
||||
- All API calls go through `client/src/api/client.ts` — never raw `fetch`/`axios` in components
|
||||
- Prefer small, focused components; avoid prop drilling beyond 2 levels — lift to Zustand
|
||||
|
||||
### Styling
|
||||
|
||||
- Tailwind CSS only — no inline styles, no CSS modules unless strictly required
|
||||
- Dark mode is the only supported mode
|
||||
- Use `cn()` utility (clsx + tailwind-merge) for conditional class composition
|
||||
- Color palette: `slate-900` bg · `slate-800` surface · `slate-700` border · `blue-500` accent · `red-500` destructive
|
||||
|
||||
### Backend
|
||||
|
||||
- Route handlers are thin — all business logic in `server/services/`
|
||||
- All Prisma queries in service files — never inline in route handlers
|
||||
- Throw typed `AppError` instances; catch in `errorHandler` middleware
|
||||
- Consistent JSON response shape: `{ data, error, meta }`
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npm run dev # Vite + Node (concurrently)
|
||||
npm run dev:client # Vite only
|
||||
npm run dev:server # Nodemon server only
|
||||
|
||||
# Auth
|
||||
npx ts-node scripts/hashPassword.ts mypassword # generate bcrypt hash for env var
|
||||
|
||||
# Database
|
||||
npx prisma migrate dev --name <name> # create + apply dev migration
|
||||
npx prisma migrate deploy # apply in production / Docker
|
||||
npx prisma generate # regenerate client after schema change
|
||||
npx prisma studio # visual DB browser at localhost:5555
|
||||
npx prisma db seed # run seed (no-op by default)
|
||||
|
||||
# Build
|
||||
npm run build # Vite build + tsc check
|
||||
npm run typecheck # tsc --noEmit — run before every PR
|
||||
|
||||
# Lint / Format
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
npm run format # Prettier --write
|
||||
|
||||
# Tests
|
||||
npm run test
|
||||
npm run test:watch
|
||||
npm run test -- path/to/file.test.ts
|
||||
|
||||
# Docker
|
||||
docker compose up --build -d
|
||||
docker compose down
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker / Unraid Configuration
|
||||
|
||||
**`docker-compose.yml` environment block:**
|
||||
```yaml
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3001
|
||||
- DATABASE_URL=file:./data/rackmapper.db
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD_HASH=$2a$12$... # bcrypt hash
|
||||
- JWT_SECRET=... # min 32 chars
|
||||
- JWT_EXPIRY=8h
|
||||
volumes:
|
||||
- ./data:/app/data # persists SQLite file across container restarts
|
||||
```
|
||||
|
||||
The Dockerfile should run `npx prisma migrate deploy && node dist/index.js` as the startup command to auto-apply any pending migrations on container start.
|
||||
|
||||
---
|
||||
|
||||
## Agent Permissions
|
||||
|
||||
### Allowed without asking
|
||||
|
||||
- Read any file
|
||||
- Run `npm run lint`, `npm run typecheck`, `npm run format`
|
||||
- Run `vitest` on a single file
|
||||
- Run `npx prisma generate`
|
||||
- Edit files in `client/src/`, `server/`, `prisma/schema.prisma`
|
||||
|
||||
### Ask first
|
||||
|
||||
- `npm install` or adding new dependencies
|
||||
- `npx prisma migrate dev` (schema-changing migration)
|
||||
- Deleting files
|
||||
- Modifying `docker-compose.yml` or `Dockerfile`
|
||||
- Running `npm run build`
|
||||
- Any `git push` or commit
|
||||
|
||||
### Never do
|
||||
|
||||
- Edit migration files that have already been applied
|
||||
- Commit secrets or `.env` files
|
||||
- Hard-code credentials, IP addresses, or VLAN IDs
|
||||
- Run `prisma db push` — migrations only
|
||||
- Rewrite the entire repo in one diff — prefer targeted, minimal changes
|
||||
|
||||
---
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **SQLite over PostgreSQL** — intentional for single-container Unraid deployment. No external DB process. Do not suggest migrating unless asked.
|
||||
2. **httpOnly cookie auth** — chosen over `localStorage` for XSS resistance on a web-facing deployment. Do not change to `localStorage`.
|
||||
3. **Single admin account via env vars** — no user table, no registration. Admin resets password by updating the Unraid Docker template env var and restarting the container.
|
||||
4. **U1 = top of rack** — all U-position logic is 1-indexed from the top. Validate and render accordingly.
|
||||
5. **`@dnd-kit` over `react-beautiful-dnd`** — `react-beautiful-dnd` is unmaintained.
|
||||
6. **React Flow for Service Mapper** — first-class TypeScript, custom node API, active maintenance. Do not swap.
|
||||
7. **Zustand over Redux** — intentional for this app's scope. Do not introduce Redux or Context API for global state.
|
||||
8. **Ports auto-generated on Module creation** — use `MODULE_PORT_DEFAULTS`. Do not require manual port addition.
|
||||
9. **DeviceNode position is canvas-independent** — linking a node to a Module does not constrain its canvas position.
|
||||
10. **VLAN seed is blank** — the user creates all VLANs manually. Do not pre-seed VLAN records.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility & UX Standards
|
||||
|
||||
- All modals: keyboard navigable (Tab order, Escape to close, Enter to confirm)
|
||||
- All interactive elements: `aria-label` or visible label
|
||||
- Drag-and-drop: keyboard fallback (click-to-select, arrow key movement)
|
||||
- Toast notifications via `sonner` for all async success/error states — no silent failures
|
||||
- Destructive actions (delete rack, module, map) require a confirmation dialog, not just a toast
|
||||
- Skeleton loaders (not spinners) for layout-heavy async views
|
||||
- Login page must show a clear error message on failed auth — no vague "something went wrong"
|
||||
Reference in New Issue
Block a user