mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-15 01:55:48 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b848d4a0a | |||
| a55d9ed729 | |||
| a5c10718ca |
@@ -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" }}>
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
11
Docs/Codex/REVERSE_TUNNEL_UPDATES.md
Normal file
11
Docs/Codex/REVERSE_TUNNEL_UPDATES.md
Normal 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.
|
||||||
Reference in New Issue
Block a user