diff --git a/app/(main)/network-map/page.tsx b/app/(main)/network-map/page.tsx
index f8129220..47b8790e 100644
--- a/app/(main)/network-map/page.tsx
+++ b/app/(main)/network-map/page.tsx
@@ -93,7 +93,7 @@ function mapBackendRowToServer(s: BackendServerRow): Server {
model: s.model ?? "—",
os: s.os ?? "—",
enabled: s.enabled,
- status: (s.status === null ? "offline" : s.status) as Server["status"],
+ status: s.status ?? "unknown",
latency: s.latency != null ? Math.round(s.latency) : null,
sessions: s.sessions ?? 0,
comment: s.comment || undefined,
@@ -217,6 +217,7 @@ const ZOOM_MAX = 6
// ─── Visual config ────────────────────────────────────────────────────────────
const STATUS_STYLE = {
+ unknown: { fill: "var(--muted)", stroke: "var(--muted-foreground)", text: "var(--muted-foreground)", glow: "transparent" },
online: { fill: "#0f2d1f", stroke: "#4ade80", text: "#4ade80", glow: "rgba(74,222,128,0.15)" },
degraded: { fill: "#2d1e06", stroke: "#fbbf24", text: "#fbbf24", glow: "rgba(251,191,36,0.15)" },
offline: { fill: "#2d0f0f", stroke: "#f87171", text: "#f87171", glow: "transparent" },
@@ -471,7 +472,7 @@ function edgeBadgePosition(
// ─── Filter ───────────────────────────────────────────────────────────────────
-type FilterKey = "all" | "online" | "degraded" | "offline" | "jump-host" | "exit-node" | "home-router"
+type FilterKey = "all" | "online" | "degraded" | "offline" | "unknown" | "jump-host" | "exit-node" | "home-router"
function greEdgeKey(e: GreMapEdge): string {
const la = (e.tunnel.localAddress ?? "").trim()
@@ -984,7 +985,7 @@ function ContextMenu({ menu, onClose }: { menu: CtxMenu; onClose: () => void })
{menu.server.name}
{menu.server.host}
@@ -1872,7 +1873,7 @@ function NetworkMapContent() {
const isVisible = (n: typeof nodes[0]) => {
if (!matchesSearch(n)) return false
if (filter === "all") return true
- if (filter === "online" || filter === "degraded" || filter === "offline") return n.status === filter
+ if (filter === "online" || filter === "degraded" || filter === "offline" || filter === "unknown") return n.status === filter
return n.type === filter
}
@@ -2010,6 +2011,7 @@ function NetworkMapContent() {
{ value: "online", label: "Онлайн" },
{ value: "degraded", label: "Внимание" },
{ value: "offline", label: "Оффлайн" },
+ { value: "unknown", label: "Неизвестно" },
{ value: "jump-host", label: "JumpHost" },
{ value: "exit-node", label: "Exit Node" },
{ value: "home-router", label: "Home Router"},
@@ -2180,6 +2182,7 @@ function NetworkMapContent() {
{useLiveData && !hasSnapshot ? "—" : mapServers.filter(s => s.status === "offline").length} оффлайн
+ {useLiveData && !hasSnapshot ? "—" : mapServers.filter(s => s.status === "unknown").length} неизвестно
{greEdges.length}
/
@@ -2682,11 +2685,11 @@ function NetworkMapContent() {
ЛЕГЕНДА
- {(["online","degraded","offline"] as const).map((s, i) => (
-
+ {(["online","degraded","offline","unknown"] as const).map((s, i) => (
+
- {s === "online" ? "Онлайн" : s === "degraded" ? "Внимание" : "Оффлайн"}
+ {s === "online" ? "Онлайн" : s === "degraded" ? "Внимание" : s === "offline" ? "Оффлайн" : "Неизвестно"}
))}
diff --git a/app/(main)/users/page.tsx b/app/(main)/users/page.tsx
index 79b2cfdd..fd3063ee 100644
--- a/app/(main)/users/page.tsx
+++ b/app/(main)/users/page.tsx
@@ -55,7 +55,7 @@ function toServerOptionsFromMock(): UserServerOption[] {
host: s.host,
site: s.site,
country: s.country,
- status: s.status,
+ status: s.status === "unknown" ? null : s.status,
}))
}
diff --git a/components/dashboard/internet-path-map.tsx b/components/dashboard/internet-path-map.tsx
index 43961f4e..b2b85cf5 100644
--- a/components/dashboard/internet-path-map.tsx
+++ b/components/dashboard/internet-path-map.tsx
@@ -15,6 +15,7 @@ const ZOOM_MIN = 0.2
const ZOOM_MAX = 6
const STATUS_STYLE = {
+ unknown: { fill: "var(--muted)", stroke: "var(--muted-foreground)", glow: "transparent" },
online: { fill: "#0f2d1f", stroke: "#4ade80", glow: "rgba(74,222,128,0.15)" },
degraded: { fill: "#2d1e06", stroke: "#fbbf24", glow: "rgba(251,191,36,0.15)" },
offline: { fill: "#2d0f0f", stroke: "#f87171", glow: "transparent" },
@@ -126,7 +127,7 @@ function ServerNode({
name: string
site: string
country: string
- status: "online" | "offline" | "degraded"
+ status: "online" | "offline" | "degraded" | "unknown"
selected: boolean
onClick: () => void
}) {
diff --git a/components/status-badge.tsx b/components/status-badge.tsx
index ab34d2de..861eec55 100644
--- a/components/status-badge.tsx
+++ b/components/status-badge.tsx
@@ -6,6 +6,7 @@ const labels: Record = {
online: "Онлайн",
offline: "Оффлайн",
degraded: "Внимание",
+ unknown: "Неизвестно",
}
export function StatusBadge({ status }: { status: ServerStatus }) {
@@ -13,6 +14,7 @@ export function StatusBadge({ status }: { status: ServerStatus }) {
({ ...w })),
diff --git a/lib/data.ts b/lib/data.ts
index dfa9bdf4..ba5b051a 100644
--- a/lib/data.ts
+++ b/lib/data.ts
@@ -1,4 +1,4 @@
-export type ServerStatus = "online" | "offline" | "degraded"
+export type ServerStatus = "online" | "offline" | "degraded" | "unknown"
export type ServerType = "jump-host" | "exit-node" | "home-router"
export interface WanUplink {
diff --git a/tests/e2e/network-map-data.spec.ts b/tests/e2e/network-map-data.spec.ts
index 8b1f743e..66a4e2da 100644
--- a/tests/e2e/network-map-data.spec.ts
+++ b/tests/e2e/network-map-data.spec.ts
@@ -23,3 +23,15 @@ for(const part of ["servers","gre","probes"] as const)test("Карта: част
await expect(page.getByText("Последний роутер",{exact:true}).first()).toBeVisible()
await expect(page.getByText("Новый роутер",{exact:true})).toHaveCount(0)
})
+
+test("Карта: неизвестный статус не становится оффлайн",async({page})=>{
+ await page.route("**/health",r=>r.fulfill({json:{ok:true}}))
+ await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;const data=body(p);return r.fulfill({json:p===paths.servers?[{...(data as object[])[0],status:null}]:data})})
+ await page.goto("/network-map")
+ await expect(page.getByText("1 неизвестно",{exact:true})).toBeVisible()
+ await expect(page.getByText("0 оффлайн",{exact:true})).toBeVisible()
+ await page.getByText("Роутер",{exact:true}).first().click()
+ await expect(page.locator("span").filter({hasText:/^Неизвестно$/}).first()).toBeVisible()
+ await page.getByRole("button",{name:"Неизвестно",exact:true}).click()
+ await expect(page.getByText("Роутер",{exact:true}).first()).toBeVisible()
+})