Algio-1452's picture
FIxed Error And Comamnds, Direct TO changed Ro
e1e9687
Raw
History Blame Contribute Delete
27.7 kB
import React, { useState } from 'react';
export default function AdminPanel({
gameState,
airports = [],
activeAirport,
activeAirportConfig,
onSelectAirport,
sendWSMessage,
draftingMode,
setDraftingMode,
airportName,
setAirportName,
isRunwayBidirectional,
setIsRunwayBidirectional,
starDraft,
setStarDraft,
sidDraft,
setSidDraft
}) {
const [editingWaypoint, setEditingWaypoint] = useState(null); // {gate, runway, index, wp}
const [editingRunway, setEditingRunway] = useState(null); // {rw}
const [runwayLabel, setRunwayLabel] = useState("");
const [spawnFields, setSpawnFields] = useState({
callsign: `UA${Math.floor(Math.random() * 900) + 100}`,
type: "B738",
altitude: 10000,
speed: 250,
gate: "N",
isDeparture: false,
runwayId: "",
terminalGateId: ""
});
const handleSpawn = () => {
sendWSMessage('spawn', {
...spawnFields,
is_departure: spawnFields.isDeparture,
runway_id: spawnFields.runwayId,
terminal_gate_id: spawnFields.terminalGateId,
altitude: parseInt(spawnFields.altitude || 0),
speed: parseInt(spawnFields.speed || 0)
});
setSpawnFields(prev => ({
...prev,
callsign: `UA${Math.floor(Math.random() * 900) + 100}`
}));
};
const renderEditModal = () => {
if (!editingWaypoint && !editingRunway) return null;
const isWaypoint = !!editingWaypoint;
const title = isWaypoint ? `Edit Waypoint: ${editingWaypoint.wp.name}` : `Edit Runway: ${editingRunway.rw.id}`;
const handleSave = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
if (isWaypoint) {
sendWSMessage('update_waypoint', {
airport_code: activeAirport.airport_code,
waypoint_id: editingWaypoint.wp.id,
name: formData.get('name'),
target_alt: parseInt(formData.get('alt')),
target_speed: parseInt(formData.get('speed'))
});
setEditingWaypoint(null);
} else {
sendWSMessage('update_runway', {
airport_code: activeAirport.airport_code,
runway_id: editingRunway.rw.id,
new_id: formData.get('id'),
heading: parseFloat(formData.get('heading'))
});
setEditingRunway(null);
}
};
return (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)', zIndex: 3000,
display: 'flex', justifyContent: 'center', alignItems: 'center'
}}>
<div style={{
background: 'white', padding: '20px', borderRadius: '8px',
width: '300px', boxShadow: '0 4px 12px rgba(0,0,0,0.2)'
}}>
<h3 style={{ marginTop: 0, fontSize: '1rem', color: '#1a1a1a' }}>{title}</h3>
<form onSubmit={handleSave}>
{isWaypoint ? (
<>
<div style={{ marginBottom: '10px' }}>
<label style={{ display: 'block', fontSize: '0.75rem', color: '#666' }}>Name</label>
<input name="name" defaultValue={editingWaypoint.wp.name} style={{ width: '100%', padding: '4px' }} />
</div>
<div style={{ marginBottom: '10px' }}>
<label style={{ display: 'block', fontSize: '0.75rem', color: '#666' }}>Target Altitude (ft)</label>
<input name="alt" type="number" defaultValue={editingWaypoint.wp.target_alt} style={{ width: '100%', padding: '4px' }} />
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', fontSize: '0.75rem', color: '#666' }}>Target Speed (kts)</label>
<input name="speed" type="number" defaultValue={editingWaypoint.wp.target_speed} style={{ width: '100%', padding: '4px' }} />
</div>
</>
) : (
<>
<div style={{ marginBottom: '10px' }}>
<label style={{ display: 'block', fontSize: '0.75rem', color: '#666' }}>Runway ID</label>
<input name="id" defaultValue={editingRunway.rw.id} style={{ width: '100%', padding: '4px' }} />
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', fontSize: '0.75rem', color: '#666' }}>Heading (°)</label>
<input name="heading" type="number" step="0.1" defaultValue={editingRunway.rw.heading} style={{ width: '100%', padding: '4px' }} />
</div>
</>
)}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
<button type="button" onClick={() => { setEditingWaypoint(null); setEditingRunway(null); }} className="btn btn-outline" style={{ padding: '4px 12px' }}>Cancel</button>
<button type="submit" className="btn btn-primary" style={{ padding: '4px 12px', background: '#007bff', color: 'white', border: 'none', borderRadius: '4px' }}>Save</button>
</div>
</form>
</div>
</div>
);
};
return (
<nav className="admin-panel thin-scroll">
<div style={{ marginBottom: '16px' }}>
<h2 style={{ color: '#555', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 'bold' }}>Admin Control</h2>
</div>
{/* Simulation Speed Section */}
<div className="section" style={{ marginBottom: '16px' }}>
<h4 style={{ margin: '0 0 8px 0', fontSize: '0.7rem', color: '#888', textTransform: 'uppercase', fontWeight: 'bold' }}>Sim Scale</h4>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="range"
min="1"
max="8"
step="1"
value={activeAirportConfig?.time_scale || 1}
onChange={(e) => sendWSMessage('set_time_scale', { scale: e.target.value })}
style={{
flex: 1,
cursor: 'pointer',
accentColor: '#007bff'
}}
/>
<span style={{
minWidth: '35px',
textAlign: 'right',
fontWeight: 'bold',
color: '#007bff',
fontSize: '0.8rem'
}}>
{activeAirportConfig?.time_scale || 1}x
</span>
</div>
</div>
{/* Airports Section */}
<div className="section" style={{ borderTop: '1px solid #f0f0f0', paddingTop: '12px', marginTop: '12px' }}>
<h4 style={{ margin: '0 0 8px 0', fontSize: '0.7rem', color: '#888', textTransform: 'uppercase', fontWeight: 'bold' }}>Airports</h4>
<div style={{ maxHeight: '80px', overflowY: 'auto', border: '1px solid #eee', padding: '4px', borderRadius: '4px', background: '#fafafa', marginBottom: '8px' }}>
{airports.map(ap => (
<div
key={ap.name}
onClick={() => onSelectAirport(ap)}
style={{
padding: '4px 8px',
cursor: 'pointer',
fontSize: '0.7rem',
borderRadius: '3px',
marginBottom: '2px',
background: activeAirport?.name === ap.name ? '#007bff' : 'none',
color: activeAirport?.name === ap.name ? '#fff' : '#444'
}}
>
{ap.name}
</div>
))}
</div>
<div style={{ display: 'flex', gap: '4px' }}>
<input
placeholder="New..."
value={airportName}
onChange={e => setAirportName(e.target.value)}
style={{ flex: 1, padding: '4px', fontSize: '0.7rem', border: '1px solid #eee', borderRadius: '4px' }}
/>
<button
className="btn"
style={{
padding: '4px 8px',
fontSize: '0.65rem',
background: draftingMode === 'airport' ? '#ffcccc' : '#e7f3ff',
color: '#0056b3'
}}
onClick={() => setDraftingMode(draftingMode === 'airport' ? null : 'airport')}
>
{draftingMode === 'airport' ? '📍' : 'Add'}
</button>
</div>
</div>
{/* Runway Management */}
<div className="section" style={{ borderTop: '1px solid #f0f0f0', paddingTop: '12px', marginTop: '12px' }}>
<h4 style={{ margin: '0 0 8px 0', fontSize: '0.7rem', color: '#888', textTransform: 'uppercase', fontWeight: 'bold' }}>Runways ({activeAirport?.name})</h4>
<div style={{ maxHeight: '120px', overflowY: 'auto', background: '#fafafa', padding: '4px', borderRadius: '4px', border: '1px solid #eee', marginBottom: '8px' }}>
{activeAirport?.runways?.map(rw => {
const statusObj = gameState?.runway_status?.[rw.id];
const status = statusObj?.status || "CLEAR";
let statusColor = "#6c757d"; // Clear
let statusText = "CLEAR";
if (status === "OCCUPIED") {
statusColor = "#dc3545"; // Red
statusText = statusObj.occupied_by;
} else if (status === "RESERVED") {
statusColor = "#007bff"; // Blue
statusText = `Reserved: ${statusObj.reserved_by}`;
} else if (status === "COOLDOWN") {
statusColor = "#ffc107"; // Amber
statusText = `Cooling: ${statusObj.cooldown_remaining}s`;
}
return (
<div key={rw.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 4px', borderBottom: '1px solid #f0f0f0', color: '#444' }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ fontSize: '0.7rem', fontWeight: 'bold' }}>{rw.heading.toFixed(0).padStart(2, '0')} [{rw.id}]</span>
<span style={{
fontSize: '0.55rem',
color: status === "CLEAR" ? "#28a745" : "white",
background: status === "CLEAR" ? "none" : statusColor,
padding: status === "CLEAR" ? "0" : "1px 4px",
borderRadius: '2px',
fontWeight: 'bold',
width: 'fit-content'
}}>
{statusText}
</span>
</div>
<div style={{ display: 'flex', gap: '4px' }}>
<button onClick={() => setEditingRunway({ rw })} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '0.7rem' }}>✏️</button>
<button
onClick={() => {
if (window.confirm(`Delete runway ${rw.id}?`)) {
sendWSMessage('delete_runway', { airport_code: activeAirport.airport_code, runway_id: rw.id });
}
}}
style={{ background: 'none', border: 'none', color: '#dc3545', cursor: 'pointer', fontWeight: 'bold', fontSize: '1rem' }}
>&times;</button>
</div>
</div>
);
})}
</div>
<button
className="btn"
disabled={!activeAirport}
style={{ width: '100%', background: draftingMode === 'runway' ? '#ffcccc' : '#f8f9fa', fontSize: '0.65rem' }}
onClick={() => setDraftingMode(draftingMode === 'runway' ? null : 'runway')}
>
{draftingMode === 'runway' ? 'Click Map...' : 'Add Runway'}
</button>
</div>
{/* Pooled Waypoint Dropper */}
<div className="section" style={{ borderTop: '1px solid #f0f0f0', paddingTop: '12px', marginTop: '12px' }}>
<h4 style={{ margin: '0 0 8px 0', fontSize: '0.7rem', color: '#888', textTransform: 'uppercase', fontWeight: 'bold' }}>Waypoints</h4>
<button
className="btn"
style={{ width: '100%', background: draftingMode === 'waypoint' ? '#ffcccc' : '#e7f3ff', fontSize: '0.65rem', marginBottom: '8px' }}
onClick={() => setDraftingMode(draftingMode === 'waypoint' ? null : 'waypoint')}
>
{draftingMode === 'waypoint' ? 'Drop Active...' : 'Activate Dropper'}
</button>
{activeAirportConfig?.waypoints && Object.keys(activeAirportConfig.waypoints).length > 0 && (
<div style={{ maxHeight: '80px', overflowY: 'auto', background: '#fafafa', borderRadius: '4px', border: '1px solid #eee' }}>
{Object.values(activeAirportConfig.waypoints).map((wp) => (
<div key={wp.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.6rem', padding: '2px 6px', borderBottom: '1px solid #f9f9f9' }}>
<span>{wp.name}</span>
<div style={{ display: 'flex', gap: '4px' }}>
<button onClick={() => setEditingWaypoint({ wp })} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '0.65rem' }}>✏️</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Arrival Route Builder (STAR) */}
<div className="section" style={{ borderTop: '1px solid #f0f0f0', paddingTop: '12px', marginTop: '12px' }}>
<h4 style={{ margin: '0 0 8px 0', fontSize: '0.7rem', color: '#888', textTransform: 'uppercase', fontWeight: 'bold' }}>Arrival Routes</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<input
placeholder="Procedure Name (e.g. SUTRA 1A)"
value={starDraft.name || ''}
onChange={e => setStarDraft({ ...starDraft, name: e.target.value })}
style={{ padding: '4px', fontSize: '0.65rem', border: '1px solid #eee', borderRadius: '4px' }}
/>
<div style={{ display: 'flex', gap: '4px' }}>
<select
value={starDraft.gate}
onChange={e => setStarDraft({ ...starDraft, gate: e.target.value })}
style={{ flex: 1, padding: '2px', fontSize: '0.65rem', border: '1px solid #eee' }}
>
{activeAirportConfig?.gates ?
Object.keys(activeAirportConfig.gates).map(g => <option key={g} value={g}>{g}</option>) :
<><option value="N">N</option><option value="S">S</option><option value="E">E</option><option value="W">W</option></>
}
</select>
<select
value={starDraft.runway_id || ''}
onChange={e => setStarDraft({ ...starDraft, runway_id: e.target.value })}
style={{ flex: 1, padding: '2px', fontSize: '0.65rem', border: '1px solid #eee' }}
>
<option value="">RWY...</option>
{activeAirport?.runways?.map(rw => <option key={rw.id} value={rw.id}>{rw.id}</option>)}
</select>
</div>
<button
className="btn"
disabled={!starDraft.runway_id}
style={{ width: '100%', background: draftingMode === 'route' ? '#ffcccc' : '#f8f9fa', fontSize: '0.65rem' }}
onClick={() => setDraftingMode(draftingMode === 'route' ? null : 'route')}
>
{draftingMode === 'route' ? 'Building STAR...' : 'Build STAR'}
</button>
{draftingMode === 'route' && (
<div style={{ padding: '6px', background: '#fff9e6', border: '1px solid #ffeeba', borderRadius: '4px', marginTop: '4px' }}>
<select
value=""
onChange={(e) => setStarDraft(prev => ({ ...prev, sequence: [...prev.sequence, e.target.value] }))}
style={{ width: '100%', padding: '2px', fontSize: '0.65rem', marginBottom: '6px' }}
>
<option value="">Add Waypoint...</option>
{Object.values(activeAirportConfig?.waypoints || {})
.sort((a,b) => a.name.localeCompare(b.name))
.map(wp => <option key={wp.id} value={wp.id}>{wp.name}</option>)}
</select>
<div style={{ fontSize: '0.6rem', marginBottom: '6px', color: '#666', maxHeight: '40px', overflowY: 'auto' }}>
{starDraft.sequence.map(id => activeAirportConfig.waypoints[id]?.name || id).join(' → ')}
</div>
<div style={{ display: 'flex', gap: '4px' }}>
<button onClick={() => setStarDraft({ ...starDraft, sequence: [] })} className="btn" style={{ flex: 1, fontSize: '0.6rem', padding: '2px' }}>Clear</button>
<button
disabled={starDraft.sequence.length === 0}
onClick={() => {
sendWSMessage('save_star_route', {
airport_code: activeAirport.airport_code,
name: starDraft.name || `Route_${starDraft.gate}_${starDraft.runway_id}`,
gate: starDraft.gate,
runway_id: starDraft.runway_id,
sequence: starDraft.sequence
});
setDraftingMode(null);
setStarDraft(prev => ({ ...prev, sequence: [], name: '' }));
}}
className="btn btn-primary"
style={{ flex: 1, background: '#007bff', fontSize: '0.6rem', padding: '2px' }}
>Save STAR</button>
</div>
</div>
)}
</div>
</div>
{/* SID Route Builder */}
<div className="section" style={{ borderTop: '1px solid #f0f0f0', paddingTop: '12px', marginTop: '12px' }}>
<h4 style={{ margin: '0 0 8px 0', fontSize: '0.7rem', color: '#888', textTransform: 'uppercase', fontWeight: 'bold' }}>Departure Routes</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<input
placeholder="Procedure Name (e.g. RWY1 EXIT)"
value={sidDraft.name || ''}
onChange={e => setSidDraft({ ...sidDraft, name: e.target.value })}
style={{ padding: '4px', fontSize: '0.65rem', border: '1px solid #eee', borderRadius: '4px' }}
/>
<div style={{ display: 'flex', gap: '4px' }}>
<select
value={sidDraft.runway_id || ''}
onChange={e => setSidDraft({ ...sidDraft, runway_id: e.target.value })}
style={{ flex: 1, padding: '2px', fontSize: '0.65rem', border: '1px solid #eee' }}
>
<option value="">RWY...</option>
{activeAirport?.runways?.map(rw => <option key={rw.id} value={rw.id}>{rw.id}</option>)}
</select>
<select
value={sidDraft.gate}
onChange={e => setSidDraft({ ...sidDraft, gate: e.target.value })}
style={{ flex: 1, padding: '2px', fontSize: '0.65rem', border: '1px solid #eee' }}
>
{activeAirportConfig?.gates ?
Object.keys(activeAirportConfig.gates).map(g => <option key={g} value={g}>{g}</option>) :
<><option value="N">N</option><option value="S">S</option><option value="E">E</option><option value="W">W</option></>
}
</select>
</div>
<button
className="btn"
disabled={!sidDraft.runway_id}
style={{ width: '100%', background: draftingMode === 'sid_route' ? '#ffcccc' : '#f8f9fa', fontSize: '0.65rem' }}
onClick={() => setDraftingMode(draftingMode === 'sid_route' ? null : 'sid_route')}
>
{draftingMode === 'sid_route' ? 'Building SID...' : 'Build SID'}
</button>
{draftingMode === 'sid_route' && (
<div style={{ padding: '6px', background: '#e6ffed', border: '1px solid #baf0cc', borderRadius: '4px', marginTop: '4px' }}>
<select
value=""
onChange={(e) => setSidDraft(prev => ({ ...prev, sequence: [...prev.sequence, e.target.value] }))}
style={{ width: '100%', padding: '2px', fontSize: '0.65rem', marginBottom: '6px' }}
>
<option value="">Add Waypoint...</option>
{Object.values(activeAirportConfig?.waypoints || {})
.sort((a,b) => a.name.localeCompare(b.name))
.map(wp => <option key={wp.id} value={wp.id}>{wp.name}</option>)}
</select>
<div style={{ fontSize: '0.6rem', marginBottom: '6px', color: '#666', maxHeight: '40px', overflowY: 'auto' }}>
{sidDraft.sequence.map(id => activeAirportConfig.waypoints[id]?.name || id).join(' → ')}
</div>
<div style={{ display: 'flex', gap: '4px' }}>
<button onClick={() => setSidDraft({ ...sidDraft, sequence: [] })} className="btn" style={{ flex: 1, fontSize: '0.6rem', padding: '2px' }}>Clear</button>
<button
disabled={sidDraft.sequence.length === 0}
onClick={() => {
sendWSMessage('save_sid_route', {
airport_code: activeAirport.airport_code,
name: sidDraft.name || `Route_${sidDraft.runway_id}_${sidDraft.gate}`,
runway_id: sidDraft.runway_id,
gate: sidDraft.gate,
sequence: sidDraft.sequence
});
setDraftingMode(null);
setSidDraft(prev => ({ ...prev, sequence: [], name: '' }));
}}
className="btn btn-primary"
style={{ flex: 1, background: '#28a745', fontSize: '0.6rem', padding: '2px' }}
>Save SID</button>
</div>
</div>
)}
</div>
</div>
{/* Spawn Section */}
<div className="section" style={{ borderTop: '1px solid #f0f0f0', paddingTop: '12px', marginTop: '12px' }}>
<h4 style={{ margin: '0 0 8px 0', fontSize: '0.7rem', color: '#888', textTransform: 'uppercase', fontWeight: 'bold' }}>Spawn Aircraft</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<input
placeholder="Callsign"
value={spawnFields.callsign}
onChange={e => setSpawnFields({ ...spawnFields, callsign: e.target.value })}
style={{ padding: '6px', fontSize: '0.7rem', border: '1px solid #eee', borderRadius: '4px' }}
/>
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
<label style={{ fontSize: '0.65rem', display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={spawnFields.isDeparture}
onChange={e => setSpawnFields({ ...spawnFields, isDeparture: e.target.checked })}
/>
Departure
</label>
{spawnFields.isDeparture && (
<select
value={spawnFields.runwayId}
onChange={e => setSpawnFields({ ...spawnFields, runwayId: e.target.value })}
style={{ flex: 1, fontSize: '0.65rem', padding: '4px', border: '1px solid #eee' }}
>
<option value="">Select Runway...</option>
{activeAirport?.runways?.map(rw => <option key={rw.id} value={rw.id}>{rw.id}</option>)}
</select>
)}
</div>
<div style={{ display: 'flex', gap: '4px' }}>
{/* Case 1: Arrival (Always show Entry Gate) */}
{!spawnFields.isDeparture && (
<select
value={spawnFields.gate}
onChange={e => setSpawnFields({ ...spawnFields, gate: e.target.value })}
style={{ flex: 1, fontSize: '0.65rem', padding: '4px', border: '1px solid #eee' }}
>
<option value="" disabled>Entry Gate</option>
{activeAirportConfig?.gates ?
Object.keys(activeAirportConfig.gates).map(g => <option key={g} value={g}>{g}</option>) :
<><option value="N">N</option><option value="S">S</option><option value="E">E</option><option value="W">W</option></>
}
</select>
)}
{/* Case 2: Departure (Show BOTH Exit Gate and Terminal Stand) */}
{spawnFields.isDeparture && (
<>
<select
value={spawnFields.gate}
onChange={e => setSpawnFields({ ...spawnFields, gate: e.target.value })}
style={{ flex: 1, fontSize: '0.65rem', padding: '4px', border: '1px solid #eee' }}
>
<option value="" disabled>Exit Gate</option>
{activeAirportConfig?.gates ?
Object.keys(activeAirportConfig.gates).map(g => <option key={g} value={g}>{g}</option>) :
<><option value="N">N</option><option value="S">S</option><option value="E">E</option><option value="W">W</option></>
}
</select>
<select
value={spawnFields.terminalGateId}
onChange={e => setSpawnFields({ ...spawnFields, terminalGateId: e.target.value })}
style={{ flex: 1, fontSize: '0.65rem', padding: '4px', border: '1px solid #eee', background: '#f0f4ff' }}
>
<option value="">Threshold (Direct)</option>
{activeAirportConfig?.terminal_gates &&
Object.keys(activeAirportConfig.terminal_gates).map(g => (
<option key={g} value={g}>Stand: {g}</option>
))
}
</select>
</>
)}
{!spawnFields.isDeparture && (
<>
<input type="number" placeholder="Alt" value={spawnFields.altitude} onChange={e => setSpawnFields({ ...spawnFields, altitude: e.target.value })} style={{ flex: 1, padding: '4px', fontSize: '0.65rem', border: '1px solid #eee' }} />
<input type="number" placeholder="Spd" value={spawnFields.speed} onChange={e => setSpawnFields({ ...spawnFields, speed: e.target.value })} style={{ flex: 1, padding: '4px', fontSize: '0.65rem', border: '1px solid #eee' }} />
</>
)}
</div>
<button onClick={handleSpawn} className="btn btn-primary" style={{ background: '#28a745', color: 'white', border: 'none', padding: '8px', fontSize: '0.7rem' }}>Spawn Flight</button>
</div>
</div>
<div style={{ marginTop: 'auto', paddingTop: '16px', display: 'flex', gap: '4px' }}>
<button className="btn" style={{ flex: 1, fontSize: '0.65rem', color: '#dc3545' }} onClick={() => sendWSMessage('reset', {})}>Reset</button>
<button
className="btn"
style={{ flex: 1, fontSize: '0.65rem', background: '#dc3545', color: '#fff', border: 'none' }}
onClick={() => {
if (window.confirm("Shut down server?")) sendWSMessage('shutdown', {});
}}
>Stop</button>
</div>
{renderEditModal()}
<style>{`
.admin-panel {
height: 100%;
display: flex;
flex-direction: column;
}
.thin-scroll::-webkit-scrollbar { width: 4px; }
.thin-scroll::-webkit-scrollbar-track { background: #f1f1f1; }
.thin-scroll::-webkit-scrollbar-thumb { background: #ccc; border-radius: 10px; }
`}</style>
</nav>
);
}