A node is a VPN server without a panel. awg-agent on it dials the panel (core) over an outbound WSS connection and runs the panel's requests on its own awg-ctrl, which stays on 127.0.0.1 and is unchanged. No inbound port on the node, works behind NAT. - awg-ui/nodes.ts: node registry in ui.db, WSS hub on NODE_PORT with a self-signed cert whose SPKI pin goes into the join string, RPC with timeouts, health polling, /ui/nodes routes. - Nodes enroll with a one-time awgjoin:// string (secret stored hashed, 1 hour) and then authenticate by their own Ed25519 key. The agent checks the pin before sending anything and only forwards /health, /api/*, /awg/*. - server_id 0 is the panel's own awg-ctrl; LOCAL_NODE=off makes the panel run without a VPN of its own. The panel picks a server with X-Server-Id, API keys use their server_id. sen:// still serves one server. - UI: a card per server in the server bar, switching remounts the tab, «Добавить сервер» gives the join command. - CLI: agent service, roles from cli.env, join / unjoin commands. - Release archives include awg-agent.
113 lines
5.7 KiB
TypeScript
113 lines
5.7 KiB
TypeScript
// Copyright (c) 2026 Ivan Vasilev
|
|
// This source code is licensed under the MIT license found in the
|
|
// LICENSE file in the root directory of this source tree.
|
|
import { type NodeInfo, genLabel } from '../lib/shared';
|
|
import { IcoRefresh, IcoPlus, IcoTrash, IcoCopy } from './icons';
|
|
|
|
// Сервер-бар: карточки серверов (core-локальный и ноды) + «Добавить сервер» и
|
|
// (на мобильной) перезапуск AWG под ними. Общий для вкладок — показывает
|
|
// серверы, между которыми переключается содержимое вкладки (пиры / абоненты).
|
|
export default function ServerBar({
|
|
nodes, serverId, hubEnabled, onSelect, onAdd, onJoin, onDelete,
|
|
onRestartAwg, restarting, showRestart = true,
|
|
}: {
|
|
nodes: NodeInfo[];
|
|
serverId: number | null;
|
|
hubEnabled: boolean;
|
|
onSelect: (id: number) => void;
|
|
onAdd: () => void;
|
|
onJoin: (n: NodeInfo) => void;
|
|
onDelete: (n: NodeInfo) => void;
|
|
onRestartAwg: () => void;
|
|
restarting: boolean;
|
|
showRestart?: boolean;
|
|
}) {
|
|
return (
|
|
<div className="server-bar">
|
|
{nodes.map(n => {
|
|
const h = n.health;
|
|
const active = n.id === serverId;
|
|
return (
|
|
<div
|
|
key={n.id}
|
|
className={`server-card server-card--${active ? 'active' : 'inactive'}`}
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-pressed={active}
|
|
onClick={() => onSelect(n.id)}
|
|
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(n.id); } }}
|
|
>
|
|
{/* toggle — виден на мобильном вместо чипа */}
|
|
<div className={`server-toggle${n.online ? ' server-toggle--on' : ''}`} aria-hidden="true">
|
|
<div className="server-toggle-thumb" />
|
|
</div>
|
|
{/* чип онлайн/офлайн — виден на десктопе */}
|
|
<span className={`chip chip--${n.online ? 'online' : 'offline'} server-online-chip`}>
|
|
<span className="chip-dot" />
|
|
</span>
|
|
<div className="server-card-info">
|
|
<span className="server-card-name">{h?.server || n.name}</span>
|
|
{n.pending ? (
|
|
<span className="server-card-ip">ждёт подключения</span>
|
|
) : !n.online ? (
|
|
<span className="server-card-ip">нет связи</span>
|
|
) : h && (
|
|
<>
|
|
<span className="server-card-ip">{h.ip}</span>
|
|
{(h.module || h.tools) ? (
|
|
<span
|
|
className="tip-wrap server-gen-wrap"
|
|
data-tip={`модуль ${h.module || '?'} · tools ${h.tools || '?'}`}
|
|
>
|
|
<span className="chip chip--offline server-gen-chip">{genLabel(h.gen)}</span>
|
|
</span>
|
|
) : (
|
|
<span className="chip chip--offline server-gen-chip">{genLabel(h.gen)}</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
{h && n.online && <span className="server-card-peers">{h.peers} peers</span>}
|
|
{n.pending && (
|
|
<button
|
|
className="btn-icon btn-icon--neutral"
|
|
aria-label="Новая ссылка подключения"
|
|
onClick={e => { e.stopPropagation(); onJoin(n); }}
|
|
>
|
|
<IcoCopy />
|
|
</button>
|
|
)}
|
|
{!n.local && (
|
|
<button
|
|
className="btn-icon btn-icon--danger"
|
|
aria-label={`Удалить сервер ${n.name}`}
|
|
onClick={e => { e.stopPropagation(); onDelete(n); }}
|
|
>
|
|
<IcoTrash />
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
<span
|
|
className="tip-wrap"
|
|
data-tip={hubEnabled ? '' : 'Приём нод выключен: задайте NODE_PORT'}
|
|
>
|
|
<button className="btn server-add" onClick={onAdd} disabled={!hubEnabled}>
|
|
<IcoPlus /> Добавить сервер
|
|
</button>
|
|
</span>
|
|
{/* Перезапуск AWG — только на мобильной (на десктопе кнопка в углу). */}
|
|
{showRestart && serverId !== null && (
|
|
<button
|
|
className={`btn btn--tonal awg-restart-mobile${restarting ? ' spinning' : ''}`}
|
|
onClick={onRestartAwg}
|
|
disabled={restarting}
|
|
>
|
|
<IcoRefresh /> {restarting ? 'Перезапуск…' : 'Перезапустить AWG'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|