Update documentation to reflect current build state
RREADME.md: rewrite from TBD to full user-facing README with features, quick-start, environment variable table, and tech stack. AGENTS.md: - Auth: ADMIN_PASSWORD_HASH → ADMIN_PASSWORD (plain text); remove bcryptjs - Schema: replace enum blocks with String fields + SQLite/Prisma enum warning - Repo structure: add vlans/, ContextMenu.tsx, NodeEditModal.tsx, /vlans route - API routes: add POST /modules/:id/move - Service Mapper canvas features: update to reflect implemented context menus, NodeEditModal, and edge type/animation toggle - Commands: remove hashPassword.ts entry - Docker env block: update to ADMIN_PASSWORD - Key Decision #3: updated to plain-text password rationale Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
110
AGENTS.md
110
AGENTS.md
@@ -29,7 +29,7 @@ All data is persisted to SQLite via Prisma ORM with a migration-first workflow.
|
||||
| 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 |
|
||||
| Auth | JWT via `jsonwebtoken`, `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 |
|
||||
@@ -72,7 +72,7 @@ All data is persisted to SQLite via Prisma ORM with a migration-first workflow.
|
||||
├── client/
|
||||
│ ├── src/
|
||||
│ │ ├── main.tsx
|
||||
│ │ ├── App.tsx ← Router root; / = login, /rack = planner, /map = mapper
|
||||
│ │ ├── App.tsx ← Router root; / → /rack, /rack, /map, /vlans
|
||||
│ │ ├── store/
|
||||
│ │ │ ├── useAuthStore.ts
|
||||
│ │ │ ├── useRackStore.ts
|
||||
@@ -81,7 +81,10 @@ All data is persisted to SQLite via Prisma ORM with a migration-first workflow.
|
||||
│ │ │ ├── auth/ ← LoginPage, ProtectedRoute
|
||||
│ │ │ ├── rack/ ← Rack Planner components
|
||||
│ │ │ ├── mapper/ ← Service Mapper components
|
||||
│ │ │ │ └── nodes/ ← Custom React Flow node components
|
||||
│ │ │ │ ├── nodes/ ← Custom React Flow node components
|
||||
│ │ │ │ ├── ContextMenu.tsx
|
||||
│ │ │ │ └── NodeEditModal.tsx
|
||||
│ │ │ ├── vlans/ ← VlanPage (full CRUD at /vlans)
|
||||
│ │ │ ├── modals/ ← All modal components
|
||||
│ │ │ └── ui/ ← Shared primitives (Button, Badge, Tooltip, etc.)
|
||||
│ │ ├── hooks/
|
||||
@@ -104,28 +107,17 @@ All data is persisted to SQLite via Prisma ORM with a migration-first workflow.
|
||||
### 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
|
||||
- Credentials are injected at runtime via Docker environment variables: `ADMIN_USERNAME` and `ADMIN_PASSWORD`
|
||||
- Password is stored as plain text in the environment variable — change it by updating the Docker env var and restarting the container
|
||||
- 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)
|
||||
→ verify username === ADMIN_USERNAME && password === ADMIN_PASSWORD
|
||||
→ sign JWT { sub: 'admin', iat, exp }
|
||||
→ Set-Cookie: token=<jwt>; HttpOnly; SameSite=Strict; Secure; Path=/
|
||||
→ 200 OK { success: true }
|
||||
@@ -150,7 +142,7 @@ GET /api/auth/me
|
||||
|
||||
```env
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD_HASH=$2a$12$... # bcrypt hash, generated via scripts/hashPassword.ts
|
||||
ADMIN_PASSWORD=yourpassword # plain text; change by updating env var + restarting container
|
||||
JWT_SECRET=your-secret-here # min 32 chars, random
|
||||
JWT_EXPIRY=8h
|
||||
DATABASE_URL=file:./data/rackmapper.db
|
||||
@@ -168,7 +160,7 @@ model Rack {
|
||||
name String
|
||||
totalU Int @default(42)
|
||||
location String?
|
||||
displayOrder Int @default(0) // controls left-to-right order in side-by-side view
|
||||
displayOrder Int @default(0)
|
||||
modules Module[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -179,7 +171,7 @@ model Module {
|
||||
rackId String
|
||||
rack Rack @relation(fields: [rackId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
type ModuleType
|
||||
type String // ModuleType: SWITCH | AGGREGATE_SWITCH | MODEM | ROUTER | NAS | PDU | PATCH_PANEL | SERVER | FIREWALL | AP | BLANK | OTHER
|
||||
uPosition Int // 1-indexed from top (U1 = topmost slot)
|
||||
uSize Int @default(1)
|
||||
manufacturer String?
|
||||
@@ -187,60 +179,30 @@ model Module {
|
||||
ipAddress String?
|
||||
notes String?
|
||||
ports Port[]
|
||||
serviceNodes ServiceNode[] // reverse relation — nodes in maps that reference this module
|
||||
serviceNodes ServiceNode[]
|
||||
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)
|
||||
portType String @default("ETHERNET") // ETHERNET | SFP | SFP_PLUS | QSFP | CONSOLE | UPLINK
|
||||
mode String @default("ACCESS") // ACCESS | TRUNK | HYBRID
|
||||
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
|
||||
color String?
|
||||
ports PortVlan[]
|
||||
}
|
||||
|
||||
@@ -254,8 +216,6 @@ model PortVlan {
|
||||
@@id([portId, vlanId])
|
||||
}
|
||||
|
||||
// --- Service Mapper ---
|
||||
|
||||
model ServiceMap {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
@@ -271,31 +231,18 @@ model ServiceNode {
|
||||
mapId String
|
||||
map ServiceMap @relation(fields: [mapId], references: [id], onDelete: Cascade)
|
||||
label String
|
||||
nodeType NodeType
|
||||
nodeType String // SERVICE | DATABASE | API | DEVICE | EXTERNAL | USER | VLAN | FIREWALL | LOAD_BALANCER | NOTE
|
||||
positionX Float
|
||||
positionY Float
|
||||
metadata String? // JSON blob for arbitrary node-specific data
|
||||
metadata String?
|
||||
color String?
|
||||
icon String?
|
||||
moduleId String? // optional link to a physical Rack Module
|
||||
moduleId String?
|
||||
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
|
||||
@@ -311,6 +258,8 @@ model ServiceEdge {
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ **SQLite / Prisma limitation:** Prisma does not support `enum` types with the SQLite connector. All enum-like fields (`type`, `portType`, `mode`, `nodeType`) are stored as `String`. Valid values are defined as TypeScript string literal unions in `server/lib/constants.ts` and mirrored in `client/src/types/index.ts`. Do **not** add Prisma `enum` declarations to this schema.
|
||||
|
||||
**Migration workflow:**
|
||||
```bash
|
||||
npx prisma migrate dev --name <descriptive_name> # dev: creates + applies migration
|
||||
@@ -354,6 +303,7 @@ Do not pre-seed VLANs, racks, or modules unless the user explicitly requests it.
|
||||
| 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) |
|
||||
| POST | `/api/modules/:id/move` | Move module to different rack/position (collision-validated) |
|
||||
| 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) |
|
||||
@@ -510,9 +460,10 @@ Each `NodeType` has a custom React Flow node component in `client/src/components
|
||||
- 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
|
||||
- Right-click canvas → context menu: Add Node (all 10 types placed at cursor position)
|
||||
- Right-click node → Edit (label/colour/module link), Duplicate, Delete
|
||||
- Right-click edge → Toggle Animation, set edge type (bezier/smooth/step/straight), Delete
|
||||
- Double-click node → `NodeEditModal` (label, accent colour swatch + custom picker, rack module link)
|
||||
|
||||
### Persistence
|
||||
|
||||
@@ -567,9 +518,6 @@ 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
|
||||
@@ -608,8 +556,8 @@ environment:
|
||||
- PORT=3001
|
||||
- DATABASE_URL=file:./data/rackmapper.db
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD_HASH=$2a$12$... # bcrypt hash
|
||||
- JWT_SECRET=... # min 32 chars
|
||||
- ADMIN_PASSWORD=yourpassword # plain text
|
||||
- JWT_SECRET=... # min 32 chars, random
|
||||
- JWT_EXPIRY=8h
|
||||
volumes:
|
||||
- ./data:/app/data # persists SQLite file across container restarts
|
||||
@@ -652,7 +600,7 @@ The Dockerfile should run `npx prisma migrate deploy && node dist/index.js` as t
|
||||
|
||||
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.
|
||||
3. **Single admin account via env vars** — no user table, no registration. Password is plain text in `ADMIN_PASSWORD`. Admin changes it by updating the Docker/Unraid env var and restarting the container. No bcrypt dependency.
|
||||
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.
|
||||
|
||||
103
RREADME.md
103
RREADME.md
@@ -1 +1,102 @@
|
||||
TBD
|
||||
# RackMapper
|
||||
|
||||
A self-hosted, dark-mode web app for visualising and managing network rack infrastructure. Built for Unraid / Docker single-container deployment.
|
||||
|
||||
## Features
|
||||
|
||||
### Rack Planner (`/rack`)
|
||||
- Drag-and-drop module placement from a device palette onto U-slots
|
||||
- Drag modules between racks or reorder racks via header grip
|
||||
- Resize modules by dragging the bottom handle
|
||||
- Click any module to edit name, IP, manufacturer, model, notes, uSize
|
||||
- Port indicator dots — click any dot to open the port configuration modal
|
||||
- Set mode (Access / Trunk / Hybrid), native VLAN, tagged VLANs
|
||||
- Quick-create VLANs without leaving the modal
|
||||
- Export the full rack view as PNG
|
||||
|
||||
### Service Mapper (`/map`)
|
||||
- React Flow canvas for mapping service dependencies and traffic flows
|
||||
- Right-click canvas → add any node type at cursor position
|
||||
- Right-click node → Edit, Duplicate, Delete
|
||||
- Right-click edge → Toggle animation, change edge type, Delete
|
||||
- Double-click a node → edit label, accent colour, and rack module link
|
||||
- Auto-populate nodes from all rack modules ("Import Rack" button)
|
||||
- Connect nodes by dragging from handles; Delete key removes selected items
|
||||
- Minimap, zoom controls, snap-to-grid (15px), PNG export
|
||||
|
||||
### VLAN Management (`/vlans`)
|
||||
- Create, edit, and delete VLANs with ID, name, description, and colour
|
||||
- VLANs defined here are available in all port configuration modals
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Docker Compose)
|
||||
|
||||
**1. Create a `docker-compose.yml`:**
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
rackmapper:
|
||||
image: rackmapper
|
||||
build: .
|
||||
container_name: rackmapper
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3001
|
||||
- DATABASE_URL=file:./data/rackmapper.db
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=yourpassword
|
||||
- JWT_SECRET=your-random-secret-min-32-chars
|
||||
- JWT_EXPIRY=8h
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
**2. Build and run:**
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
**3. Open** `http://localhost:3001` and log in with the credentials above.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `ADMIN_USERNAME` | Yes | `admin` | Login username |
|
||||
| `ADMIN_PASSWORD` | Yes | — | Login password (plain text) |
|
||||
| `JWT_SECRET` | Yes | — | Secret for signing JWTs (min 32 chars) |
|
||||
| `JWT_EXPIRY` | No | `8h` | Session token lifetime |
|
||||
| `DATABASE_URL` | No | `file:./data/rackmapper.db` | SQLite file path |
|
||||
| `PORT` | No | `3001` | HTTP port |
|
||||
| `NODE_ENV` | No | — | Set to `production` in Docker |
|
||||
|
||||
To change the password, update `ADMIN_PASSWORD` in your Docker environment and restart the container.
|
||||
|
||||
---
|
||||
|
||||
## Data Persistence
|
||||
|
||||
The SQLite database is stored at `./data/rackmapper.db` inside the container. Mount `./data:/app/data` to persist it across container restarts (already included in the compose file above).
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Frontend | React 18 + TypeScript + Vite |
|
||||
| Styling | Tailwind CSS (dark-mode only) |
|
||||
| State | Zustand |
|
||||
| Node Graph | React Flow (`@xyflow/react` v12+) |
|
||||
| Drag & Drop | `@dnd-kit/core` + `@dnd-kit/sortable` |
|
||||
| Backend | Node.js + Express |
|
||||
| Database | SQLite via Prisma ORM (`better-sqlite3`) |
|
||||
| Auth | JWT in `httpOnly` cookie |
|
||||
| Containerisation | Docker — single container serves API + static build |
|
||||
|
||||
Reference in New Issue
Block a user