[优化] 格式化

This commit is contained in:
ryan
2026-05-30 13:15:20 +08:00
parent 28b1305906
commit 9ec3d5b42d
110 changed files with 29155 additions and 7642 deletions
@@ -64,9 +64,9 @@ export function requestNodeAgentUpdate(
}
export function requestNodeForceSync(id: number) {
return apiRequest<NodeItem>(`/nodes/${id}/force-sync`, {
method: 'POST',
});
return apiRequest<NodeItem>(`/nodes/${id}/force-sync`, {
method: 'POST',
});
}
export function requestNodeOpenrestyRestart(id: number) {
@@ -404,7 +404,9 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
});
setHealthEventCleanupModalOpen(false);
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['node-observability', nodeId] }),
queryClient.invalidateQueries({
queryKey: ['node-observability', nodeId],
}),
queryClient.invalidateQueries({ queryKey: ['dashboard', 'overview'] }),
]);
},
@@ -869,9 +871,7 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
)}
</AppCard>
<AppCard
title="实时资源"
>
<AppCard title="实时资源">
{observabilityQuery.isLoading ? (
<LoadingState />
) : observabilityQuery.isError ? (
@@ -930,9 +930,7 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
)}
</AppCard>
<AppCard
title="网络流量"
>
<AppCard title="网络流量">
{observabilityQuery.isLoading ? (
<LoadingState />
) : observabilityQuery.isError ? (
@@ -1628,7 +1626,9 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
{nodeInstallCommand ? (
<div>
<p className="mb-2 text-sm font-medium text-[var(--foreground-primary)]">脚本部署 (Linux / macOS)</p>
<p className="mb-2 text-sm font-medium text-[var(--foreground-primary)]">
脚本部署 (Linux / macOS)
</p>
<CodeBlock className="whitespace-pre-wrap">
{nodeInstallCommand}
</CodeBlock>
@@ -1637,7 +1637,9 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
{nodeDockerInstallCommand ? (
<div>
<p className="mb-2 text-sm font-medium text-[var(--foreground-primary)]">Docker 部署</p>
<p className="mb-2 text-sm font-medium text-[var(--foreground-primary)]">
Docker 部署
</p>
<CodeBlock className="whitespace-pre-wrap">
{nodeDockerInstallCommand}
</CodeBlock>
@@ -1835,8 +1837,12 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
/>
) : (
<div className="space-y-3 text-sm text-[var(--foreground-secondary)]">
<p>该操作会删除当前节点已记录的全部健康事件,包括活动中与已恢复事件。</p>
<p>这不会影响节点后续继续上报新的健康事件,但现有时间线与相关摘要会立即刷新。</p>
<p>
该操作会删除当前节点已记录的全部健康事件,包括活动中与已恢复事件。
</p>
<p>
这不会影响节点后续继续上报新的健康事件,但现有时间线与相关摘要会立即刷新。
</p>
</div>
)}
</AppModal>
@@ -151,8 +151,8 @@ function getRegionCenter(feature: GeoJsonFeature) {
}
function buildRegionOptions() {
const features = ((worldGeoJson as { features?: GeoJsonFeature[] }).features ??
[]) as GeoJsonFeature[];
const features = ((worldGeoJson as { features?: GeoJsonFeature[] })
.features ?? []) as GeoJsonFeature[];
const options = new Map<string, RegionOption>();
for (const feature of features) {
@@ -186,7 +186,7 @@ function buildFormValues(node?: Partial<NodeItem> | null): NodeEditorValues {
ip: node.ip ?? '',
auto_update_enabled: node.auto_update_enabled ?? false,
geo_manual_override: node.geo_manual_override ?? false,
geo_region: node.geo_manual_override ? node.geo_name ?? '' : '',
geo_region: node.geo_manual_override ? (node.geo_name ?? '') : '',
geo_name: node.geo_name ?? '',
geo_latitude:
node.geo_latitude === undefined || node.geo_latitude === null
@@ -354,10 +354,10 @@ export function NodesPage() {
{isWSConnectedLastSeen(node.last_seen_at)
? 'WS 已连接'
: isMeaningfulTime(node.last_seen_at)
? `${formatRelativeTime(
node.last_seen_at,
)} · ${formatDateTime(node.last_seen_at)}`
: '暂无'}
? `${formatRelativeTime(
node.last_seen_at,
)} · ${formatDateTime(node.last_seen_at)}`
: '暂无'}
</td>
<td className="px-3 py-4">
<div className="flex flex-wrap gap-2">
+195 -195
View File
@@ -1,188 +1,188 @@
import type { ReleaseChannel } from '@/features/update/types';
export interface NodeItem {
id: number;
node_id: string;
name: string;
ip: string;
geo_name: string;
geo_latitude?: number | null;
geo_longitude?: number | null;
geo_manual_override: boolean;
agent_token: string;
auto_update_enabled: boolean;
update_requested: boolean;
update_channel: ReleaseChannel;
update_tag: string;
restart_openresty_requested: boolean;
agent_version: string;
nginx_version: string;
openresty_status: 'healthy' | 'unhealthy' | 'unknown';
openresty_message: string;
status: 'online' | 'offline' | 'pending';
current_version: string;
last_seen_at: string;
last_error: string;
latest_apply_result: 'success' | 'warning' | 'failed' | '';
latest_apply_message: string;
latest_apply_checksum: string;
latest_main_config_checksum: string;
latest_route_config_checksum: string;
latest_support_file_count: number;
latest_apply_at?: string | null;
created_at: string;
updated_at: string;
}
export interface NodeBootstrapToken {
discovery_token: string;
}
export interface NodeMutationPayload {
name: string;
ip: string;
auto_update_enabled: boolean;
geo_name: string;
geo_latitude?: number | null;
geo_longitude?: number | null;
geo_manual_override: boolean;
}
export interface NodeAgentReleaseInfo {
tag_name: string;
body: string;
html_url: string;
published_at: string;
current_version: string;
has_update: boolean;
channel: ReleaseChannel;
prerelease: boolean;
update_requested: boolean;
requested_channel: ReleaseChannel;
requested_tag: string;
}
export interface NodeAgentUpdatePayload {
channel?: ReleaseChannel;
tag_name?: string;
}
export interface NodeSystemProfile {
hostname: string;
os_name: string;
os_version: string;
kernel_version: string;
architecture: string;
cpu_model: string;
cpu_cores: number;
total_memory_bytes: number;
total_disk_bytes: number;
uptime_seconds: number;
reported_at: string;
}
export interface NodeMetricSnapshot {
captured_at: string;
cpu_usage_percent: number;
memory_used_bytes: number;
memory_total_bytes: number;
storage_used_bytes: number;
storage_total_bytes: number;
disk_read_bytes: number;
disk_write_bytes: number;
network_rx_bytes: number;
network_tx_bytes: number;
openresty_rx_bytes: number;
openresty_tx_bytes: number;
openresty_connections: number;
}
export interface NodeTrafficReport {
window_started_at: string;
window_ended_at: string;
request_count: number;
error_count: number;
unique_visitor_count: number;
status_codes_json: string;
top_domains_json: string;
source_countries_json: string;
}
export interface NodeTrafficTrendPoint {
bucket_started_at: string;
request_count: number;
error_count: number;
unique_visitor_count: number;
}
export interface NodeCapacityTrendPoint {
bucket_started_at: string;
average_cpu_usage_percent: number;
average_memory_usage_percent: number;
reported_nodes: number;
}
export interface NodeNetworkTrendPoint {
bucket_started_at: string;
network_rx_bytes: number;
network_tx_bytes: number;
openresty_rx_bytes: number;
openresty_tx_bytes: number;
reported_nodes: number;
}
export interface NodeDiskIOTrendPoint {
bucket_started_at: string;
disk_read_bytes: number;
disk_write_bytes: number;
reported_nodes: number;
}
export interface NodeDistributionItem {
key: string;
value: number;
}
export interface NodeTrafficDistributions {
status_codes: NodeDistributionItem[];
top_domains: NodeDistributionItem[];
source_countries: NodeDistributionItem[];
}
export interface NodeTrafficSummary {
window_started_at: string;
window_ended_at: string;
request_count: number;
unique_visitor_count: number;
error_count: number;
estimated_qps: number;
error_rate_percent: number;
}
export interface NodeHealthSummary {
active_alerts: number;
critical_alerts: number;
warning_alerts: number;
info_alerts: number;
resolved_alerts: number;
has_capacity_risk: boolean;
has_traffic_risk: boolean;
has_runtime_risk: boolean;
}
export interface NodeObservabilityAnalytics {
traffic: NodeTrafficSummary;
distributions: NodeTrafficDistributions;
health: NodeHealthSummary;
}
export interface NodeObservabilityTrends {
traffic_24h: NodeTrafficTrendPoint[];
capacity_24h: NodeCapacityTrendPoint[];
network_24h: NodeNetworkTrendPoint[];
disk_io_24h: NodeDiskIOTrendPoint[];
}
import type { ReleaseChannel } from '@/features/update/types';
export interface NodeItem {
id: number;
node_id: string;
name: string;
ip: string;
geo_name: string;
geo_latitude?: number | null;
geo_longitude?: number | null;
geo_manual_override: boolean;
agent_token: string;
auto_update_enabled: boolean;
update_requested: boolean;
update_channel: ReleaseChannel;
update_tag: string;
restart_openresty_requested: boolean;
agent_version: string;
nginx_version: string;
openresty_status: 'healthy' | 'unhealthy' | 'unknown';
openresty_message: string;
status: 'online' | 'offline' | 'pending';
current_version: string;
last_seen_at: string;
last_error: string;
latest_apply_result: 'success' | 'warning' | 'failed' | '';
latest_apply_message: string;
latest_apply_checksum: string;
latest_main_config_checksum: string;
latest_route_config_checksum: string;
latest_support_file_count: number;
latest_apply_at?: string | null;
created_at: string;
updated_at: string;
}
export interface NodeBootstrapToken {
discovery_token: string;
}
export interface NodeMutationPayload {
name: string;
ip: string;
auto_update_enabled: boolean;
geo_name: string;
geo_latitude?: number | null;
geo_longitude?: number | null;
geo_manual_override: boolean;
}
export interface NodeAgentReleaseInfo {
tag_name: string;
body: string;
html_url: string;
published_at: string;
current_version: string;
has_update: boolean;
channel: ReleaseChannel;
prerelease: boolean;
update_requested: boolean;
requested_channel: ReleaseChannel;
requested_tag: string;
}
export interface NodeAgentUpdatePayload {
channel?: ReleaseChannel;
tag_name?: string;
}
export interface NodeSystemProfile {
hostname: string;
os_name: string;
os_version: string;
kernel_version: string;
architecture: string;
cpu_model: string;
cpu_cores: number;
total_memory_bytes: number;
total_disk_bytes: number;
uptime_seconds: number;
reported_at: string;
}
export interface NodeMetricSnapshot {
captured_at: string;
cpu_usage_percent: number;
memory_used_bytes: number;
memory_total_bytes: number;
storage_used_bytes: number;
storage_total_bytes: number;
disk_read_bytes: number;
disk_write_bytes: number;
network_rx_bytes: number;
network_tx_bytes: number;
openresty_rx_bytes: number;
openresty_tx_bytes: number;
openresty_connections: number;
}
export interface NodeTrafficReport {
window_started_at: string;
window_ended_at: string;
request_count: number;
error_count: number;
unique_visitor_count: number;
status_codes_json: string;
top_domains_json: string;
source_countries_json: string;
}
export interface NodeTrafficTrendPoint {
bucket_started_at: string;
request_count: number;
error_count: number;
unique_visitor_count: number;
}
export interface NodeCapacityTrendPoint {
bucket_started_at: string;
average_cpu_usage_percent: number;
average_memory_usage_percent: number;
reported_nodes: number;
}
export interface NodeNetworkTrendPoint {
bucket_started_at: string;
network_rx_bytes: number;
network_tx_bytes: number;
openresty_rx_bytes: number;
openresty_tx_bytes: number;
reported_nodes: number;
}
export interface NodeDiskIOTrendPoint {
bucket_started_at: string;
disk_read_bytes: number;
disk_write_bytes: number;
reported_nodes: number;
}
export interface NodeDistributionItem {
key: string;
value: number;
}
export interface NodeTrafficDistributions {
status_codes: NodeDistributionItem[];
top_domains: NodeDistributionItem[];
source_countries: NodeDistributionItem[];
}
export interface NodeTrafficSummary {
window_started_at: string;
window_ended_at: string;
request_count: number;
unique_visitor_count: number;
error_count: number;
estimated_qps: number;
error_rate_percent: number;
}
export interface NodeHealthSummary {
active_alerts: number;
critical_alerts: number;
warning_alerts: number;
info_alerts: number;
resolved_alerts: number;
has_capacity_risk: boolean;
has_traffic_risk: boolean;
has_runtime_risk: boolean;
}
export interface NodeObservabilityAnalytics {
traffic: NodeTrafficSummary;
distributions: NodeTrafficDistributions;
health: NodeHealthSummary;
}
export interface NodeObservabilityTrends {
traffic_24h: NodeTrafficTrendPoint[];
capacity_24h: NodeCapacityTrendPoint[];
network_24h: NodeNetworkTrendPoint[];
disk_io_24h: NodeDiskIOTrendPoint[];
}
export interface NodeHealthEvent {
event_type: string;
severity: string;
@@ -194,13 +194,13 @@ export interface NodeHealthEvent {
reported_at: string;
resolved_at?: string | null;
}
export interface NodeObservability {
node_id: string;
profile: NodeSystemProfile | null;
metric_snapshots: NodeMetricSnapshot[];
traffic_reports: NodeTrafficReport[];
health_events: NodeHealthEvent[];
analytics: NodeObservabilityAnalytics;
trends: NodeObservabilityTrends;
}
export interface NodeObservability {
node_id: string;
profile: NodeSystemProfile | null;
metric_snapshots: NodeMetricSnapshot[];
traffic_reports: NodeTrafficReport[];
health_events: NodeHealthEvent[];
analytics: NodeObservabilityAnalytics;
trends: NodeObservabilityTrends;
}
+194 -189
View File
@@ -1,189 +1,194 @@
import type { NodeItem } from '@/features/nodes/types';
export const WS_CONNECTED_LAST_SEEN = '__OPENFLARE_WS_CONNECTED__';
export function isWSConnectedLastSeen(value: string | null | undefined) {
return value === WS_CONNECTED_LAST_SEEN;
}
export function isMeaningfulTime(value: string | null | undefined) {
return (
Boolean(value) &&
!isWSConnectedLastSeen(value) &&
!String(value).startsWith('0001-01-01')
);
}
export function getNodeStatusVariant(status: NodeItem['status']) {
if (status === 'online') {
return 'success';
}
if (status === 'pending') {
return 'warning';
}
return 'danger';
}
export function getNodeStatusLabel(status: NodeItem['status']) {
if (status === 'online') {
return '在线';
}
if (status === 'pending') {
return '待接入';
}
return '离线';
}
export function getApplyVariant(result: NodeItem['latest_apply_result']) {
if (result === 'success') {
return 'success';
}
if (result === 'warning') {
return 'warning';
}
if (result === 'failed') {
return 'danger';
}
return 'warning';
}
export function getApplyLabel(result: NodeItem['latest_apply_result']) {
if (result === 'success') {
return '成功';
}
if (result === 'warning') {
return '警告';
}
if (result === 'failed') {
return '失败';
}
return '暂无';
}
export function getUpdateMode(node: NodeItem) {
if (node.update_requested) {
if (node.update_channel === 'preview') {
return { label: '等待预览更新', variant: 'warning' as const };
}
return { label: '等待更新', variant: 'warning' as const };
}
if (node.auto_update_enabled) {
return { label: '自动', variant: 'success' as const };
}
return { label: '手动', variant: 'info' as const };
}
export function getOpenrestyStatusVariant(status: NodeItem['openresty_status']) {
if (status === 'healthy') {
return 'success';
}
if (status === 'unhealthy') {
return 'danger';
}
return 'warning';
}
export function getOpenrestyStatusLabel(status: NodeItem['openresty_status']) {
if (status === 'healthy') {
return '健康';
}
if (status === 'unhealthy') {
return '异常';
}
return '未知';
}
function parseVersionParts(version: string) {
const normalized = version.trim().replace(/^v/i, '');
if (!normalized || normalized.toLowerCase() === 'unknown') {
return null;
}
return normalized.split('.').map((segment) => {
const matched = segment.trim().match(/^\d+/);
return matched ? Number.parseInt(matched[0], 10) : 0;
});
}
function isOlderVersion(current: string, target: string) {
const currentParts = parseVersionParts(current);
const targetParts = parseVersionParts(target);
if (!currentParts || !targetParts) {
return false;
}
const maxLength = Math.max(currentParts.length, targetParts.length);
for (let index = 0; index < maxLength; index += 1) {
const currentPart = currentParts[index] ?? 0;
const targetPart = targetParts[index] ?? 0;
if (currentPart < targetPart) {
return true;
}
if (currentPart > targetPart) {
return false;
}
}
return false;
}
export function shouldShowManualUpdate(
agentVersion: string,
serverVersion: string,
) {
const normalizedServerVersion = serverVersion.trim();
const normalizedAgentVersion = agentVersion.trim();
if (
!normalizedServerVersion ||
normalizedServerVersion.toLowerCase() === 'dev' ||
!normalizedAgentVersion ||
normalizedAgentVersion.toLowerCase() === 'unknown'
) {
return false;
}
return isOlderVersion(normalizedAgentVersion, normalizedServerVersion);
}
export function getServerUrl(value: string) {
return value.trim().replace(/\/+$/, '');
}
const installerScriptUrl =
'https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh';
export function buildNodeInstallCommand(serverUrl: string, agentToken: string) {
return [
`curl -fsSL ${installerScriptUrl} | bash -s -- \\`,
` --server-url ${serverUrl} \\`,
` --agent-token ${agentToken}`,
].join('\n');
}
export function buildNodeDockerInstallCommand(serverUrl: string, agentToken: string) {
return [
`docker run -d --name openflare-agent --restart unless-stopped \\`,
` -p 80:80 -p 443:443 \\`,
` -e OPENFLARE_SERVER_URL=${serverUrl} \\`,
` -e OPENFLARE_AGENT_TOKEN=${agentToken} \\`,
` ghcr.io/rain-kl/openflare-agent:latest`
].join('\n');
}
import type { NodeItem } from '@/features/nodes/types';
export const WS_CONNECTED_LAST_SEEN = '__OPENFLARE_WS_CONNECTED__';
export function isWSConnectedLastSeen(value: string | null | undefined) {
return value === WS_CONNECTED_LAST_SEEN;
}
export function isMeaningfulTime(value: string | null | undefined) {
return (
Boolean(value) &&
!isWSConnectedLastSeen(value) &&
!String(value).startsWith('0001-01-01')
);
}
export function getNodeStatusVariant(status: NodeItem['status']) {
if (status === 'online') {
return 'success';
}
if (status === 'pending') {
return 'warning';
}
return 'danger';
}
export function getNodeStatusLabel(status: NodeItem['status']) {
if (status === 'online') {
return '在线';
}
if (status === 'pending') {
return '待接入';
}
return '离线';
}
export function getApplyVariant(result: NodeItem['latest_apply_result']) {
if (result === 'success') {
return 'success';
}
if (result === 'warning') {
return 'warning';
}
if (result === 'failed') {
return 'danger';
}
return 'warning';
}
export function getApplyLabel(result: NodeItem['latest_apply_result']) {
if (result === 'success') {
return '成功';
}
if (result === 'warning') {
return '警告';
}
if (result === 'failed') {
return '失败';
}
return '暂无';
}
export function getUpdateMode(node: NodeItem) {
if (node.update_requested) {
if (node.update_channel === 'preview') {
return { label: '等待预览更新', variant: 'warning' as const };
}
return { label: '等待更新', variant: 'warning' as const };
}
if (node.auto_update_enabled) {
return { label: '自动', variant: 'success' as const };
}
return { label: '手动', variant: 'info' as const };
}
export function getOpenrestyStatusVariant(
status: NodeItem['openresty_status'],
) {
if (status === 'healthy') {
return 'success';
}
if (status === 'unhealthy') {
return 'danger';
}
return 'warning';
}
export function getOpenrestyStatusLabel(status: NodeItem['openresty_status']) {
if (status === 'healthy') {
return '健康';
}
if (status === 'unhealthy') {
return '异常';
}
return '未知';
}
function parseVersionParts(version: string) {
const normalized = version.trim().replace(/^v/i, '');
if (!normalized || normalized.toLowerCase() === 'unknown') {
return null;
}
return normalized.split('.').map((segment) => {
const matched = segment.trim().match(/^\d+/);
return matched ? Number.parseInt(matched[0], 10) : 0;
});
}
function isOlderVersion(current: string, target: string) {
const currentParts = parseVersionParts(current);
const targetParts = parseVersionParts(target);
if (!currentParts || !targetParts) {
return false;
}
const maxLength = Math.max(currentParts.length, targetParts.length);
for (let index = 0; index < maxLength; index += 1) {
const currentPart = currentParts[index] ?? 0;
const targetPart = targetParts[index] ?? 0;
if (currentPart < targetPart) {
return true;
}
if (currentPart > targetPart) {
return false;
}
}
return false;
}
export function shouldShowManualUpdate(
agentVersion: string,
serverVersion: string,
) {
const normalizedServerVersion = serverVersion.trim();
const normalizedAgentVersion = agentVersion.trim();
if (
!normalizedServerVersion ||
normalizedServerVersion.toLowerCase() === 'dev' ||
!normalizedAgentVersion ||
normalizedAgentVersion.toLowerCase() === 'unknown'
) {
return false;
}
return isOlderVersion(normalizedAgentVersion, normalizedServerVersion);
}
export function getServerUrl(value: string) {
return value.trim().replace(/\/+$/, '');
}
const installerScriptUrl =
'https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh';
export function buildNodeInstallCommand(serverUrl: string, agentToken: string) {
return [
`curl -fsSL ${installerScriptUrl} | bash -s -- \\`,
` --server-url ${serverUrl} \\`,
` --agent-token ${agentToken}`,
].join('\n');
}
export function buildNodeDockerInstallCommand(
serverUrl: string,
agentToken: string,
) {
return [
`docker run -d --name openflare-agent --restart unless-stopped \\`,
` -p 80:80 -p 443:443 \\`,
` -e OPENFLARE_SERVER_URL=${serverUrl} \\`,
` -e OPENFLARE_AGENT_TOKEN=${agentToken} \\`,
` ghcr.io/rain-kl/openflare-agent:latest`,
].join('\n');
}