3 Commits

3 changed files with 112 additions and 198 deletions

View File

@@ -13,6 +13,7 @@ import {
TextField, TextField,
Tooltip, Tooltip,
Checkbox, Checkbox,
Stack,
} from "@mui/material"; } from "@mui/material";
import MoreVertIcon from "@mui/icons-material/MoreVert"; import MoreVertIcon from "@mui/icons-material/MoreVert";
import ViewColumnIcon from "@mui/icons-material/ViewColumn"; import ViewColumnIcon from "@mui/icons-material/ViewColumn";
@@ -67,50 +68,6 @@ const MAGIC_UI = {
const PAGE_ICON = DevicesOtherIcon; 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 = { const RAINBOW_BUTTON_SX = {
borderRadius: 999, borderRadius: 999,
textTransform: "none", textTransform: "none",
@@ -642,49 +599,6 @@ export default function DeviceList({
}; };
}, [rows, repoHash, computeAgentVersion]); }, [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(() => { const heroSubtitle = useMemo(() => {
if (!heroStats.total) { if (!heroStats.total) {
return "Connect your first device to start streaming telemetry into Borealis."; return "Connect your first device to start streaming telemetry into Borealis.";
@@ -1598,50 +1512,103 @@ export default function DeviceList({
}} }}
elevation={0} elevation={0}
> >
<Box sx={{ position: "relative", zIndex: 1, p: { xs: 2, md: 0 }, pb: 2 }}> <Box
<Box sx={{
sx={{ position: "fixed",
borderRadius: 0, top: { xs: 72, md: 88 },
border: "none", right: { xs: 12, md: 20 },
background: "transparent", display: "flex",
boxShadow: "none", justifyContent: "flex-end",
p: { xs: 2, md: 3 }, zIndex: 1400,
display: "flex", pointerEvents: "none",
flexWrap: "wrap", }}
gap: 3, >
overflow: "hidden", <Stack direction="row" spacing={1.25} sx={{ pointerEvents: "auto" }}>
position: "relative", <Button
isolation: "isolate", variant="contained"
}} size="small"
> disabled={!canLaunchQuickJob}
<Box sx={{ flex: "1 1 320px", minWidth: 0, display: "flex", flexWrap: "wrap", gap: 1 }}> disableElevation
{hasActiveFilters ? ( onClick={() => {
<Box sx={HERO_BADGE_SX}> if (!canLaunchQuickJob) return;
<span>Filters</span> const hostnames = rows
<Typography component="span" sx={{ fontWeight: 700, fontSize: "0.8rem", color: MAGIC_UI.accentA }}> .filter((r) => selectedIds.has(r.id))
{activeFilterCount} .map((r) => r.hostname)
</Typography> .filter((hostname) => Boolean(hostname));
</Box> if (!hostnames.length) return;
) : null} onQuickJobLaunch(hostnames);
{selectedIds.size > 0 ? ( }}
<Box sx={HERO_BADGE_SX}> sx={{
<span>Selected</span> borderRadius: 999,
<Typography component="span" sx={{ fontWeight: 700, fontSize: "0.8rem", color: MAGIC_UI.accentB }}> px: 2.2,
{selectedIds.size} textTransform: "none",
</Typography> fontWeight: 600,
</Box> background: canLaunchQuickJob ? "linear-gradient(135deg, #34d399, #22d3ee)" : "rgba(148,163,184,0.2)",
) : null} color: canLaunchQuickJob ? "#041224" : MAGIC_UI.textMuted,
</Box> border: canLaunchQuickJob ? "none" : "1px solid rgba(148,163,184,0.35)",
<Box sx={{ flex: "1 1 320px", minWidth: 0, position: "relative", zIndex: 1 }}> boxShadow: canLaunchQuickJob ? "0 0 24px rgba(45, 212, 191, 0.45)" : "none",
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 1.2 }}> }}
{statTiles.map(({ key, ...tileProps }) => ( >
<StatTile key={key} {...tileProps} /> Quick Job
))} </Button>
</Box> <Tooltip title="Refresh devices to detect changes">
</Box> <span>
</Box> <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>
<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 }}> <Typography sx={{ fontSize: "0.72rem", color: MAGIC_UI.textMuted, textTransform: "uppercase", letterSpacing: 0.45, mb: 0.5 }}>
Custom View Custom View
</Typography> </Typography>
@@ -1724,75 +1691,11 @@ export default function DeviceList({
width: 36, width: 36,
background: "rgba(12,18,35,0.8)", background: "rgba(12,18,35,0.8)",
"&:hover": { borderColor: MAGIC_UI.accentA }, "&: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} <AddIcon fontSize="small" />
</Button> </IconButton>
)} </Box>
</Box>
</Box> </Box>
</Box> </Box>
<Box sx={{ px: { xs: 2, md: 3 }, pb: 3, flexGrow: 1, minHeight: 0, display: "flex", flexDirection: "column" }}> <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 <TextField
select select
label="Connection Type" label="Connection Protocol"
size="small" size="small"
value={connectionType} value={connectionType}
onChange={(e) => setConnectionType(e.target.value)} 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.