3 Commits

3 changed files with 112 additions and 198 deletions

View File

@@ -13,6 +13,7 @@ import {
TextField,
Tooltip,
Checkbox,
Stack,
} from "@mui/material";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import ViewColumnIcon from "@mui/icons-material/ViewColumn";
@@ -67,50 +68,6 @@ const MAGIC_UI = {
const PAGE_ICON = DevicesOtherIcon;
const StatTile = React.memo(function StatTile({ label, value, meta, gradient }) {
return (
<Box
sx={{
minWidth: 160,
px: 2,
py: 1.5,
borderRadius: 2,
border: `1px solid rgba(255,255,255,0.08)`,
background: gradient,
boxShadow: "0 15px 45px rgba(5, 8, 28, 0.45)",
display: "flex",
flexDirection: "column",
gap: 0.5,
}}
>
<Typography sx={{ fontSize: "0.75rem", letterSpacing: 0.6, textTransform: "uppercase", color: "rgba(255,255,255,0.68)" }}>
{label}
</Typography>
<Typography sx={{ fontSize: "1.8rem", fontWeight: 700, color: "#f8fafc", lineHeight: 1 }}>
{value}
</Typography>
{meta ? (
<Typography sx={{ fontSize: "0.85rem", color: "rgba(226,232,240,0.75)" }}>{meta}</Typography>
) : null}
</Box>
);
});
const HERO_BADGE_SX = {
px: 1.5,
py: 0.4,
borderRadius: 999,
border: "1px solid rgba(255,255,255,0.18)",
background: "rgba(12,18,35,0.85)",
fontSize: "0.72rem",
letterSpacing: 0.35,
textTransform: "uppercase",
color: MAGIC_UI.textBright,
display: "inline-flex",
alignItems: "center",
gap: 0.5,
};
const RAINBOW_BUTTON_SX = {
borderRadius: 999,
textTransform: "none",
@@ -642,49 +599,6 @@ export default function DeviceList({
};
}, [rows, repoHash, computeAgentVersion]);
const shortRepoSha = useMemo(() => (repoHash || "").slice(0, 7), [repoHash]);
const statTiles = useMemo(() => {
const total = heroStats.total || 1;
const onlinePct = Math.round((heroStats.online / total) * 100);
return [
{
key: "online",
label: "Online",
value: heroStats.online,
meta: `${onlinePct}% live`,
gradient: "linear-gradient(135deg, rgba(56, 189, 248, 0.35), rgba(34, 197, 94, 0.45))",
},
{
key: "stale",
label: "Stale (>1h)",
value: heroStats.stale,
meta: heroStats.stale ? "Needs attention" : "All synced",
gradient: "linear-gradient(135deg, rgba(249, 115, 22, 0.55), rgba(239, 68, 68, 0.55))",
},
{
key: "updates",
label: "Needs Agent Update",
value: heroStats.needsUpdate,
meta: repoHash ? `Repo Hash: ${shortRepoSha}` : "Syncing repo…",
gradient: "linear-gradient(135deg, rgba(192, 132, 252, 0.4), rgba(14, 165, 233, 0.35))",
},
{
key: "sites",
label: "Sites",
value: heroStats.sites,
meta: heroStats.sites === 1 ? "Single site" : "Multi-site",
gradient: "linear-gradient(135deg, rgba(125, 183, 255, 0.45), rgba(148, 163, 184, 0.35))",
},
];
}, [heroStats, repoHash, shortRepoSha]);
const activeFilterCount = useMemo(
() => Object.keys(filters || {}).length,
[filters]
);
const hasActiveFilters = activeFilterCount > 0;
const heroSubtitle = useMemo(() => {
if (!heroStats.total) {
return "Connect your first device to start streaming telemetry into Borealis.";
@@ -1598,50 +1512,103 @@ export default function DeviceList({
}}
elevation={0}
>
<Box sx={{ position: "relative", zIndex: 1, p: { xs: 2, md: 0 }, pb: 2 }}>
<Box
sx={{
borderRadius: 0,
border: "none",
background: "transparent",
boxShadow: "none",
p: { xs: 2, md: 3 },
display: "flex",
flexWrap: "wrap",
gap: 3,
overflow: "hidden",
position: "relative",
isolation: "isolate",
}}
>
<Box sx={{ flex: "1 1 320px", minWidth: 0, display: "flex", flexWrap: "wrap", gap: 1 }}>
{hasActiveFilters ? (
<Box sx={HERO_BADGE_SX}>
<span>Filters</span>
<Typography component="span" sx={{ fontWeight: 700, fontSize: "0.8rem", color: MAGIC_UI.accentA }}>
{activeFilterCount}
</Typography>
</Box>
) : null}
{selectedIds.size > 0 ? (
<Box sx={HERO_BADGE_SX}>
<span>Selected</span>
<Typography component="span" sx={{ fontWeight: 700, fontSize: "0.8rem", color: MAGIC_UI.accentB }}>
{selectedIds.size}
</Typography>
</Box>
) : null}
</Box>
<Box sx={{ flex: "1 1 320px", minWidth: 0, position: "relative", zIndex: 1 }}>
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 1.2 }}>
{statTiles.map(({ key, ...tileProps }) => (
<StatTile key={key} {...tileProps} />
))}
</Box>
</Box>
</Box>
<Box
sx={{
position: "fixed",
top: { xs: 72, md: 88 },
right: { xs: 12, md: 20 },
display: "flex",
justifyContent: "flex-end",
zIndex: 1400,
pointerEvents: "none",
}}
>
<Stack direction="row" spacing={1.25} sx={{ pointerEvents: "auto" }}>
<Button
variant="contained"
size="small"
disabled={!canLaunchQuickJob}
disableElevation
onClick={() => {
if (!canLaunchQuickJob) return;
const hostnames = rows
.filter((r) => selectedIds.has(r.id))
.map((r) => r.hostname)
.filter((hostname) => Boolean(hostname));
if (!hostnames.length) return;
onQuickJobLaunch(hostnames);
}}
sx={{
borderRadius: 999,
px: 2.2,
textTransform: "none",
fontWeight: 600,
background: canLaunchQuickJob ? "linear-gradient(135deg, #34d399, #22d3ee)" : "rgba(148,163,184,0.2)",
color: canLaunchQuickJob ? "#041224" : MAGIC_UI.textMuted,
border: canLaunchQuickJob ? "none" : "1px solid rgba(148,163,184,0.35)",
boxShadow: canLaunchQuickJob ? "0 0 24px rgba(45, 212, 191, 0.45)" : "none",
}}
>
Quick Job
</Button>
<Tooltip title="Refresh devices to detect changes">
<span>
<Button
variant="outlined"
size="small"
startIcon={<CachedIcon fontSize="small" />}
onClick={() => fetchDevices({ refreshRepo: true })}
sx={{
borderRadius: 999,
textTransform: "none",
fontWeight: 600,
color: MAGIC_UI.textBright,
borderColor: "rgba(148,163,184,0.45)",
px: 1.8,
"&:hover": { borderColor: MAGIC_UI.accentA, backgroundColor: "rgba(125,211,252,0.08)" },
}}
>
Refresh
</Button>
</span>
</Tooltip>
<Tooltip title="Column chooser">
<Button
variant="outlined"
size="small"
startIcon={<ViewColumnIcon fontSize="small" />}
onClick={(e) => setColChooserAnchor(e.currentTarget)}
sx={{
borderRadius: 999,
textTransform: "none",
fontWeight: 600,
color: MAGIC_UI.textBright,
borderColor: "rgba(148,163,184,0.45)",
px: 1.8,
"&:hover": { borderColor: MAGIC_UI.accentA, backgroundColor: "rgba(125,211,252,0.08)" },
}}
>
Columns
</Button>
</Tooltip>
{derivedShowAddButton && (
<Button
variant="contained"
size="small"
startIcon={<AddIcon />}
disableElevation
sx={RAINBOW_BUTTON_SX}
onClick={() => {
setAddDeviceType(derivedDefaultType ?? null);
setAddDeviceOpen(true);
}}
>
{derivedAddLabel}
</Button>
)}
</Stack>
</Box>
<Box sx={{ px: { xs: 2, md: 3 }, pb: 1.5 }}>
<Box sx={{ px: { xs: 2, md: 3 }, pt: { xs: 2, md: 3 }, pb: 1.5 }}>
<Typography sx={{ fontSize: "0.72rem", color: MAGIC_UI.textMuted, textTransform: "uppercase", letterSpacing: 0.45, mb: 0.5 }}>
Custom View
</Typography>
@@ -1724,75 +1691,11 @@ export default function DeviceList({
width: 36,
background: "rgba(12,18,35,0.8)",
"&:hover": { borderColor: MAGIC_UI.accentA },
}}
>
<AddIcon fontSize="small" />
</IconButton>
</Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 0.75, flexWrap: "wrap", ml: "auto" }}>
<Button
variant="contained"
size="small"
disabled={!canLaunchQuickJob}
disableElevation
onClick={() => {
if (!canLaunchQuickJob) return;
const hostnames = rows
.filter((r) => selectedIds.has(r.id))
.map((r) => r.hostname)
.filter((hostname) => Boolean(hostname));
if (!hostnames.length) return;
onQuickJobLaunch(hostnames);
}}
sx={{
borderRadius: 999,
px: 2.2,
textTransform: "none",
fontWeight: 600,
background: canLaunchQuickJob ? "linear-gradient(135deg, #34d399, #22d3ee)" : "rgba(148,163,184,0.2)",
color: canLaunchQuickJob ? "#041224" : MAGIC_UI.textMuted,
border: canLaunchQuickJob ? "none" : "1px solid rgba(148,163,184,0.35)",
boxShadow: canLaunchQuickJob ? "0 0 24px rgba(45, 212, 191, 0.45)" : "none",
}}
>
Quick Job
</Button>
<Tooltip title="Refresh devices to detect changes">
<span>
<IconButton
size="small"
onClick={() => fetchDevices({ refreshRepo: true })}
sx={{ color: MAGIC_UI.textBright, border: "1px solid rgba(148,163,184,0.35)", borderRadius: 2 }}
>
<CachedIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Column chooser">
<IconButton
size="small"
onClick={(e) => setColChooserAnchor(e.currentTarget)}
sx={{ color: MAGIC_UI.textBright, border: "1px solid rgba(148,163,184,0.35)", borderRadius: 2 }}
>
<ViewColumnIcon fontSize="small" />
</IconButton>
</Tooltip>
{derivedShowAddButton && (
<Button
variant="contained"
size="small"
startIcon={<AddIcon />}
disableElevation
sx={RAINBOW_BUTTON_SX}
onClick={() => {
setAddDeviceType(derivedDefaultType ?? null);
setAddDeviceOpen(true);
}}
>
{derivedAddLabel}
</Button>
)}
</Box>
<AddIcon fontSize="small" />
</IconButton>
</Box>
</Box>
</Box>
<Box sx={{ px: { xs: 2, md: 3 }, pb: 3, flexGrow: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>

View File

@@ -618,7 +618,7 @@ export default function ReverseTunnelPowershell({ device }) {
>
<TextField
select
label="Connection Type"
label="Connection Protocol"
size="small"
value={connectionType}
onChange={(e) => setConnectionType(e.target.value)}

View File

@@ -0,0 +1,11 @@
# Reverse Tunnel Updates Checklist
Keep these tasks aligned with `Docs/Codex/REVERSE_TUNNELS.md` and the current Engine/Agent implementations.
- [ ] **Signed tokens only**: Require Ed25519 signing when issuing tunnel tokens and have both Engine and Agent reject unsigned tokens (no unsigned fallbacks).
- [ ] **Agent-targeted start/stop**: Emit `reverse_tunnel_start/stop` to the intended agent only (Socket.IO room or equivalent), not a broadcast.
- [ ] **Close per-lease listeners**: When a lease ends (stop/idle/grace/agent disconnect), close the WebSocket server bound to that lease port and free it.
- [ ] **Enforce idle/grace fully**: Lease sweeper should call `stop_tunnel` for expired/idle leases; Agent watchdog should treat `expires_at` as an absolute cutoff (no doubled grace).
- [ ] **TLS required**: Refuse to start tunnel listeners without cert/key (or pinned bundle); disable plaintext listeners and surface clear errors.
Out of scope (per current decision): payload size limits and backpressure changes.