mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 23:13:53 +08:00
Harden config sync defaults, crash resume, and network nav collapse.
Disable auto-sync by default, enforce single-flight cycles with crash requeue, delay new scheduled runs after restart, and make the network sidebar collapsible. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0eead0e662
commit
f5b4a14c0f
14 changed files with 387 additions and 99 deletions
|
|
@ -3,6 +3,8 @@ import { NavLink, Outlet, useLocation } from "react-router-dom";
|
|||
import { NETWORK_NAV, type NetworkNavGroupId } from "../../config/networkNav";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
||||
const SIDEBAR_KEY = "netx.network.sidebarCollapsed";
|
||||
|
||||
function groupContainsPath(groupId: NetworkNavGroupId, pathname: string): boolean {
|
||||
const group = NETWORK_NAV.find((g) => g.id === groupId);
|
||||
if (!group) return false;
|
||||
|
|
@ -11,9 +13,18 @@ function groupContainsPath(groupId: NetworkNavGroupId, pathname: string): boolea
|
|||
);
|
||||
}
|
||||
|
||||
function readSidebarCollapsed(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(SIDEBAR_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function NetworkLayout() {
|
||||
const { t } = useI18n();
|
||||
const { pathname } = useLocation();
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(readSidebarCollapsed);
|
||||
const [openGroups, setOpenGroups] = useState<Record<NetworkNavGroupId, boolean>>(() => {
|
||||
const init = {} as Record<NetworkNavGroupId, boolean>;
|
||||
for (const g of NETWORK_NAV) {
|
||||
|
|
@ -35,50 +46,95 @@ export function NetworkLayout() {
|
|||
setOpenGroups((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setSidebarCollapsed((prev) => {
|
||||
const next = !prev;
|
||||
try {
|
||||
localStorage.setItem(SIDEBAR_KEY, next ? "1" : "0");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="network-shell">
|
||||
<div className={`network-shell${sidebarCollapsed ? " is-nav-collapsed" : ""}`}>
|
||||
<aside className="network-nav" aria-label={t("network.title")}>
|
||||
<nav className="network-nav__scroll">
|
||||
{NETWORK_NAV.map((group) => {
|
||||
const open = Boolean(openGroups[group.id]);
|
||||
const activeGroup = groupContainsPath(group.id, pathname);
|
||||
return (
|
||||
<div
|
||||
key={group.id}
|
||||
className={`network-nav__group${open ? " is-open" : ""}${activeGroup ? " is-active-group" : ""}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="network-nav__group-toggle"
|
||||
aria-expanded={open}
|
||||
onClick={() => toggleGroup(group.id)}
|
||||
<div className="network-nav__head">
|
||||
{!sidebarCollapsed ? <span className="network-nav__title">{t("network.title")}</span> : null}
|
||||
<button
|
||||
type="button"
|
||||
className="network-nav__collapse-btn"
|
||||
title={sidebarCollapsed ? t("network.expandNav") : t("network.collapseNav")}
|
||||
aria-label={sidebarCollapsed ? t("network.expandNav") : t("network.collapseNav")}
|
||||
aria-expanded={!sidebarCollapsed}
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
{sidebarCollapsed ? "»" : "«"}
|
||||
</button>
|
||||
</div>
|
||||
{!sidebarCollapsed ? (
|
||||
<nav className="network-nav__scroll">
|
||||
{NETWORK_NAV.map((group) => {
|
||||
const open = Boolean(openGroups[group.id]);
|
||||
const activeGroup = groupContainsPath(group.id, pathname);
|
||||
return (
|
||||
<div
|
||||
key={group.id}
|
||||
className={`network-nav__group${open ? " is-open" : ""}${activeGroup ? " is-active-group" : ""}`}
|
||||
>
|
||||
<span className="network-nav__chevron" aria-hidden>
|
||||
{open ? "▾" : "▸"}
|
||||
</span>
|
||||
<span className="network-nav__group-label">{t(group.labelKey)}</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<ul className="network-nav__list">
|
||||
{group.items.map((item) => (
|
||||
<li key={item.id}>
|
||||
<NavLink
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`network-nav__link${isActive ? " is-active" : ""}`
|
||||
}
|
||||
end={item.path === "/network/devices"}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<button
|
||||
type="button"
|
||||
className="network-nav__group-toggle"
|
||||
aria-expanded={open}
|
||||
onClick={() => toggleGroup(group.id)}
|
||||
>
|
||||
<span className="network-nav__chevron" aria-hidden>
|
||||
{open ? "▾" : "▸"}
|
||||
</span>
|
||||
<span className="network-nav__group-label">{t(group.labelKey)}</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<ul className="network-nav__list">
|
||||
{group.items.map((item) => (
|
||||
<li key={item.id}>
|
||||
<NavLink
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`network-nav__link${isActive ? " is-active" : ""}`
|
||||
}
|
||||
end={item.path === "/network/devices"}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
) : (
|
||||
<nav className="network-nav__rail" aria-label={t("network.title")}>
|
||||
{NETWORK_NAV.flatMap((group) =>
|
||||
group.items.map((item) => (
|
||||
<NavLink
|
||||
key={item.id}
|
||||
to={item.path}
|
||||
title={t(item.labelKey)}
|
||||
className={({ isActive }) =>
|
||||
`network-nav__rail-link${isActive ? " is-active" : ""}`
|
||||
}
|
||||
end={item.path === "/network/devices"}
|
||||
>
|
||||
{t(item.labelKey).slice(0, 1)}
|
||||
</NavLink>
|
||||
)),
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
</aside>
|
||||
<div className="network-main">
|
||||
<Outlet />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue