2026-04-14 18:05:32 +08:00

348 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { getRequestOrigin } from "@/lib/request-origin";
import { buildNodeHy2Config } from "@/lib/subscription";
import { buildNodeAuthUrl, buildTrafficStatsUrl, listNodes } from "@/lib/store";
import { createNodeAction, deleteNodeAction, updateNodeAction } from "./actions";
function getNodePills(node: {
enabled: number;
last_error_message: string | null;
}) {
return [
{
label: node.enabled ? "已启用" : "已禁用",
className: `status-pill ${node.enabled ? "status-pill--ok" : "status-pill--off"}`,
},
node.last_error_message
? { label: "同步异常", className: "status-pill status-pill--danger" }
: { label: "同步正常", className: "status-pill status-pill--sage" },
];
}
export default async function NodesPage() {
const [nodes, origin] = await Promise.all([listNodes(), getRequestOrigin()]);
return (
<main className="page-stack">
<section className="hero-panel">
<div className="hero-copy">
<span className="eyebrow">NODES</span>
<h2></h2>
<p className="page-lede">
hy2 URLtrafficStats
</p>
<div className="badge-row">
<span className="status-pill status-pill--neutral">URL </span>
<span className="status-pill status-pill--warm">Secret </span>
</div>
</div>
<div className="hero-aside">
<div className="stat-tile">
<span></span>
<strong>{nodes.length}</strong>
</div>
<div className="stat-tile">
<span></span>
<strong>{nodes.filter((node) => node.enabled).length}</strong>
</div>
</div>
</section>
<section className="panel-card">
<div className="section-title">
<h3></h3>
</div>
<form action={createNodeAction} className="form-grid">
<label className="field">
<span>Slug</span>
<input name="slug" placeholder="tokyo-01" required />
</label>
<label className="field">
<span></span>
<input name="name" placeholder="Tokyo 01" required />
</label>
<label className="field">
<span></span>
<input name="server_host" placeholder="jp1.example.com" required />
</label>
<label className="field">
<span>hy2 </span>
<input name="hy2_listen" defaultValue=":443" placeholder=":443 或 0.0.0.0:443" />
</label>
<label className="field">
<span></span>
<input
name="server_port"
type="number"
min="1"
max="65535"
defaultValue={443}
required
/>
</label>
<label className="field">
<span>Clash </span>
<input name="client_name" placeholder="JP1 - 30M - hy2" />
</label>
<label className="field">
<span> Token</span>
<input name="auth_token" placeholder="留空自动生成" />
</label>
<label className="field">
<span> Mbps</span>
<input name="bandwidth_up_mbps" type="number" min="1" defaultValue={30} required />
</label>
<label className="field">
<span> Mbps</span>
<input name="bandwidth_down_mbps" type="number" min="1" defaultValue={30} required />
</label>
<label className="field">
<span>SNI</span>
<input name="sni" placeholder="留空使用默认" />
</label>
<label className="checkbox">
<input name="skip_cert_verify" type="checkbox" />
<span></span>
</label>
<label className="field">
<span>trafficStats </span>
<input name="traffic_stats_listen" defaultValue=":9999" required />
</label>
<label className="field">
<span>trafficStats URL</span>
<input
name="traffic_stats_url"
placeholder="留空自动按客户端地址 + 监听端口生成"
/>
</label>
<label className="field">
<span>trafficStats Secret</span>
<input name="traffic_stats_secret" placeholder="留空自动生成" />
</label>
<label className="field">
<span></span>
<input
name="poll_interval_seconds"
type="number"
min="10"
max="30"
defaultValue={15}
required
/>
</label>
<label className="checkbox">
<input name="enabled" type="checkbox" defaultChecked />
<span></span>
</label>
<button className="primary-button" type="submit">
</button>
</form>
</section>
<section className="stack-lg">
{nodes.map((node) => {
const authUrl = buildNodeAuthUrl(node.slug, node.auth_token, origin);
const suggestedTrafficStatsUrl = buildTrafficStatsUrl(
node.server_host,
node.traffic_stats_listen,
);
return (
<article key={node.id} className="panel-card entity-card">
<div className="section-title section-title--top">
<div>
<h3>{node.name}</h3>
<p className="muted mono">/{node.slug}</p>
</div>
<div className="align-right stack-tight">
<div className="badge-row badge-row--end">
{getNodePills(node).map((pill) => (
<span key={pill.label} className={pill.className}>
{pill.label}
</span>
))}
</div>
<p className="muted">
{node.last_sync_ok_at ?? "无"} / {" "}
{node.last_error_at ?? "无"}
</p>
</div>
</div>
<div className="summary-grid">
<div className="metric-inline">
<span>hy2 </span>
<strong className="mono">{node.hy2_listen}</strong>
</div>
<div className="metric-inline">
<span></span>
<strong className="mono">{node.server_host}:{node.server_port}</strong>
</div>
<div className="metric-inline">
<span>trafficStats</span>
<strong className="mono break-all">{node.traffic_stats_url}</strong>
</div>
</div>
<form action={updateNodeAction} className="form-grid compact">
<input name="id" type="hidden" value={node.id} />
<label className="field">
<span>Slug</span>
<input name="slug" defaultValue={node.slug} required />
</label>
<label className="field">
<span></span>
<input name="name" defaultValue={node.name} required />
</label>
<label className="field">
<span></span>
<input name="server_host" defaultValue={node.server_host} required />
</label>
<label className="field">
<span>hy2 </span>
<input
name="hy2_listen"
defaultValue={node.hy2_listen}
placeholder=":443 或 0.0.0.0:443"
/>
</label>
<label className="field">
<span></span>
<input
name="server_port"
type="number"
min="1"
max="65535"
defaultValue={node.server_port}
required
/>
</label>
<label className="field">
<span>Clash </span>
<input name="client_name" defaultValue={node.client_name} />
</label>
<label className="field">
<span> Token</span>
<input name="auth_token" defaultValue={node.auth_token} required />
</label>
<label className="field">
<span> Mbps</span>
<input
name="bandwidth_up_mbps"
type="number"
min="1"
defaultValue={node.bandwidth_up_mbps}
required
/>
</label>
<label className="field">
<span> Mbps</span>
<input
name="bandwidth_down_mbps"
type="number"
min="1"
defaultValue={node.bandwidth_down_mbps}
required
/>
</label>
<label className="field">
<span>SNI</span>
<input name="sni" defaultValue={node.sni ?? ""} />
</label>
<label className="checkbox">
<input
name="skip_cert_verify"
type="checkbox"
defaultChecked={Boolean(node.skip_cert_verify)}
/>
<span></span>
</label>
<label className="field">
<span>trafficStats </span>
<input
name="traffic_stats_listen"
defaultValue={node.traffic_stats_listen}
required
/>
</label>
<label className="field">
<span>trafficStats URL</span>
<input
name="traffic_stats_url"
defaultValue={node.traffic_stats_url}
/>
</label>
<label className="field">
<span>trafficStats Secret</span>
<input
name="traffic_stats_secret"
defaultValue={node.traffic_stats_secret}
placeholder="留空保持当前值"
/>
</label>
<label className="field">
<span></span>
<input
name="poll_interval_seconds"
type="number"
min="10"
max="30"
defaultValue={node.poll_interval_seconds}
required
/>
</label>
<label className="checkbox">
<input
name="enabled"
type="checkbox"
defaultChecked={Boolean(node.enabled)}
/>
<span></span>
</label>
<button className="ghost-button" type="submit">
</button>
</form>
<div className="instruction-block">
<strong></strong>
<p className="muted">
hy2 HTTP URL
</p>
<pre>
{buildNodeHy2Config(node, origin)}
</pre>
<p className="muted">
<span className="mono">{node.traffic_stats_url}</span>
</p>
<p className="muted">
<span className="mono">{suggestedTrafficStatsUrl}</span>
</p>
<p className="muted">
URL<span className="mono">{authUrl}</span>
</p>
<a
className="ghost-button inline-button"
href={`/api/admin/nodes/${node.slug}/config`}
>
hy2 config.yaml
</a>
{node.last_error_message ? (
<p className="error-text">{node.last_error_message}</p>
) : null}
</div>
<form action={deleteNodeAction}>
<input name="id" type="hidden" value={node.id} />
<button className="danger-button" type="submit">
</button>
</form>
</article>
);
})}
</section>
</main>
);
}