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:
152
AGENTS.md
152
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` |
|
| Drag & Drop (Rack Planner) | `@dnd-kit/core` + `@dnd-kit/sortable` |
|
||||||
| Backend | Node.js + Express (REST API) |
|
| Backend | Node.js + Express (REST API) |
|
||||||
| ORM / DB | Prisma ORM + SQLite (`better-sqlite3`) |
|
| 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 |
|
| Export | `html-to-image` or `dom-to-svg` for PNG export |
|
||||||
| Containerization | Docker (single container — Node serves Vite static build + API) |
|
| Containerization | Docker (single container — Node serves Vite static build + API) |
|
||||||
| Testing | Vitest + React Testing Library |
|
| Testing | Vitest + React Testing Library |
|
||||||
@@ -72,7 +72,7 @@ All data is persisted to SQLite via Prisma ORM with a migration-first workflow.
|
|||||||
├── client/
|
├── client/
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── main.tsx
|
│ │ ├── main.tsx
|
||||||
│ │ ├── App.tsx ← Router root; / = login, /rack = planner, /map = mapper
|
│ │ ├── App.tsx ← Router root; / → /rack, /rack, /map, /vlans
|
||||||
│ │ ├── store/
|
│ │ ├── store/
|
||||||
│ │ │ ├── useAuthStore.ts
|
│ │ │ ├── useAuthStore.ts
|
||||||
│ │ │ ├── useRackStore.ts
|
│ │ │ ├── useRackStore.ts
|
||||||
@@ -81,7 +81,10 @@ All data is persisted to SQLite via Prisma ORM with a migration-first workflow.
|
|||||||
│ │ │ ├── auth/ ← LoginPage, ProtectedRoute
|
│ │ │ ├── auth/ ← LoginPage, ProtectedRoute
|
||||||
│ │ │ ├── rack/ ← Rack Planner components
|
│ │ │ ├── rack/ ← Rack Planner components
|
||||||
│ │ │ ├── mapper/ ← Service Mapper 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
|
│ │ │ ├── modals/ ← All modal components
|
||||||
│ │ │ └── ui/ ← Shared primitives (Button, Badge, Tooltip, etc.)
|
│ │ │ └── ui/ ← Shared primitives (Button, Badge, Tooltip, etc.)
|
||||||
│ │ ├── hooks/
|
│ │ ├── hooks/
|
||||||
@@ -104,28 +107,17 @@ All data is persisted to SQLite via Prisma ORM with a migration-first workflow.
|
|||||||
### Strategy
|
### Strategy
|
||||||
|
|
||||||
- **Single admin account** — no registration UI, no user table in the database
|
- **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`
|
- Credentials are injected at runtime via Docker environment variables: `ADMIN_USERNAME` and `ADMIN_PASSWORD`
|
||||||
- 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
|
- 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`
|
- 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`
|
- All `/api/*` routes except `/api/auth/login` are protected by `authMiddleware.ts`
|
||||||
- Token expiry: `8h` (configurable via `JWT_EXPIRY` env var)
|
- 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
|
### Auth Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /api/auth/login { username, password }
|
POST /api/auth/login { username, password }
|
||||||
→ verify username === ADMIN_USERNAME
|
→ verify username === ADMIN_USERNAME && password === ADMIN_PASSWORD
|
||||||
→ bcrypt.compare(password, ADMIN_PASSWORD_HASH)
|
|
||||||
→ sign JWT { sub: 'admin', iat, exp }
|
→ sign JWT { sub: 'admin', iat, exp }
|
||||||
→ Set-Cookie: token=<jwt>; HttpOnly; SameSite=Strict; Secure; Path=/
|
→ Set-Cookie: token=<jwt>; HttpOnly; SameSite=Strict; Secure; Path=/
|
||||||
→ 200 OK { success: true }
|
→ 200 OK { success: true }
|
||||||
@@ -150,7 +142,7 @@ GET /api/auth/me
|
|||||||
|
|
||||||
```env
|
```env
|
||||||
ADMIN_USERNAME=admin
|
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_SECRET=your-secret-here # min 32 chars, random
|
||||||
JWT_EXPIRY=8h
|
JWT_EXPIRY=8h
|
||||||
DATABASE_URL=file:./data/rackmapper.db
|
DATABASE_URL=file:./data/rackmapper.db
|
||||||
@@ -164,47 +156,32 @@ NODE_ENV=production
|
|||||||
|
|
||||||
```prisma
|
```prisma
|
||||||
model Rack {
|
model Rack {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
totalU Int @default(42)
|
totalU Int @default(42)
|
||||||
location String?
|
location String?
|
||||||
displayOrder Int @default(0) // controls left-to-right order in side-by-side view
|
displayOrder Int @default(0)
|
||||||
modules Module[]
|
modules Module[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Module {
|
model Module {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
rackId String
|
rackId String
|
||||||
rack Rack @relation(fields: [rackId], references: [id], onDelete: Cascade)
|
rack Rack @relation(fields: [rackId], references: [id], onDelete: Cascade)
|
||||||
name String
|
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)
|
uPosition Int // 1-indexed from top (U1 = topmost slot)
|
||||||
uSize Int @default(1)
|
uSize Int @default(1)
|
||||||
manufacturer String?
|
manufacturer String?
|
||||||
model String?
|
model String?
|
||||||
ipAddress String?
|
ipAddress String?
|
||||||
notes String?
|
notes String?
|
||||||
ports Port[]
|
ports Port[]
|
||||||
serviceNodes ServiceNode[] // reverse relation — nodes in maps that reference this module
|
serviceNodes ServiceNode[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
|
||||||
|
|
||||||
enum ModuleType {
|
|
||||||
SWITCH
|
|
||||||
AGGREGATE_SWITCH
|
|
||||||
MODEM
|
|
||||||
ROUTER
|
|
||||||
NAS
|
|
||||||
PDU
|
|
||||||
PATCH_PANEL
|
|
||||||
SERVER
|
|
||||||
FIREWALL
|
|
||||||
AP
|
|
||||||
BLANK
|
|
||||||
OTHER
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Port {
|
model Port {
|
||||||
@@ -213,49 +190,32 @@ model Port {
|
|||||||
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
||||||
portNumber Int
|
portNumber Int
|
||||||
label String?
|
label String?
|
||||||
portType PortType @default(ETHERNET)
|
portType String @default("ETHERNET") // ETHERNET | SFP | SFP_PLUS | QSFP | CONSOLE | UPLINK
|
||||||
mode VlanMode @default(ACCESS)
|
mode String @default("ACCESS") // ACCESS | TRUNK | HYBRID
|
||||||
nativeVlan Int?
|
nativeVlan Int?
|
||||||
vlans PortVlan[]
|
vlans PortVlan[]
|
||||||
notes String?
|
notes String?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PortType {
|
|
||||||
ETHERNET
|
|
||||||
SFP
|
|
||||||
SFP_PLUS
|
|
||||||
QSFP
|
|
||||||
CONSOLE
|
|
||||||
UPLINK
|
|
||||||
}
|
|
||||||
|
|
||||||
enum VlanMode {
|
|
||||||
ACCESS
|
|
||||||
TRUNK
|
|
||||||
HYBRID
|
|
||||||
}
|
|
||||||
|
|
||||||
model Vlan {
|
model Vlan {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
vlanId Int @unique
|
vlanId Int @unique
|
||||||
name String
|
name String
|
||||||
description String?
|
description String?
|
||||||
color String? // hex color for UI display
|
color String?
|
||||||
ports PortVlan[]
|
ports PortVlan[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model PortVlan {
|
model PortVlan {
|
||||||
portId String
|
portId String
|
||||||
port Port @relation(fields: [portId], references: [id], onDelete: Cascade)
|
port Port @relation(fields: [portId], references: [id], onDelete: Cascade)
|
||||||
vlanId String
|
vlanId String
|
||||||
vlan Vlan @relation(fields: [vlanId], references: [id], onDelete: Cascade)
|
vlan Vlan @relation(fields: [vlanId], references: [id], onDelete: Cascade)
|
||||||
tagged Boolean @default(false)
|
tagged Boolean @default(false)
|
||||||
|
|
||||||
@@id([portId, vlanId])
|
@@id([portId, vlanId])
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Service Mapper ---
|
|
||||||
|
|
||||||
model ServiceMap {
|
model ServiceMap {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
@@ -267,35 +227,22 @@ model ServiceMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model ServiceNode {
|
model ServiceNode {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
mapId String
|
mapId String
|
||||||
map ServiceMap @relation(fields: [mapId], references: [id], onDelete: Cascade)
|
map ServiceMap @relation(fields: [mapId], references: [id], onDelete: Cascade)
|
||||||
label String
|
label String
|
||||||
nodeType NodeType
|
nodeType String // SERVICE | DATABASE | API | DEVICE | EXTERNAL | USER | VLAN | FIREWALL | LOAD_BALANCER | NOTE
|
||||||
positionX Float
|
positionX Float
|
||||||
positionY Float
|
positionY Float
|
||||||
metadata String? // JSON blob for arbitrary node-specific data
|
metadata String?
|
||||||
color String?
|
color String?
|
||||||
icon String?
|
icon String?
|
||||||
moduleId String? // optional link to a physical Rack Module
|
moduleId String?
|
||||||
module Module? @relation(fields: [moduleId], references: [id], onDelete: SetNull)
|
module Module? @relation(fields: [moduleId], references: [id], onDelete: SetNull)
|
||||||
sourceEdges ServiceEdge[] @relation("EdgeSource")
|
sourceEdges ServiceEdge[] @relation("EdgeSource")
|
||||||
targetEdges ServiceEdge[] @relation("EdgeTarget")
|
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 {
|
model ServiceEdge {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
mapId String
|
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:**
|
**Migration workflow:**
|
||||||
```bash
|
```bash
|
||||||
npx prisma migrate dev --name <descriptive_name> # dev: creates + applies migration
|
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) |
|
| DELETE | `/api/racks/:id` | Delete rack (cascades) |
|
||||||
| POST | `/api/racks/:id/modules` | Add a module to a rack |
|
| POST | `/api/racks/:id/modules` | Add a module to a rack |
|
||||||
| PUT | `/api/modules/:id` | Update module (position, size, metadata) |
|
| 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 |
|
| DELETE | `/api/modules/:id` | Remove a module |
|
||||||
| GET | `/api/modules/:id/ports` | Get ports for a module |
|
| GET | `/api/modules/:id/ports` | Get ports for a module |
|
||||||
| PUT | `/api/ports/:id` | Update port config (VLAN, mode, label) |
|
| 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
|
- Multi-select: Shift+click or drag-select box
|
||||||
- Edge types: `smoothstep` (default), `straight`, `bezier` — selectable per edge
|
- Edge types: `smoothstep` (default), `straight`, `bezier` — selectable per edge
|
||||||
- Animated edges for "active traffic" flows (toggle per edge)
|
- Animated edges for "active traffic" flows (toggle per edge)
|
||||||
- Right-click canvas → context menu: Add Node (type picker with icons)
|
- Right-click canvas → context menu: Add Node (all 10 types placed at cursor position)
|
||||||
- Right-click node → Edit, Duplicate, Delete, Link to Module
|
- Right-click node → Edit (label/colour/module link), Duplicate, Delete
|
||||||
- Right-click edge → Edit Label, Change Type, Toggle Animation, 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
|
### Persistence
|
||||||
|
|
||||||
@@ -567,9 +518,6 @@ npm run dev # Vite + Node (concurrently)
|
|||||||
npm run dev:client # Vite only
|
npm run dev:client # Vite only
|
||||||
npm run dev:server # Nodemon server only
|
npm run dev:server # Nodemon server only
|
||||||
|
|
||||||
# Auth
|
|
||||||
npx ts-node scripts/hashPassword.ts mypassword # generate bcrypt hash for env var
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
npx prisma migrate dev --name <name> # create + apply dev migration
|
npx prisma migrate dev --name <name> # create + apply dev migration
|
||||||
npx prisma migrate deploy # apply in production / Docker
|
npx prisma migrate deploy # apply in production / Docker
|
||||||
@@ -608,8 +556,8 @@ environment:
|
|||||||
- PORT=3001
|
- PORT=3001
|
||||||
- DATABASE_URL=file:./data/rackmapper.db
|
- DATABASE_URL=file:./data/rackmapper.db
|
||||||
- ADMIN_USERNAME=admin
|
- ADMIN_USERNAME=admin
|
||||||
- ADMIN_PASSWORD_HASH=$2a$12$... # bcrypt hash
|
- ADMIN_PASSWORD=yourpassword # plain text
|
||||||
- JWT_SECRET=... # min 32 chars
|
- JWT_SECRET=... # min 32 chars, random
|
||||||
- JWT_EXPIRY=8h
|
- JWT_EXPIRY=8h
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data # persists SQLite file across container restarts
|
- ./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.
|
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`.
|
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.
|
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.
|
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.
|
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