This commit is contained in:
2026-08-09 17:04:38 -05:00
parent 0fa80d1fb9
commit 140d006159
4 changed files with 143 additions and 52 deletions

View File

@@ -25,6 +25,7 @@ services:
git \
ca-certificates
rm -rf /var/lib/apt/lists/*
git config set advice.defaultBranchName false
if [ -f /apps/package.json ] && [ -d /apps/.git ]; then
cd /apps
git reset --hard
@@ -38,9 +39,10 @@ services:
git fetch origin main
git checkout -B main origin/main
fi
npm install
npm install --include=dev
npm run build
npm run start
npm prune --omit=dev
node server/index.js
volumes:
- dcm_data:/apps/data
@@ -54,4 +56,4 @@ volumes:
networks:
dcm_internal:
driver: overlay
attachable: true
attachable: true

View File

@@ -1314,59 +1314,61 @@ function RackView({
const rackHeight = rack.sizeU * RACK_ROW_HEIGHT;
const horizontalEquipment = rack.equipment.filter((item) => !(item.type === 'pdu' && item.pduMount === 'vertical'));
const verticalPdus = rack.equipment.filter((item) => item.type === 'pdu' && item.pduMount === 'vertical');
const totalWatts = rack.equipment.reduce((sum, item) => sum + item.powerWatts, 0);
const locationDisplay = [rack.locationArea, rack.locationRow, rack.locationColumn].filter(Boolean).join(' / ') || 'Unassigned';
return (
<Paper
variant="outlined"
onClick={onSelectRack}
sx={{
width: 270,
flex: '0 0 270px',
overflow: 'hidden',
borderColor: (theme) =>
selected?.kind === 'rack' && selected.rackId === rack.id
? 'primary.main'
: theme.palette.mode === 'dark'
? 'rgba(97, 255, 207, 0.16)'
: 'rgba(0, 108, 95, 0.24)',
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(3, 8, 9, 0.72)' : 'rgba(238, 244, 236, 0.96)'),
borderRadius: 1,
boxShadow: (theme) =>
selected?.kind === 'rack' && selected.rackId === rack.id
? theme.palette.mode === 'dark'
? '0 0 34px rgba(97, 255, 207, 0.2), 0 18px 40px rgba(0, 0, 0, 0.32)'
: '0 0 0 1px rgba(0, 108, 95, 0.18), 0 16px 34px rgba(23, 38, 33, 0.18)'
: theme.palette.mode === 'dark'
? '0 18px 40px rgba(0, 0, 0, 0.24)'
: '0 14px 28px rgba(23, 38, 33, 0.14)',
clipPath: 'polygon(0 0, 96% 0, 100% 12px, 100% 100%, 4% 100%, 0 calc(100% - 12px))',
}}
>
<Box
<Box sx={{ width: 270, flex: '0 0 270px', display: 'flex', flexDirection: 'column', gap: 1 }}>
<Paper
variant="outlined"
onClick={onSelectRack}
sx={{
p: 1.25,
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(9, 20, 20, 0.78)' : 'rgba(218, 232, 220, 0.96)'),
borderBottom: 1,
borderColor: 'divider',
backgroundImage: (theme) =>
theme.palette.mode === 'dark'
? 'linear-gradient(90deg, rgba(97, 255, 207, 0.12), rgba(240, 184, 77, 0.04))'
: 'linear-gradient(90deg, rgba(0, 108, 95, 0.12), rgba(156, 92, 12, 0.07))',
width: '100%',
overflow: 'hidden',
borderColor: (theme) =>
selected?.kind === 'rack' && selected.rackId === rack.id
? 'primary.main'
: theme.palette.mode === 'dark'
? 'rgba(97, 255, 207, 0.16)'
: 'rgba(0, 108, 95, 0.24)',
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(3, 8, 9, 0.72)' : 'rgba(238, 244, 236, 0.96)'),
borderRadius: 1,
boxShadow: (theme) =>
selected?.kind === 'rack' && selected.rackId === rack.id
? theme.palette.mode === 'dark'
? '0 0 34px rgba(97, 255, 207, 0.2), 0 18px 40px rgba(0, 0, 0, 0.32)'
: '0 0 0 1px rgba(0, 108, 95, 0.18), 0 16px 34px rgba(23, 38, 33, 0.18)'
: theme.palette.mode === 'dark'
? '0 18px 40px rgba(0, 0, 0, 0.24)'
: '0 14px 28px rgba(23, 38, 33, 0.14)',
clipPath: 'polygon(0 0, 96% 0, 100% 12px, 100% 100%, 4% 100%, 0 calc(100% - 12px))',
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={1}>
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" noWrap sx={{ fontWeight: 800 }}>
{rack.name}
</Typography>
<Typography variant="caption" color="text.secondary">
{rack.sizeU}U {rack.type}
</Typography>
</Box>
<Chip size="small" label={`${occupancy.percent}%`} color={occupancy.percent > 85 ? 'warning' : 'default'} />
</Stack>
<LinearProgress variant="determinate" value={occupancy.percent} sx={{ mt: 1, height: 5, borderRadius: 1 }} />
</Box>
<Box
sx={{
p: 1.25,
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(9, 20, 20, 0.78)' : 'rgba(218, 232, 220, 0.96)'),
borderBottom: 1,
borderColor: 'divider',
backgroundImage: (theme) =>
theme.palette.mode === 'dark'
? 'linear-gradient(90deg, rgba(97, 255, 207, 0.12), rgba(240, 184, 77, 0.04))'
: 'linear-gradient(90deg, rgba(0, 108, 95, 0.12), rgba(156, 92, 12, 0.07))',
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={1}>
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" noWrap sx={{ fontWeight: 800 }}>
{rack.name}
</Typography>
<Typography variant="caption" color="text.secondary">
{rack.sizeU}U {rack.type}
</Typography>
</Box>
<Chip size="small" label={`${occupancy.percent}%`} color={occupancy.percent > 85 ? 'warning' : 'default'} />
</Stack>
<LinearProgress variant="determinate" value={occupancy.percent} sx={{ mt: 1, height: 5, borderRadius: 1 }} />
</Box>
<Box
onDrop={onDrop}
@@ -1515,6 +1517,50 @@ function RackView({
})}
</Box>
</Paper>
<Paper
variant="outlined"
onClick={onSelectRack}
sx={{
width: '100%',
p: 1,
cursor: 'pointer',
borderColor: (theme) =>
selected?.kind === 'rack' && selected.rackId === rack.id
? 'primary.main'
: theme.palette.mode === 'dark'
? 'rgba(97, 255, 207, 0.18)'
: 'rgba(0, 108, 95, 0.24)',
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(7, 17, 16, 0.72)' : 'rgba(244, 249, 241, 0.9)'),
backgroundImage: (theme) =>
theme.palette.mode === 'dark'
? 'linear-gradient(135deg, rgba(97, 255, 207, 0.08), rgba(240, 184, 77, 0.04))'
: 'linear-gradient(135deg, rgba(0, 108, 95, 0.08), rgba(156, 92, 12, 0.05))',
boxShadow: (theme) =>
theme.palette.mode === 'dark'
? '0 12px 28px rgba(0, 0, 0, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.05)'
: '0 10px 22px rgba(23, 38, 33, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.65)',
}}
>
<Stack spacing={0.45}>
<Box sx={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 1, minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" sx={{ flex: '0 0 auto', fontSize: 9, textTransform: 'uppercase' }}>
Location
</Typography>
<Typography variant="caption" noWrap sx={{ minWidth: 0, fontWeight: 900, textAlign: 'right' }}>
{locationDisplay}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 1, minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: 9, textTransform: 'uppercase' }}>
Total Watts
</Typography>
<Typography variant="caption" sx={{ fontWeight: 900 }}>
{totalWatts.toLocaleString()} W
</Typography>
</Box>
</Stack>
</Paper>
</Box>
);
}
@@ -1654,7 +1700,41 @@ function PropertiesPanel({
/>
<TextField label="Manufacturer" value={selectedRack.manufacturer} onChange={(event) => onRackChange(selectedRack.id, { manufacturer: event.target.value })} fullWidth />
<TextField label="Model" value={selectedRack.model} onChange={(event) => onRackChange(selectedRack.id, { model: event.target.value })} fullWidth />
<TextField label="Location" value={selectedRack.location} onChange={(event) => onRackChange(selectedRack.id, { location: event.target.value })} fullWidth />
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>
<TextField
label="Area"
value={selectedRack.locationArea}
onChange={(event) =>
onRackChange(selectedRack.id, {
locationArea: event.target.value,
location: [event.target.value, selectedRack.locationRow, selectedRack.locationColumn].filter(Boolean).join('/'),
})
}
fullWidth
/>
<TextField
label="Row"
value={selectedRack.locationRow}
onChange={(event) =>
onRackChange(selectedRack.id, {
locationRow: event.target.value,
location: [selectedRack.locationArea, event.target.value, selectedRack.locationColumn].filter(Boolean).join('/'),
})
}
fullWidth
/>
<TextField
label="Column"
value={selectedRack.locationColumn}
onChange={(event) =>
onRackChange(selectedRack.id, {
locationColumn: event.target.value,
location: [selectedRack.locationArea, selectedRack.locationRow, event.target.value].filter(Boolean).join('/'),
})
}
fullWidth
/>
</Stack>
<TextField label="Notes" value={selectedRack.notes} onChange={(event) => onRackChange(selectedRack.id, { notes: event.target.value })} multiline minRows={3} fullWidth />
<Button color="error" variant="outlined" startIcon={<Delete />} onClick={onDelete}>
Delete rack

View File

@@ -93,6 +93,9 @@ export interface RackContainer {
manufacturer: string;
model: string;
location: string;
locationArea: string;
locationRow: string;
locationColumn: string;
notes: string;
equipment: Equipment[];
}

View File

@@ -27,6 +27,9 @@ export const createRack = (type: 'rack' | 'cabinet', index: number, sizeU = 42):
manufacturer: '',
model: '',
location: '',
locationArea: '',
locationRow: '',
locationColumn: '',
notes: '',
equipment: [],
});
@@ -157,6 +160,9 @@ export const normalizeDiagram = (value: unknown): DiagramData => {
manufacturer: rack.manufacturer || '',
model: rack.model || '',
location: rack.location || '',
locationArea: rack.locationArea || rack.location || '',
locationRow: rack.locationRow || '',
locationColumn: rack.locationColumn || '',
notes: rack.notes || '',
equipment: Array.isArray(rack.equipment)
? rack.equipment.map((item, itemIndex) => {