DCIM: rack and power management for a live data center
What it is
An in-house data center infrastructure management (DCIM) console for a hosting company. It tracks the physical layer and how it connects: data centers, racks, devices, PDUs, network ports, and which PDU socket feeds which PSU bay.
Every mutation is role-gated on the server and written to an audit log. Operators wire and re-wire hardware. Viewers audit what is wired where.
The stack is a React 19 + Vite console, an Express API, and Drizzle ORM on MariaDB 11, deployed as a Docker Compose stack behind nginx.
What runs where
Browser (React 19 console)
│
│ session auth (Better Auth)
▼
┌──────────────────────────────┐
│ nginx + certbot │ TLS termination
└────────┬─────────────────────┘
▼
┌──────────────────────────────┐
│ Express API │
│ requireRole() ──────────────│── super_admin / operator / viewer
│ Drizzle ORM ────────────────│── typed SQL, migrations in CI
└────────┬─────────────────────┘
▼
┌──────────────────────────────┐
│ MariaDB 11 │
└──────────────────────────────┘
Every connect and disconnect writes an audit row.
How it works
The power model
Power wiring is first-class data, not a diagram. Each PDU socket row carries connected_device_id and connected_psu_slot, so "what feeds this device" is a query instead of tribal knowledge.
Connecting a socket is one PUT /api/pdu-sockets/:socketId. The server rejects a slot that is already fed elsewhere, and the UI renders the 400 as an inline error on that socket's control.
Power Map: wire a rack from one page
The PDU detail table worked one PDU at a time. Wiring a full rack meant opening every PDU in turn. Power Map gives operators one page per rack: a deep-linkable rack selection (/power-map?rack=<id>, so browser back and shared links work), a summary bar with server-computed wired power, and one card per PDU.
Editing is optimistic, with debounced coalesced commits. A burst of changes becomes one request, and the row updates after the server confirms.
Network Map: wire a rack's ports
Power has Power Map; the network side works the same way. Network Map picks a rack from the URL (?rack=<id>), renders one card per switch or router, and wires servers to switch ports in place.
Each card stacks two views. SwitchFrontPanel draws the switch as an operator sees it: ports grouped in pairs with a wider gap every eight, colored green when connected or up, orange when disabled, gray otherwise. PortWiringTable does the wiring against the same ports.
A connection is a row in network_ports, so patching is data, not a diagram. Each port carries port_number, port_speed (10G default), connector_type (SFP28 default), status (up, down, disabled), cable_type, and mac_address. Wiring server port to switch port sets connected_port_id, a self-referencing foreign key, and the change lands in the audit log like every other mutation.
IPs and VLANs as queryable data
Every server and router carries management_ip and mac_address as schema columns, and the servers table is indexed on the IP, so "which device holds this address" is an index lookup.
The CSV import pipeline keeps that data honest at the boundary. Raw rack exports ship loose headers (ip, primary mac, vlan, port_sw); the field-mapping layer normalizes them into typed columns, so an IP validates as an IP and a VLAN lands as an integer, not a string.
Roles and audit
requireRole() gates each route server-side; the UI only mirrors it. Wiring changes stay behind the operator role. Viewers get read-only rendering with mutating controls omitted. Every connect and disconnect writes an audit row, so "who touched this rack, when" always has an answer.
Tests that don't need a running stack
Pure logic lives in server/lib/* and module-level functions, so aggregation and mapping code unit-tests without a DOM or a database. Playwright E2E covers the rest against a seeded MariaDB; the CI seed uses a dedicated password, so real credentials never enter the pipeline. pnpm lint runs tsc --noEmit and acts as the merge gatekeeper.
Why these choices
| Decision | Rationale |
|---|---|
| Drizzle over a heavier ORM | Typed SQL without a query DSL to learn. Migrations run in CI before any test touches the schema |
| A MariaDB sidecar per test stage | Unit and E2E suites get isolated databases. No shared state, no order dependence, torn down even on red |
| TanStack Query + Zustand | Query owns server state and invalidates on connect/disconnect. Zustand owns the small client-side bits |
snake_case payloads mirrored in .d.ts | One type per payload on both sides of the wire. The compiler catches drift |
| Debounced, coalesced commits | One PUT per editing burst instead of one per keystroke. Fewer round trips, cleaner audit trail |
Self-referencing network_ports.connected_port_id | A patch is one row, and "what is this port plugged into" is one lookup. No join table, no duplicated endpoint |
| console-kit primitives | PageHeader, Card, and Toolbar are shared across pages, so the console reads as one product |
Where it stands
Feature-complete and handed to my senior. The app is in the migration & testing phase on the company's infrastructure.