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,
|
||||
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 },
|
||||
position: "fixed",
|
||||
top: { xs: 72, md: 88 },
|
||||
right: { xs: 12, md: 20 },
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 3,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
isolation: "isolate",
|
||||
justifyContent: "flex-end",
|
||||
zIndex: 1400,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
) : 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>
|
||||
<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>
|
||||
@@ -1729,70 +1696,6 @@ export default function DeviceList({
|
||||
<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>
|
||||
</Box>
|
||||
</Box>
|
||||
<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
|
||||
select
|
||||
label="Connection Type"
|
||||
label="Connection Protocol"
|
||||
size="small"
|
||||
value={connectionType}
|
||||
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