From bcb8a95fae4c34f1bf8df5c0413b1374eba8bc5d Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 21 Mar 2026 22:05:42 -0500 Subject: [PATCH] Switch auth to plain-text password env var (remove bcrypt) - Replace ADMIN_PASSWORD_HASH with ADMIN_PASSWORD in auth route and docker-compose - Remove bcryptjs / @types/bcryptjs dependencies - Delete scripts/hashPassword.ts (no longer needed) Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.local.json | 14 ++ client/src/api/client.ts | 2 + .../src/components/modals/AddModuleModal.tsx | 26 +- .../src/components/modals/PortConfigModal.tsx | 10 +- client/src/components/rack/DevicePalette.tsx | 81 ++++--- client/src/components/rack/RackColumn.tsx | 58 +++-- client/src/components/rack/RackPlanner.tsx | 223 +++++++++++++++--- client/src/components/rack/RackSlot.tsx | 33 ++- client/src/store/useRackStore.ts | 19 ++ docker-compose.yml | 2 +- package-lock.json | 15 -- package.json | 2 - scripts/hashPassword.ts | 20 -- server/routes/auth.ts | 13 +- server/routes/modules.ts | 9 + server/services/moduleService.ts | 36 +++ 16 files changed, 407 insertions(+), 156 deletions(-) create mode 100644 .claude/settings.local.json delete mode 100644 scripts/hashPassword.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..b684854 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(npm install:*)", + "Bash(mkdir -p data)", + "Bash(DATABASE_URL=\"file:./data/rackmapper.db\" npx prisma migrate dev --name init)", + "Bash(npx prisma:*)", + "Bash(npx tsc:*)", + "Bash(git commit:*)", + "Bash(npm uninstall:*)", + "Bash(git add:*)" + ] + } +} diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 2f9e087..7a2673d 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -101,6 +101,8 @@ const modules = { }> ) => put(`/modules/${id}`, data), delete: (id: string) => del(`/modules/${id}`), + move: (id: string, rackId: string, uPosition: number) => + post(`/modules/${id}/move`, { rackId, uPosition }), getPorts: (id: string) => get(`/modules/${id}/ports`), }; diff --git a/client/src/components/modals/AddModuleModal.tsx b/client/src/components/modals/AddModuleModal.tsx index 0d6c4bd..6a1c655 100644 --- a/client/src/components/modals/AddModuleModal.tsx +++ b/client/src/components/modals/AddModuleModal.tsx @@ -1,4 +1,4 @@ -import { useState, type FormEvent } from 'react'; +import { useState, useEffect, type FormEvent } from 'react'; import { toast } from 'sonner'; import type { ModuleType } from '../../types'; import { Modal } from '../ui/Modal'; @@ -17,6 +17,8 @@ interface AddModuleModalProps { onClose: () => void; rackId: string; uPosition: number; + /** Pre-select a type (e.g. from a palette drag) — skips the type picker step. */ + initialType?: ModuleType; } const ALL_TYPES: ModuleType[] = [ @@ -24,17 +26,29 @@ const ALL_TYPES: ModuleType[] = [ 'MODEM', 'SERVER', 'NAS', 'PDU', 'AP', 'BLANK', 'OTHER', ]; -export function AddModuleModal({ open, onClose, rackId, uPosition }: AddModuleModalProps) { +export function AddModuleModal({ open, onClose, rackId, uPosition, initialType }: AddModuleModalProps) { const { addModule } = useRackStore(); - const [selectedType, setSelectedType] = useState(null); - const [name, setName] = useState(''); - const [uSize, setUSize] = useState(1); - const [portCount, setPortCount] = useState(0); + const [selectedType, setSelectedType] = useState(initialType ?? null); + const [name, setName] = useState(initialType ? MODULE_TYPE_LABELS[initialType] : ''); + const [uSize, setUSize] = useState(initialType ? MODULE_U_DEFAULTS[initialType] : 1); + const [portCount, setPortCount] = useState(initialType ? MODULE_PORT_DEFAULTS[initialType] : 0); const [ipAddress, setIpAddress] = useState(''); const [manufacturer, setManufacturer] = useState(''); const [model, setModel] = useState(''); const [loading, setLoading] = useState(false); + // Sync state when modal opens with a new initialType (e.g. drag-drop reuse) + useEffect(() => { + if (open && initialType) { + setSelectedType(initialType); + setName(MODULE_TYPE_LABELS[initialType]); + setUSize(MODULE_U_DEFAULTS[initialType]); + setPortCount(MODULE_PORT_DEFAULTS[initialType]); + } else if (!open) { + reset(); + } + }, [open, initialType]); // eslint-disable-line react-hooks/exhaustive-deps + function handleTypeSelect(type: ModuleType) { setSelectedType(type); setName(MODULE_TYPE_LABELS[type]); diff --git a/client/src/components/modals/PortConfigModal.tsx b/client/src/components/modals/PortConfigModal.tsx index 19f666d..3626059 100644 --- a/client/src/components/modals/PortConfigModal.tsx +++ b/client/src/components/modals/PortConfigModal.tsx @@ -14,7 +14,7 @@ interface PortConfigModalProps { } export function PortConfigModal({ portId, open, onClose }: PortConfigModalProps) { - const { racks, updateModuleLocal } = useRackStore(); + const { racks, fetchRacks } = useRackStore(); const [port, setPort] = useState(null); const [vlans, setVlans] = useState([]); const [label, setLabel] = useState(''); @@ -82,12 +82,8 @@ export function PortConfigModal({ portId, open, onClose }: PortConfigModalProps) vlans: vlanAssignments, }); - // Refresh racks to reflect changes - const portsInRack = racks.flatMap((r) => r.modules).find((m) => m.ports.some((p) => p.id === portId)); - if (portsInRack) { - const updatedPorts = await apiClient.modules.getPorts(portsInRack.id); - updateModuleLocal(portsInRack.id, { ports: updatedPorts }); - } + // Refresh all rack state so port dots and VLAN assignments are current + await fetchRacks(); toast.success('Port saved'); onClose(); diff --git a/client/src/components/rack/DevicePalette.tsx b/client/src/components/rack/DevicePalette.tsx index 24be983..7cbd1e7 100644 --- a/client/src/components/rack/DevicePalette.tsx +++ b/client/src/components/rack/DevicePalette.tsx @@ -1,11 +1,11 @@ -/** - * DevicePalette — sidebar showing all available device types. - * - * SCAFFOLD: Currently a static visual list. Full drag-to-rack DnD requires - * @dnd-kit integration with the RackColumn drop targets (see roadmap). - */ +import { useDraggable } from '@dnd-kit/core'; import type { ModuleType } from '../../types'; -import { MODULE_TYPE_LABELS, MODULE_TYPE_COLORS, MODULE_U_DEFAULTS, MODULE_PORT_DEFAULTS } from '../../lib/constants'; +import { + MODULE_TYPE_LABELS, + MODULE_TYPE_COLORS, + MODULE_U_DEFAULTS, + MODULE_PORT_DEFAULTS, +} from '../../lib/constants'; import { cn } from '../../lib/utils'; const ALL_TYPES: ModuleType[] = [ @@ -13,44 +13,51 @@ const ALL_TYPES: ModuleType[] = [ 'MODEM', 'SERVER', 'NAS', 'PDU', 'AP', 'BLANK', 'OTHER', ]; -interface DevicePaletteProps { - /** Called when user clicks a device type to place it. */ - onSelect?: (type: ModuleType) => void; +function PaletteItem({ type }: { type: ModuleType }) { + const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ + id: `palette-${type}`, + data: { type }, + }); + + const colors = MODULE_TYPE_COLORS[type]; + + return ( +
+
+
+
+ {MODULE_TYPE_LABELS[type]} +
+
+ {MODULE_U_DEFAULTS[type]}U · {MODULE_PORT_DEFAULTS[type]} ports +
+
+
+ ); } -export function DevicePalette({ onSelect }: DevicePaletteProps) { +export function DevicePalette() { return ( ); diff --git a/client/src/components/rack/RackColumn.tsx b/client/src/components/rack/RackColumn.tsx index 4963697..b5007f6 100644 --- a/client/src/components/rack/RackColumn.tsx +++ b/client/src/components/rack/RackColumn.tsx @@ -1,5 +1,7 @@ import { useState } from 'react'; -import { Trash2, MapPin } from 'lucide-react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { Trash2, MapPin, GripVertical } from 'lucide-react'; import { toast } from 'sonner'; import type { Rack } from '../../types'; import { buildOccupancyMap } from '../../lib/utils'; @@ -10,14 +12,29 @@ import { useRackStore } from '../../store/useRackStore'; interface RackColumnProps { rack: Rack; + /** ID of the module currently being dragged — render its slots as droppable ghosts. */ + draggingModuleId?: string | null; } -export function RackColumn({ rack }: RackColumnProps) { +export function RackColumn({ rack, draggingModuleId }: RackColumnProps) { const { deleteRack } = useRackStore(); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); + // Sortable for rack reorder + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: rack.id, + data: { dragType: 'rack' }, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.4 : 1, + }; + const occupancy = buildOccupancyMap(rack.modules); + const renderedModuleIds = new Set(); async function handleDelete() { setDeleting(true); @@ -32,20 +49,21 @@ export function RackColumn({ rack }: RackColumnProps) { } } - // Build the slot render list — modules span multiple U slots - const slots: Array<{ u: number; moduleId: string | null }> = []; - const renderedModuleIds = new Set(); - - for (let u = 1; u <= rack.totalU; u++) { - const moduleId = occupancy.get(u) ?? null; - slots.push({ u, moduleId }); - } - return ( <> -
- {/* Rack header */} +
+ {/* Rack header — drag handle for reorder */}
+ {/* Drag handle */} +
+ +
+
{rack.name}
{rack.location && ( @@ -55,6 +73,7 @@ export function RackColumn({ rack }: RackColumnProps) {
)}
+