/* Page components — Home, Security, Servers, Documentation */
const { useState, useMemo } = React;
/* ---------- shared ---------- */
const Section = ({ id, title, sub, children, kicker, className = "" }) => (
{kicker && (
{kicker}
)}
{title && (
{title}
)}
{sub && (
{sub}
)}
{children}
);
const Btn = ({ kind = "primary", as = "button", href, children, icon, ...rest }) => {
const Comp = as;
const base =
"inline-flex items-center gap-2 px-5 py-3 text-[13px] uppercase tracking-[0.2em] transition-all duration-200 select-none";
const styles =
kind === "primary"
? {
background: "var(--blood)",
color: "#f5e7e7",
border: "1px solid var(--blood-bright)",
boxShadow: "0 0 0 0 rgba(139,0,0,0)",
}
: kind === "ghost"
? {
background: "transparent",
color: "var(--text)",
border: "1px solid var(--border-strong)",
}
: {
background: "var(--panel)",
color: "var(--text)",
border: "1px solid var(--border)",
};
return (
{
if (kind === "primary") e.currentTarget.style.boxShadow = "0 0 24px 0 rgba(139,0,0,0.55)";
if (kind === "ghost") {
e.currentTarget.style.borderColor = "var(--blood)";
e.currentTarget.style.color = "#fff";
}
}}
onMouseLeave={(e) => {
if (kind === "primary") e.currentTarget.style.boxShadow = "0 0 0 0 rgba(139,0,0,0)";
if (kind === "ghost") {
e.currentTarget.style.borderColor = "var(--border-strong)";
e.currentTarget.style.color = "var(--text)";
}
}}
{...rest}
>
{icon}
{children}
);
};
const Card = ({ children, className = "", hoverable = true }) => {
const [hov, setHov] = useState(false);
return (
hoverable && setHov(true)}
onMouseLeave={() => hoverable && setHov(false)}
className={"rounded-sm transition-all duration-300 " + className}
style={{
background: "var(--panel)",
border: "1px solid",
borderColor: hov ? "var(--blood-deep)" : "var(--border)",
boxShadow: hov ? "0 0 0 1px rgba(139,0,0,0.18) inset" : "none",
}}
>
{children}
);
};
/* ---------- HOME ---------- */
const StackTable = ({ t }) => {
const [open, setOpen] = useState(false);
return (
setOpen(o => !o)}
className="w-full flex items-center justify-between px-5 py-4 text-left transition-colors"
style={{ background: "transparent", color: "var(--text)" }}
onMouseEnter={(e) => (e.currentTarget.style.background = "var(--panel-2, rgba(255,255,255,0.02))")}
onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
aria-expanded={open}
>
{t.home.stack.length} {t.home.stackCols.component.toLowerCase()}
{open ? "—" : "+"} {open ? (t.home.collapse || "свернуть") : (t.home.expand || "развернуть")}
▾
{t.home.stackCols.component}
{t.home.stackCols.tech}
{t.home.stackCols.purpose}
{t.home.stack.map((row, i) => (
{row[0]}
{row[1]}
{row[2]}
))}
);
};
const HomePage = ({ t, lang, onGo, theme, showHand = true, bannerSrc = null }) => {
const [tab, setTab] = useState("win");
const [open, setOpen] = useState(false);
const art = window.ARTIFACTS[tab];
const verifyCmd = window.VERIFY_CMDS[tab];
// Live status of FEAR servers — polled every 30s from /api/status.json
const [statusMap, setStatusMap] = useState({}); // id -> { status, ping_ms }
useEffect(() => {
let cancelled = false;
const fetchStatus = async () => {
try {
const r = await fetch("/api/status.json", { cache: "no-store" });
if (!r.ok) return;
const data = await r.json();
if (cancelled) return;
const map = {};
for (const s of (data.servers || [])) map[s.id] = s;
setStatusMap(map);
} catch (e) { /* ignore — show as checking */ }
};
fetchStatus();
const id = setInterval(fetchStatus, 30000);
return () => { cancelled = true; clearInterval(id); };
}, []);
const overallOk = window.SERVERS.some((s) => statusMap[s.id]?.status === "ok");
return (
{/* Hero — server status pinned top-left of the page, banner centered */}
{/* Server status — anchored top-left, just under the header */}
{window.SERVERS.map((s, i) => {
const live = statusMap[s.id];
const isOk = live?.status === "ok";
const isOff = live && live.status !== "ok";
const dotColor = isOk ? "var(--ok)" : isOff ? "var(--err, #b91c1c)" : "var(--text-3)";
const labelColor = dotColor;
const label = isOk ? t.home.online : isOff ? t.home.offline : t.home.checking;
const subLine = isOk && live.ping_ms != null
? `ping · ${live.ping_ms} ms`
: (s.host ? s.host : "");
return (
{lang === "ru" ? s.regionRu : s.regionEn}
{subLine}
{label}
);
})}
{/* Banner — centered, smaller */}
{t.hero.slogan}
{t.hero.sub}
} onClick={() => onGo("home", "download")}>
{t.hero.download}
}
>
{t.hero.github}
{/* What */}
{t.home.whatBody.split(/\n\n+/).map((para, i) => (
{para}
))}
{/* Features */}
{t.home.features.map((f, i) => {
const Icon = [window.Ic.Lock, window.Ic.Network, window.Ic.Brace][i];
return (
{f.t}
{f.d}
);
})}
{/* Status section removed from main flow — lives as overlay in the hero. */}
{/* Tech stack */}
{/* Download */}
{Object.entries(t.home.tabs).map(([k, v]) => (
setTab(k)}
className="relative px-4 py-2.5 text-[12px] uppercase tracking-[0.2em] transition-colors"
style={{
fontFamily: "var(--font-mono)",
color: tab === k ? "var(--text)" : "var(--text-3)",
}}
aria-current={tab === k}
>
{v}
{tab === k && (
)}
))}
Filename
{art.file}
{t.home.sha256}
{art.sha
?
:
{art.note || "—"}
}
}
>
{art.file.split(".").pop().toUpperCase()}
setOpen((v) => !v)}
className="mt-6 flex items-center gap-2 text-[12px] uppercase tracking-[0.2em]"
style={{ color: "var(--text-2)", fontFamily: "var(--font-mono)" }}
>
{t.home.verifyHow}
{open && (
)}
{/* Cross-link to the self-host quick-start. Distinct from the per-platform
client downloads above — Docker is the server side, lives in Servers. */}
);
};
/* ---------- SECURITY ---------- */
const SecurityPage = ({ t }) => {
const [open, setOpen] = useState(0);
return (
{t.sec.crypto.map(([k, v], i) => (
))}
{t.sec.noSeeTitle}
{t.sec.noSee.map((x, i) => (
{x}
))}
{t.sec.seeTitle}
{t.sec.see.map((x, i) => (
{x}
))}
{t.sec.defendTitle}
{t.sec.defend.map((x, i) => (
+ {x}
))}
{t.sec.noDefendTitle}
{t.sec.noDefend.map((x, i) => (
- {x}
))}
{t.sec.known.map((k, i) => {
const isOpen = open === i;
return (
setOpen(isOpen ? -1 : i)}
className="w-full flex items-center gap-3 px-5 py-4 text-left"
aria-expanded={isOpen}
>
{k.s}
{k.t}
{isOpen && (
)}
);
})}
);
};
/* ---------- SERVERS ---------- */
const ServersPage = ({ t }) => {
return (
{t.srv.pick.map((x, i) => (
{String(i + 1).padStart(2, "0")}
{x}
))}
{t.srv.storedTitle}
{t.srv.stored.map((x, i) => (
· {x}
))}
{t.srv.offTitle}
{t.srv.offBody}
{t.srv.fallbackTitle}
{t.srv.fallback}
{/* Requirements — not a numbered step, just a prerequisite list. */}
{t.srv.qsRequirements && (
{t.srv.qsRequirementsTitle}
{t.srv.qsRequirements.map((r, i) => (
· {r}
))}
)}
{/* Numbered steps — install + run + verify. */}
{t.srv.steps.map((s, i) => (
{t.srv.step}
{String(i + 1).padStart(2, "0")}
{s.t}
{s.d}
{s.code && (
)}
))}
{/* "Next" — operational topics that aren't part of the launch sequence. */}
{t.srv.qsAfter && (
{t.srv.qsAfterTitle}
{t.srv.qsAfter.map((it, i) => (
{it.t}
{it.d}
{it.code && (
)}
))}
)}
);
};
/* ---------- DOCS ---------- */
const DocsPage = ({ t }) => (
github.com/shchuchkin-pkims/fear/wiki
{t.doc.intro}
}
>
{t.doc.goto}
);
window.HomePage = HomePage;
window.SecurityPage = SecurityPage;
window.ServersPage = ServersPage;
window.DocsPage = DocsPage;