mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 01:36:37 +08:00
[优化] ui优化
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"server_url": "http://127.0.0.1:3000",
|
"server_url": "http://127.0.0.1:3000",
|
||||||
"agent_token": "123",
|
"agent_token": "2bae8c0eef58c3d7cf84464d983052ca",
|
||||||
"data_dir": "./data",
|
"data_dir": "./data",
|
||||||
"openresty_container_name": "atsflare-openresty",
|
"openresty_container_name": "atsflare-openresty",
|
||||||
"openresty_docker_image": "openresty/openresty:alpine",
|
"openresty_docker_image": "openresty/openresty:alpine",
|
||||||
|
|||||||
@@ -113,12 +113,9 @@ func (r *Runner) Run(ctx context.Context) error {
|
|||||||
if !r.hasAgentToken() {
|
if !r.hasAgentToken() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("agent sync tick: node_id=%s", nodeID)
|
|
||||||
if err = r.SyncService.SyncOnce(ctx); err != nil {
|
if err = r.SyncService.SyncOnce(ctx); err != nil {
|
||||||
r.recordSyncError(err)
|
r.recordSyncError(err)
|
||||||
log.Printf("agent sync failed: %v", err)
|
log.Printf("agent sync failed: %v", err)
|
||||||
} else {
|
|
||||||
log.Printf("agent sync completed")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ func (c *Client) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetActiveConfig(ctx context.Context) (*protocol.ActiveConfigResponse, error) {
|
func (c *Client) GetActiveConfig(ctx context.Context) (*protocol.ActiveConfigResponse, error) {
|
||||||
log.Printf("http get active config request")
|
|
||||||
resp := protocol.APIResponse[protocol.ActiveConfigResponse]{}
|
resp := protocol.APIResponse[protocol.ActiveConfigResponse]{}
|
||||||
if err := c.getJSON(ctx, "/api/agent/config-versions/active", &resp); err != nil {
|
if err := c.getJSON(ctx, "/api/agent/config-versions/active", &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -100,9 +99,6 @@ func (c *Client) postJSON(ctx context.Context, path string, body any, target any
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) do(req *http.Request, target any) error {
|
func (c *Client) do(req *http.Request, target any) error {
|
||||||
if !isHeartbeatRequest(req) {
|
|
||||||
log.Printf("http request start: method=%s path=%s", req.Method, req.URL.Path)
|
|
||||||
}
|
|
||||||
res, err := c.httpClient.Do(req)
|
res, err := c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("http request failed: method=%s path=%s error=%v", req.Method, req.URL.Path, err)
|
log.Printf("http request failed: method=%s path=%s error=%v", req.Method, req.URL.Path, err)
|
||||||
@@ -123,24 +119,11 @@ func (c *Client) do(req *http.Request, target any) error {
|
|||||||
log.Printf("http api response failed: method=%s path=%s message=%s", req.Method, req.URL.Path, wrapper.Message)
|
log.Printf("http api response failed: method=%s path=%s message=%s", req.Method, req.URL.Path, wrapper.Message)
|
||||||
return errors.New(wrapper.Message)
|
return errors.New(wrapper.Message)
|
||||||
}
|
}
|
||||||
if !isHeartbeatRequest(req) {
|
|
||||||
log.Printf("http request succeeded: method=%s path=%s", req.Method, req.URL.Path)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err = json.NewDecoder(res.Body).Decode(target); err != nil {
|
if err = json.NewDecoder(res.Body).Decode(target); err != nil {
|
||||||
log.Printf("http response decode failed: method=%s path=%s error=%v", req.Method, req.URL.Path, err)
|
log.Printf("http response decode failed: method=%s path=%s error=%v", req.Method, req.URL.Path, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !isHeartbeatRequest(req) {
|
|
||||||
log.Printf("http request succeeded: method=%s path=%s", req.Method, req.URL.Path)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isHeartbeatRequest(req *http.Request) bool {
|
|
||||||
if req == nil || req.URL == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return req.Method == http.MethodPost && req.URL.Path == "/api/agent/nodes/heartbeat"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ func (m *Manager) backup() (*backupState, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
state.Files = files
|
state.Files = files
|
||||||
log.Printf("nginx backup captured: route_exists=%t support_files=%d", state.RouteExisted, len(state.Files))
|
log.Printf("backup captured: route_exists=%t support_files=%d", state.RouteExisted, len(state.Files))
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,7 +450,6 @@ func (m *Manager) restore(state *backupState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("nginx backup restored")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +457,6 @@ func (m *Manager) writeSupportFiles(supportFiles []protocol.SupportFile) error {
|
|||||||
if m.CertDir == "" {
|
if m.CertDir == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Printf("writing nginx support files: cert_dir=%s count=%d", m.CertDir, len(supportFiles))
|
|
||||||
if err := os.RemoveAll(m.CertDir); err != nil && !os.IsNotExist(err) {
|
if err := os.RemoveAll(m.CertDir); err != nil && !os.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -474,7 +472,6 @@ func (m *Manager) writeSupportFiles(supportFiles []protocol.SupportFile) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("nginx support files written: cert_dir=%s count=%d", m.CertDir, len(supportFiles))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ func (s *Service) sync(ctx context.Context, startup bool) error {
|
|||||||
if startup {
|
if startup {
|
||||||
mode = "startup"
|
mode = "startup"
|
||||||
}
|
}
|
||||||
log.Printf("sync started: mode=%s", mode)
|
|
||||||
snapshot, err := s.stateStore.Load()
|
snapshot, err := s.stateStore.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -61,12 +60,10 @@ func (s *Service) sync(ctx context.Context, startup bool) error {
|
|||||||
log.Printf("fetch active config failed: mode=%s error=%v", mode, err)
|
log.Printf("fetch active config failed: mode=%s error=%v", mode, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("active config fetched: mode=%s version=%s checksum=%s support_files=%d", mode, config.Version, config.Checksum, len(config.SupportFiles))
|
|
||||||
currentChecksum, err := s.nginxManager.CurrentChecksum()
|
currentChecksum, err := s.nginxManager.CurrentChecksum()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("current local checksum loaded: mode=%s checksum=%s", mode, currentChecksum)
|
|
||||||
if currentChecksum == config.Checksum {
|
if currentChecksum == config.Checksum {
|
||||||
log.Printf("local openresty config already up to date: mode=%s version=%s", mode, config.Version)
|
log.Printf("local openresty config already up to date: mode=%s version=%s", mode, config.Version)
|
||||||
if startup {
|
if startup {
|
||||||
|
|||||||
@@ -1589,8 +1589,6 @@ var SwaggerInfo = &swag.Spec{
|
|||||||
Description: "ATSFlare Server 管理端与 Agent API 文档。",
|
Description: "ATSFlare Server 管理端与 Agent API 文档。",
|
||||||
InfoInstanceName: "swagger",
|
InfoInstanceName: "swagger",
|
||||||
SwaggerTemplate: docTemplate,
|
SwaggerTemplate: docTemplate,
|
||||||
LeftDelim: "{{",
|
|
||||||
RightDelim: "}}",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -344,19 +344,31 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
updateAgentMutation.mutate(selectedAgentRelease ?? null);
|
updateAgentMutation.mutate(selectedAgentRelease ?? null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRefreshing = nodesQuery.isFetching || applyLogsQuery.isFetching;
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setFeedback(null);
|
||||||
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({ queryKey: nodesQueryKey }),
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['apply-logs', node.node_id],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={node.name}
|
title={node.name}
|
||||||
description="节点详情每 5 秒自动刷新一次。部署命令、Node ID、Token、更新模式和最近应用记录统一在这里查看。"
|
description="节点详情"
|
||||||
action={
|
action={
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
href="/node"
|
href="/node"
|
||||||
className="inline-flex items-center justify-center rounded-2xl border border-[var(--border-default)] bg-[var(--control-background)] px-4 py-3 text-sm font-medium text-[var(--foreground-primary)] transition hover:bg-[var(--control-background-hover)]"
|
className="inline-flex items-center justify-center rounded-2xl border border-[var(--border-default)] bg-[var(--control-background)] px-4 py-3 text-sm font-medium text-[var(--foreground-primary)] transition hover:bg-[var(--control-background-hover)]"
|
||||||
>
|
>
|
||||||
返回列表
|
返回
|
||||||
</Link>
|
</Link>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
type="button"
|
type="button"
|
||||||
@@ -364,30 +376,26 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
>
|
>
|
||||||
编辑节点
|
编辑节点
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => void handleRefresh()}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
>
|
||||||
|
{isRefreshing ? '刷新中...' : '刷新'}
|
||||||
|
</SecondaryButton>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleOpenAgentUpdateModal}
|
onClick={handleOpenAgentUpdateModal}
|
||||||
disabled={updateAgentMutation.isPending}
|
disabled={updateAgentMutation.isPending}
|
||||||
>
|
>
|
||||||
{node.update_requested ? '查看 Agent 更新' : 'Agent 更新'}
|
{node.update_requested ? '查看升级' : '升级'}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
<SecondaryButton
|
|
||||||
type="button"
|
|
||||||
onClick={handleRestartOpenresty}
|
|
||||||
disabled={restartOpenrestyMutation.isPending}
|
|
||||||
>
|
|
||||||
{restartOpenrestyMutation.isPending
|
|
||||||
? '下发重启中...'
|
|
||||||
: node.restart_openresty_requested
|
|
||||||
? '等待 OpenResty 重启'
|
|
||||||
: '重启 OpenResty'}
|
|
||||||
</SecondaryButton>
|
|
||||||
<DangerButton
|
<DangerButton
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={deleteMutation.isPending}
|
disabled={deleteMutation.isPending}
|
||||||
>
|
>
|
||||||
删除节点
|
删除
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -423,20 +431,6 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
|
||||||
<AppCard title="OpenResty 健康">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<StatusBadge
|
|
||||||
label={getOpenrestyStatusLabel(node.openresty_status)}
|
|
||||||
variant={getOpenrestyStatusVariant(node.openresty_status)}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-[var(--foreground-secondary)]">
|
|
||||||
{node.restart_openresty_requested
|
|
||||||
? '已等待节点在下一次心跳后执行 OpenResty 重启。'
|
|
||||||
: node.openresty_message || '当前未上报额外错误。'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</AppCard>
|
|
||||||
|
|
||||||
<AppCard title="更新模式">
|
<AppCard title="更新模式">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
@@ -470,10 +464,60 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
</AppCard>
|
</AppCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AppCard
|
||||||
|
title="OpenResty 健康与控制"
|
||||||
|
description="OpenResty 当前健康状态。"
|
||||||
|
action={
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<PrimaryButton
|
||||||
|
type="button"
|
||||||
|
onClick={handleRestartOpenresty}
|
||||||
|
disabled={restartOpenrestyMutation.isPending}
|
||||||
|
>
|
||||||
|
{restartOpenrestyMutation.isPending
|
||||||
|
? '下发重启中...'
|
||||||
|
: node.restart_openresty_requested
|
||||||
|
? '等待 OpenResty 重启'
|
||||||
|
: '重启 OpenResty'}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 lg:grid-cols-[220px_minmax(0,1fr)]">
|
||||||
|
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
|
||||||
|
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
|
||||||
|
健康状态
|
||||||
|
</p>
|
||||||
|
<div className="mt-3 flex flex-wrap items-center gap-3">
|
||||||
|
<StatusBadge
|
||||||
|
label={getOpenrestyStatusLabel(node.openresty_status)}
|
||||||
|
variant={getOpenrestyStatusVariant(node.openresty_status)}
|
||||||
|
/>
|
||||||
|
{node.restart_openresty_requested ? (
|
||||||
|
<StatusBadge label="等待重启执行" variant="warning" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-sm text-[var(--foreground-secondary)]">
|
||||||
|
{node.restart_openresty_requested
|
||||||
|
? '已等待节点在下一次心跳后执行 OpenResty 重启。'
|
||||||
|
: '系统会在每次心跳前自动采集健康状态。'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
|
||||||
|
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
|
||||||
|
状态消息
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-sm leading-6 break-words whitespace-pre-wrap text-[var(--foreground-secondary)]">
|
||||||
|
{node.openresty_message || '当前未上报额外错误。'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppCard>
|
||||||
|
|
||||||
<div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
|
<div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
|
||||||
<AppCard
|
<AppCard
|
||||||
title="节点标识与部署"
|
title="节点标识与部署"
|
||||||
description="Node ID、Token 与部署命令不再在列表中展示,统一收敛到详情页。"
|
|
||||||
action={
|
action={
|
||||||
nodeInstallCommand ? (
|
nodeInstallCommand ? (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
@@ -638,7 +682,7 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
<AppModal
|
<AppModal
|
||||||
isOpen={isEditorOpen}
|
isOpen={isEditorOpen}
|
||||||
onClose={() => setIsEditorOpen(false)}
|
onClose={() => setIsEditorOpen(false)}
|
||||||
title="编辑节点"
|
title="编辑"
|
||||||
description="更新模式和节点名都在详情页维护。"
|
description="更新模式和节点名都在详情页维护。"
|
||||||
footer={
|
footer={
|
||||||
<div className="flex flex-wrap justify-end gap-3">
|
<div className="flex flex-wrap justify-end gap-3">
|
||||||
|
|||||||
@@ -279,9 +279,6 @@ export function NodesPage() {
|
|||||||
node.openresty_status,
|
node.openresty_status,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<p className="max-w-64 text-xs break-words whitespace-pre-wrap text-[var(--foreground-secondary)]">
|
|
||||||
{node.openresty_message || '无额外错误'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4 text-[var(--foreground-secondary)]">
|
<td className="px-3 py-4 text-[var(--foreground-secondary)]">
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ export function getUpdateMode(node: NodeItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.auto_update_enabled) {
|
if (node.auto_update_enabled) {
|
||||||
return { label: '自动更新', variant: 'success' as const };
|
return { label: '自动', variant: 'success' as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { label: '手动更新', variant: 'info' as const };
|
return { label: '手动', variant: 'info' as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOpenrestyStatusVariant(status: NodeItem['openresty_status']) {
|
export function getOpenrestyStatusVariant(status: NodeItem['openresty_status']) {
|
||||||
|
|||||||
Reference in New Issue
Block a user