From 594057be06d9d171c18b2a96a1285b5b56d5c607 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 12 Mar 2026 22:49:18 +0800 Subject: [PATCH] feat: enhance apply logs and config versions features - Add detailed diff reporting for OpenResty options in TestPreviewAndDiffConfigVersion. - Update ApplyLogsPage to display additional log details including checksums and support file counts. - Introduce ConfigVersionSnapshotModal for viewing configuration snapshots. - Refactor config versions page to utilize new snapshot modal and improve option diff display. - Extend ApplyLogItem type to include checksums and support file count. - Enhance NodeDetailPage to show target version details and checksums. --- atsf_agent/internal/protocol/agent_api.go | 12 +- atsf_agent/internal/sync/service.go | 33 ++- atsf_server/model/apply_log.go | 16 +- atsf_server/service/agent.go | 37 ++- atsf_server/service/config_version.go | 126 ++++++---- atsf_server/service/https_phase1_test.go | 15 ++ .../apply-logs/components/apply-logs-page.tsx | 230 +++++++++++++----- atsf_server/web/features/apply-logs/types.ts | 4 + .../config-version-snapshot-modal.tsx | 90 +++++++ .../components/config-versions-page.tsx | 138 ++++------- .../web/features/config-versions/types.ts | 7 + .../nodes/components/node-detail-page.tsx | 140 ++++++++++- atsf_server/web/features/nodes/types.ts | 4 + 13 files changed, 618 insertions(+), 234 deletions(-) create mode 100644 atsf_server/web/features/config-versions/components/config-version-snapshot-modal.tsx diff --git a/atsf_agent/internal/protocol/agent_api.go b/atsf_agent/internal/protocol/agent_api.go index 6f8f42bc..24d8d86f 100644 --- a/atsf_agent/internal/protocol/agent_api.go +++ b/atsf_agent/internal/protocol/agent_api.go @@ -49,10 +49,14 @@ type RegisterNodeResponse struct { } type ApplyLogPayload struct { - NodeID string `json:"node_id"` - Version string `json:"version"` - Result string `json:"result"` - Message string `json:"message"` + NodeID string `json:"node_id"` + Version string `json:"version"` + Result string `json:"result"` + Message string `json:"message"` + Checksum string `json:"checksum"` + MainConfigChecksum string `json:"main_config_checksum"` + RouteConfigChecksum string `json:"route_config_checksum"` + SupportFileCount int `json:"support_file_count"` } type ActiveConfigResponse struct { diff --git a/atsf_agent/internal/sync/service.go b/atsf_agent/internal/sync/service.go index a45828b7..08418025 100644 --- a/atsf_agent/internal/sync/service.go +++ b/atsf_agent/internal/sync/service.go @@ -2,6 +2,8 @@ package sync import ( "context" + "crypto/sha256" + "encoding/hex" "log" "atsflare-agent/internal/protocol" @@ -92,6 +94,8 @@ func (s *Service) sync(ctx context.Context, startup bool) error { if routeConfig == "" { routeConfig = config.RenderedConfig } + mainConfigChecksum := checksumString(config.MainConfig) + routeConfigChecksum := checksumString(routeConfig) log.Printf("applying new openresty config: mode=%s from_version=%s to_version=%s old_checksum=%s new_checksum=%s", mode, snapshot.CurrentVersion, config.Version, currentChecksum, config.Checksum) if err = s.nginxManager.Apply(ctx, config.MainConfig, routeConfig, config.SupportFiles); err != nil { log.Printf("apply openresty config failed: mode=%s version=%s error=%v", mode, config.Version, err) @@ -100,10 +104,14 @@ func (s *Service) sync(ctx context.Context, startup bool) error { snapshot.OpenrestyMessage = err.Error() _ = s.stateStore.Save(snapshot) reportErr := s.client.ReportApplyLog(ctx, protocol.ApplyLogPayload{ - NodeID: snapshot.NodeID, - Version: config.Version, - Result: ApplyResultFailed, - Message: err.Error(), + NodeID: snapshot.NodeID, + Version: config.Version, + Result: ApplyResultFailed, + Message: err.Error(), + Checksum: config.Checksum, + MainConfigChecksum: mainConfigChecksum, + RouteConfigChecksum: routeConfigChecksum, + SupportFileCount: len(config.SupportFiles), }) if reportErr != nil { log.Printf("report failed apply log failed: version=%s error=%v", config.Version, reportErr) @@ -122,10 +130,14 @@ func (s *Service) sync(ctx context.Context, startup bool) error { return err } if err = s.client.ReportApplyLog(ctx, protocol.ApplyLogPayload{ - NodeID: snapshot.NodeID, - Version: config.Version, - Result: ApplyResultSuccess, - Message: "apply success", + NodeID: snapshot.NodeID, + Version: config.Version, + Result: ApplyResultSuccess, + Message: "apply success", + Checksum: config.Checksum, + MainConfigChecksum: mainConfigChecksum, + RouteConfigChecksum: routeConfigChecksum, + SupportFileCount: len(config.SupportFiles), }); err != nil { log.Printf("report successful apply log failed: version=%s error=%v", config.Version, err) return err @@ -133,3 +145,8 @@ func (s *Service) sync(ctx context.Context, startup bool) error { log.Printf("successful apply log reported: version=%s", config.Version) return nil } + +func checksumString(content string) string { + sum := sha256.Sum256([]byte(content)) + return hex.EncodeToString(sum[:]) +} diff --git a/atsf_server/model/apply_log.go b/atsf_server/model/apply_log.go index 888af1e0..5a660ab9 100644 --- a/atsf_server/model/apply_log.go +++ b/atsf_server/model/apply_log.go @@ -3,12 +3,16 @@ package model import "time" type ApplyLog struct { - ID uint `json:"id" gorm:"primaryKey"` - NodeID string `json:"node_id" gorm:"index;size:64;not null"` - Version string `json:"version" gorm:"size:32;not null"` - Result string `json:"result" gorm:"size:32;not null"` - Message string `json:"message" gorm:"size:1024"` - CreatedAt time.Time `json:"created_at"` + ID uint `json:"id" gorm:"primaryKey"` + NodeID string `json:"node_id" gorm:"index;size:64;not null"` + Version string `json:"version" gorm:"size:32;not null"` + Result string `json:"result" gorm:"size:32;not null"` + Message string `json:"message" gorm:"size:1024"` + Checksum string `json:"checksum" gorm:"size:64;not null;default:''"` + MainConfigChecksum string `json:"main_config_checksum" gorm:"size:64;not null;default:''"` + RouteConfigChecksum string `json:"route_config_checksum" gorm:"size:64;not null;default:''"` + SupportFileCount int `json:"support_file_count" gorm:"not null;default:0"` + CreatedAt time.Time `json:"created_at"` } func ListApplyLogs(nodeID string) (logs []*ApplyLog, err error) { diff --git a/atsf_server/service/agent.go b/atsf_server/service/agent.go index f1ab3a33..0de02339 100644 --- a/atsf_server/service/agent.go +++ b/atsf_server/service/agent.go @@ -35,10 +35,14 @@ type AgentNodePayload struct { } type ApplyLogPayload struct { - NodeID string `json:"node_id"` - Version string `json:"version"` - Result string `json:"result"` - Message string `json:"message"` + NodeID string `json:"node_id"` + Version string `json:"version"` + Result string `json:"result"` + Message string `json:"message"` + Checksum string `json:"checksum"` + MainConfigChecksum string `json:"main_config_checksum"` + RouteConfigChecksum string `json:"route_config_checksum"` + SupportFileCount int `json:"support_file_count"` } type AgentConfigResponse struct { @@ -88,6 +92,10 @@ type NodeView struct { LastError string `json:"last_error"` LatestApplyResult string `json:"latest_apply_result"` LatestApplyMessage string `json:"latest_apply_message"` + LatestApplyChecksum string `json:"latest_apply_checksum"` + LatestMainConfigChecksum string `json:"latest_main_config_checksum"` + LatestRouteConfigChecksum string `json:"latest_route_config_checksum"` + LatestSupportFileCount int `json:"latest_support_file_count"` LatestApplyAt *time.Time `json:"latest_apply_at"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -161,6 +169,9 @@ func ReportApplyLog(payload ApplyLogPayload) (*model.ApplyLog, error) { payload.Version = strings.TrimSpace(payload.Version) payload.Result = strings.TrimSpace(strings.ToLower(payload.Result)) payload.Message = strings.TrimSpace(payload.Message) + payload.Checksum = strings.TrimSpace(payload.Checksum) + payload.MainConfigChecksum = strings.TrimSpace(payload.MainConfigChecksum) + payload.RouteConfigChecksum = strings.TrimSpace(payload.RouteConfigChecksum) if payload.NodeID == "" { return nil, errors.New("node_id 不能为空") } @@ -173,11 +184,15 @@ func ReportApplyLog(payload ApplyLogPayload) (*model.ApplyLog, error) { common.SysLog("agent apply log received: node_id=" + payload.NodeID + " version=" + payload.Version + " result=" + payload.Result) log := &model.ApplyLog{ - NodeID: payload.NodeID, - Version: payload.Version, - Result: payload.Result, - Message: payload.Message, - CreatedAt: now, + NodeID: payload.NodeID, + Version: payload.Version, + Result: payload.Result, + Message: payload.Message, + Checksum: payload.Checksum, + MainConfigChecksum: payload.MainConfigChecksum, + RouteConfigChecksum: payload.RouteConfigChecksum, + SupportFileCount: payload.SupportFileCount, + CreatedAt: now, } err := model.DB.Transaction(func(tx *gorm.DB) error { node := &model.Node{} @@ -230,6 +245,10 @@ func ListNodeViews() ([]*NodeView, error) { if log, err := model.GetLatestApplyLog(node.NodeID); err == nil { view.LatestApplyResult = log.Result view.LatestApplyMessage = log.Message + view.LatestApplyChecksum = log.Checksum + view.LatestMainConfigChecksum = log.MainConfigChecksum + view.LatestRouteConfigChecksum = log.RouteConfigChecksum + view.LatestSupportFileCount = log.SupportFileCount view.LatestApplyAt = &log.CreatedAt } views = append(views, view) diff --git a/atsf_server/service/config_version.go b/atsf_server/service/config_version.go index 7080a839..8576ac5d 100644 --- a/atsf_server/service/config_version.go +++ b/atsf_server/service/config_version.go @@ -36,12 +36,19 @@ type ConfigPreviewResult struct { } type ConfigDiffResult struct { - ActiveVersion string `json:"active_version,omitempty"` - AddedDomains []string `json:"added_domains"` - RemovedDomains []string `json:"removed_domains"` - ModifiedDomains []string `json:"modified_domains"` - MainConfigChanged bool `json:"main_config_changed"` - ChangedOptionKeys []string `json:"changed_option_keys"` + ActiveVersion string `json:"active_version,omitempty"` + AddedDomains []string `json:"added_domains"` + RemovedDomains []string `json:"removed_domains"` + ModifiedDomains []string `json:"modified_domains"` + MainConfigChanged bool `json:"main_config_changed"` + ChangedOptionKeys []string `json:"changed_option_keys"` + ChangedOptionDetails []ConfigOptionDiffItem `json:"changed_option_details"` +} + +type ConfigOptionDiffItem struct { + Key string `json:"key"` + PreviousValue string `json:"previous_value"` + CurrentValue string `json:"current_value"` } type snapshotRoute struct { @@ -139,10 +146,11 @@ func DiffConfigVersion() (*ConfigDiffResult, error) { return nil, err } result := &ConfigDiffResult{ - AddedDomains: []string{}, - RemovedDomains: []string{}, - ModifiedDomains: []string{}, - ChangedOptionKeys: []string{}, + AddedDomains: []string{}, + RemovedDomains: []string{}, + ModifiedDomains: []string{}, + ChangedOptionKeys: []string{}, + ChangedOptionDetails: []ConfigOptionDiffItem{}, } activeVersion, err := model.GetActiveConfigVersion() if err != nil { @@ -152,6 +160,7 @@ func DiffConfigVersion() (*ConfigDiffResult, error) { } result.MainConfigChanged = true result.ChangedOptionKeys = openRestyOptionKeys() + result.ChangedOptionDetails = buildInitialOpenRestyOptionDiffs(bundle.OpenRestyConfig) return result, nil } return nil, err @@ -185,7 +194,8 @@ func DiffConfigVersion() (*ConfigDiffResult, error) { } } result.MainConfigChanged = activeVersion.MainConfig != bundle.MainConfig - result.ChangedOptionKeys = diffOpenRestyOptionKeys(activeSnapshot.OpenRestyConfig, bundle.OpenRestyConfig) + result.ChangedOptionDetails = diffOpenRestyOptionDetails(activeSnapshot.OpenRestyConfig, bundle.OpenRestyConfig) + result.ChangedOptionKeys = extractOptionDiffKeys(result.ChangedOptionDetails) sort.Strings(result.AddedDomains) sort.Strings(result.RemovedDomains) sort.Strings(result.ModifiedDomains) @@ -426,44 +436,70 @@ func buildOpenRestyConfigSnapshot() openRestyConfigSnapshot { } func diffOpenRestyOptionKeys(left openRestyConfigSnapshot, right openRestyConfigSnapshot) []string { - changes := make([]string, 0) - appendIfChanged := func(key string, changed bool) { - if changed { - changes = append(changes, key) - } + details := diffOpenRestyOptionDetails(left, right) + return extractOptionDiffKeys(details) +} + +func buildInitialOpenRestyOptionDiffs(current openRestyConfigSnapshot) []ConfigOptionDiffItem { + details := diffOpenRestyOptionDetails(openRestyConfigSnapshot{}, current) + for index := range details { + details[index].PreviousValue = "" } - appendIfChanged("OpenRestyWorkerProcesses", left.WorkerProcesses != right.WorkerProcesses) - appendIfChanged("OpenRestyWorkerConnections", left.WorkerConnections != right.WorkerConnections) - appendIfChanged("OpenRestyWorkerRlimitNofile", left.WorkerRlimitNofile != right.WorkerRlimitNofile) - appendIfChanged("OpenRestyEventsUse", left.EventsUse != right.EventsUse) - appendIfChanged("OpenRestyEventsMultiAcceptEnabled", left.EventsMultiAcceptEnabled != right.EventsMultiAcceptEnabled) - appendIfChanged("OpenRestyKeepaliveTimeout", left.KeepaliveTimeout != right.KeepaliveTimeout) - appendIfChanged("OpenRestyKeepaliveRequests", left.KeepaliveRequests != right.KeepaliveRequests) - appendIfChanged("OpenRestyClientHeaderTimeout", left.ClientHeaderTimeout != right.ClientHeaderTimeout) - appendIfChanged("OpenRestyClientBodyTimeout", left.ClientBodyTimeout != right.ClientBodyTimeout) - appendIfChanged("OpenRestySendTimeout", left.SendTimeout != right.SendTimeout) - appendIfChanged("OpenRestyProxyConnectTimeout", left.ProxyConnectTimeout != right.ProxyConnectTimeout) - appendIfChanged("OpenRestyProxySendTimeout", left.ProxySendTimeout != right.ProxySendTimeout) - appendIfChanged("OpenRestyProxyReadTimeout", left.ProxyReadTimeout != right.ProxyReadTimeout) - appendIfChanged("OpenRestyProxyBufferingEnabled", left.ProxyBufferingEnabled != right.ProxyBufferingEnabled) - appendIfChanged("OpenRestyProxyBuffers", left.ProxyBuffers != right.ProxyBuffers) - appendIfChanged("OpenRestyProxyBufferSize", left.ProxyBufferSize != right.ProxyBufferSize) - appendIfChanged("OpenRestyProxyBusyBuffersSize", left.ProxyBusyBuffersSize != right.ProxyBusyBuffersSize) - appendIfChanged("OpenRestyGzipEnabled", left.GzipEnabled != right.GzipEnabled) - appendIfChanged("OpenRestyGzipMinLength", left.GzipMinLength != right.GzipMinLength) - appendIfChanged("OpenRestyGzipCompLevel", left.GzipCompLevel != right.GzipCompLevel) - appendIfChanged("OpenRestyCacheEnabled", left.CacheEnabled != right.CacheEnabled) - appendIfChanged("OpenRestyCachePath", left.CachePath != right.CachePath) - appendIfChanged("OpenRestyCacheLevels", left.CacheLevels != right.CacheLevels) - appendIfChanged("OpenRestyCacheInactive", left.CacheInactive != right.CacheInactive) - appendIfChanged("OpenRestyCacheMaxSize", left.CacheMaxSize != right.CacheMaxSize) - appendIfChanged("OpenRestyCacheKeyTemplate", left.CacheKeyTemplate != right.CacheKeyTemplate) - appendIfChanged("OpenRestyCacheLockEnabled", left.CacheLockEnabled != right.CacheLockEnabled) - appendIfChanged("OpenRestyCacheLockTimeout", left.CacheLockTimeout != right.CacheLockTimeout) - appendIfChanged("OpenRestyCacheUseStale", left.CacheUseStale != right.CacheUseStale) + return details +} + +func diffOpenRestyOptionDetails(left openRestyConfigSnapshot, right openRestyConfigSnapshot) []ConfigOptionDiffItem { + changes := make([]ConfigOptionDiffItem, 0) + appendIfChanged := func(key string, previous string, current string) { + if previous == current { + return + } + changes = append(changes, ConfigOptionDiffItem{ + Key: key, + PreviousValue: previous, + CurrentValue: current, + }) + } + appendIfChanged("OpenRestyWorkerProcesses", left.WorkerProcesses, right.WorkerProcesses) + appendIfChanged("OpenRestyWorkerConnections", fmt.Sprintf("%d", left.WorkerConnections), fmt.Sprintf("%d", right.WorkerConnections)) + appendIfChanged("OpenRestyWorkerRlimitNofile", fmt.Sprintf("%d", left.WorkerRlimitNofile), fmt.Sprintf("%d", right.WorkerRlimitNofile)) + appendIfChanged("OpenRestyEventsUse", left.EventsUse, right.EventsUse) + appendIfChanged("OpenRestyEventsMultiAcceptEnabled", fmt.Sprintf("%t", left.EventsMultiAcceptEnabled), fmt.Sprintf("%t", right.EventsMultiAcceptEnabled)) + appendIfChanged("OpenRestyKeepaliveTimeout", fmt.Sprintf("%d", left.KeepaliveTimeout), fmt.Sprintf("%d", right.KeepaliveTimeout)) + appendIfChanged("OpenRestyKeepaliveRequests", fmt.Sprintf("%d", left.KeepaliveRequests), fmt.Sprintf("%d", right.KeepaliveRequests)) + appendIfChanged("OpenRestyClientHeaderTimeout", fmt.Sprintf("%d", left.ClientHeaderTimeout), fmt.Sprintf("%d", right.ClientHeaderTimeout)) + appendIfChanged("OpenRestyClientBodyTimeout", fmt.Sprintf("%d", left.ClientBodyTimeout), fmt.Sprintf("%d", right.ClientBodyTimeout)) + appendIfChanged("OpenRestySendTimeout", fmt.Sprintf("%d", left.SendTimeout), fmt.Sprintf("%d", right.SendTimeout)) + appendIfChanged("OpenRestyProxyConnectTimeout", fmt.Sprintf("%d", left.ProxyConnectTimeout), fmt.Sprintf("%d", right.ProxyConnectTimeout)) + appendIfChanged("OpenRestyProxySendTimeout", fmt.Sprintf("%d", left.ProxySendTimeout), fmt.Sprintf("%d", right.ProxySendTimeout)) + appendIfChanged("OpenRestyProxyReadTimeout", fmt.Sprintf("%d", left.ProxyReadTimeout), fmt.Sprintf("%d", right.ProxyReadTimeout)) + appendIfChanged("OpenRestyProxyBufferingEnabled", fmt.Sprintf("%t", left.ProxyBufferingEnabled), fmt.Sprintf("%t", right.ProxyBufferingEnabled)) + appendIfChanged("OpenRestyProxyBuffers", left.ProxyBuffers, right.ProxyBuffers) + appendIfChanged("OpenRestyProxyBufferSize", left.ProxyBufferSize, right.ProxyBufferSize) + appendIfChanged("OpenRestyProxyBusyBuffersSize", left.ProxyBusyBuffersSize, right.ProxyBusyBuffersSize) + appendIfChanged("OpenRestyGzipEnabled", fmt.Sprintf("%t", left.GzipEnabled), fmt.Sprintf("%t", right.GzipEnabled)) + appendIfChanged("OpenRestyGzipMinLength", fmt.Sprintf("%d", left.GzipMinLength), fmt.Sprintf("%d", right.GzipMinLength)) + appendIfChanged("OpenRestyGzipCompLevel", fmt.Sprintf("%d", left.GzipCompLevel), fmt.Sprintf("%d", right.GzipCompLevel)) + appendIfChanged("OpenRestyCacheEnabled", fmt.Sprintf("%t", left.CacheEnabled), fmt.Sprintf("%t", right.CacheEnabled)) + appendIfChanged("OpenRestyCachePath", left.CachePath, right.CachePath) + appendIfChanged("OpenRestyCacheLevels", left.CacheLevels, right.CacheLevels) + appendIfChanged("OpenRestyCacheInactive", left.CacheInactive, right.CacheInactive) + appendIfChanged("OpenRestyCacheMaxSize", left.CacheMaxSize, right.CacheMaxSize) + appendIfChanged("OpenRestyCacheKeyTemplate", left.CacheKeyTemplate, right.CacheKeyTemplate) + appendIfChanged("OpenRestyCacheLockEnabled", fmt.Sprintf("%t", left.CacheLockEnabled), fmt.Sprintf("%t", right.CacheLockEnabled)) + appendIfChanged("OpenRestyCacheLockTimeout", left.CacheLockTimeout, right.CacheLockTimeout) + appendIfChanged("OpenRestyCacheUseStale", left.CacheUseStale, right.CacheUseStale) return changes } +func extractOptionDiffKeys(details []ConfigOptionDiffItem) []string { + keys := make([]string, 0, len(details)) + for _, item := range details { + keys = append(keys, item.Key) + } + return keys +} + func openRestyOptionKeys() []string { return []string{ "OpenRestyWorkerProcesses", diff --git a/atsf_server/service/https_phase1_test.go b/atsf_server/service/https_phase1_test.go index 229457da..09576a24 100644 --- a/atsf_server/service/https_phase1_test.go +++ b/atsf_server/service/https_phase1_test.go @@ -221,6 +221,21 @@ func TestPreviewAndDiffConfigVersion(t *testing.T) { if len(diff.ChangedOptionKeys) == 0 || diff.ChangedOptionKeys[0] == "" { t.Fatal("expected changed OpenResty option keys to be reported") } + if len(diff.ChangedOptionDetails) == 0 { + t.Fatal("expected changed OpenResty option details to be reported") + } + found := false + for _, item := range diff.ChangedOptionDetails { + if item.Key == "OpenRestyProxyReadTimeout" { + found = true + if item.PreviousValue != "60" || item.CurrentValue != "120" { + t.Fatalf("unexpected option diff values: %+v", item) + } + } + } + if !found { + t.Fatal("expected OpenRestyProxyReadTimeout diff detail") + } } func TestCreateTLSCertificateRejectsInvalidPEM(t *testing.T) { diff --git a/atsf_server/web/features/apply-logs/components/apply-logs-page.tsx b/atsf_server/web/features/apply-logs/components/apply-logs-page.tsx index cabdd6cc..59ae7a80 100644 --- a/atsf_server/web/features/apply-logs/components/apply-logs-page.tsx +++ b/atsf_server/web/features/apply-logs/components/apply-logs-page.tsx @@ -39,12 +39,26 @@ function buildSummary(logs: ApplyLogItem[]) { return [ { label: '记录总数', value: logs.length }, - { label: '成功', value: logs.filter((item) => item.result === 'success').length }, - { label: '失败', value: logs.filter((item) => item.result !== 'success').length }, + { + label: '成功', + value: logs.filter((item) => item.result === 'success').length, + }, + { + label: '失败', + value: logs.filter((item) => item.result !== 'success').length, + }, { label: '节点数', value: nodeIds.size }, ]; } +function truncateHash(value: string) { + if (!value) { + return '—'; + } + + return value.length > 12 ? `${value.slice(0, 12)}...` : value; +} + export function ApplyLogsPage() { const queryClient = useQueryClient(); const [nodeFilterInput, setNodeFilterInput] = useState(''); @@ -85,62 +99,73 @@ export function ApplyLogsPage() { }; return ( -
+
返回节点页 } /> - {feedback ? : null} + {feedback ? : null} - -
+ +
{summary.map((item) => (
-

{item.label}

-

{item.value}

+

+ {item.label} +

+

+ {item.value} +

))}
-
+
void queryClient.invalidateQueries({ queryKey: applyLogsQueryKey(nodeFilter) })} + type="button" + onClick={() => + void queryClient.invalidateQueries({ + queryKey: applyLogsQueryKey(nodeFilter), + }) + } > 刷新 } > -
-
+
+
setNodeFilterInput(event.target.value)} - placeholder='输入 node_id 过滤应用记录' - className='lg:max-w-md' + placeholder="输入 node_id 过滤应用记录" + className="lg:max-w-md" /> -
- +
+ 筛选 - + 清空
@@ -149,45 +174,69 @@ export function ApplyLogsPage() { {logsQuery.isLoading ? ( ) : logsQuery.isError ? ( - + ) : logs.length === 0 ? ( - + ) : ( -
- +
+
- - - - - - + + + + + + + - + {logs.map((log) => { const resultMeta = getResultMeta(log.result); return ( - - - - + - + + + - + @@ -661,14 +769,35 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { } /> + ))} @@ -679,6 +808,11 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { + setIsTargetSnapshotOpen(false)} + /> + setIsEditorOpen(false)} diff --git a/atsf_server/web/features/nodes/types.ts b/atsf_server/web/features/nodes/types.ts index 1ba03b9a..22c9ebe1 100644 --- a/atsf_server/web/features/nodes/types.ts +++ b/atsf_server/web/features/nodes/types.ts @@ -21,6 +21,10 @@ export interface NodeItem { last_error: string; latest_apply_result: 'success' | '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;
Node ID版本结果时间详情
Node ID版本结果Checksum时间详情
{log.node_id}{log.version} - +
+ {log.node_id} -
+
+ {log.version} + + + + {truncateHash(log.checksum)} + +

{formatDateTime(log.created_at)}

-

{formatRelativeTime(log.created_at)}

+

+ {formatRelativeTime(log.created_at)} +

+ { setSelectedLogId(log.id); - setFeedback(`已选中节点 ${log.node_id} 的版本 ${log.version} 记录。`); + setFeedback( + `已选中节点 ${log.node_id} 的版本 ${log.version} 记录。`, + ); }} - className='px-3 py-2 text-xs' + className="px-3 py-2 text-xs" > 查看详情 @@ -202,39 +251,92 @@ export function ApplyLogsPage() { - + {selectedLog ? ( -
-
+
+
- - + +
-
-
-

创建时间

-

+

+
+

+ 创建时间 +

+

{formatDateTime(selectedLog.created_at)}

-
-

相对时间

-

+

+

+ 相对时间 +

+

{formatRelativeTime(selectedLog.created_at)}

+
+

+ 目标 Checksum +

+

+ {selectedLog.checksum || '无'} +

+
+
+

+ 支持文件数 +

+

+ {selectedLog.support_file_count} +

+
-
-

应用信息

-

+

+
+

+ 主配置摘要 +

+

+ {selectedLog.main_config_checksum || '无'} +

+
+
+

+ 路由配置摘要 +

+

+ {selectedLog.route_config_checksum || '无'} +

+
+
+ +
+

+ 应用信息 +

+

{selectedLog.message || '无附加信息'}

) : ( - + )}
diff --git a/atsf_server/web/features/apply-logs/types.ts b/atsf_server/web/features/apply-logs/types.ts index cbadaf02..82049a3d 100644 --- a/atsf_server/web/features/apply-logs/types.ts +++ b/atsf_server/web/features/apply-logs/types.ts @@ -4,5 +4,9 @@ export interface ApplyLogItem { version: string; result: 'success' | 'failed' | string; message: string; + checksum: string; + main_config_checksum: string; + route_config_checksum: string; + support_file_count: number; created_at: string; } diff --git a/atsf_server/web/features/config-versions/components/config-version-snapshot-modal.tsx b/atsf_server/web/features/config-versions/components/config-version-snapshot-modal.tsx new file mode 100644 index 00000000..bfc0b2b7 --- /dev/null +++ b/atsf_server/web/features/config-versions/components/config-version-snapshot-modal.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { AppModal } from '@/components/ui/app-modal'; +import type { ConfigVersionItem } from '@/features/config-versions/types'; +import { + CodeBlock, + SecondaryButton, +} from '@/features/shared/components/resource-primitives'; +import { formatDateTime } from '@/lib/utils/date'; + +export function ConfigVersionSnapshotModal({ + version, + onClose, +}: { + version: ConfigVersionItem | null; + onClose: () => void; +}) { + return ( + + + 关闭 + +
+ } + > + {version ? ( +
+
+
+

+ Checksum +

+

+ {version.checksum} +

+
+
+

+ 创建人 +

+

+ {version.created_by || '系统'} +

+
+
+

+ 创建时间 +

+

+ {formatDateTime(version.created_at)} +

+
+
+ +
+

+ 快照 JSON +

+ + {version.snapshot_json} + +
+
+

+ 主配置 +

+ + {version.main_config} + +
+
+

+ 路由配置 +

+ + {version.rendered_config} + +
+
+ ) : null} + + ); +} diff --git a/atsf_server/web/features/config-versions/components/config-versions-page.tsx b/atsf_server/web/features/config-versions/components/config-versions-page.tsx index 6ab355ff..45b8a16b 100644 --- a/atsf_server/web/features/config-versions/components/config-versions-page.tsx +++ b/atsf_server/web/features/config-versions/components/config-versions-page.tsx @@ -8,7 +8,6 @@ import { ErrorState } from '@/components/feedback/error-state'; import { InlineMessage } from '@/components/feedback/inline-message'; import { LoadingState } from '@/components/feedback/loading-state'; import { PageHeader } from '@/components/layout/page-header'; -import { AppModal } from '@/components/ui/app-modal'; import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; import { @@ -18,7 +17,9 @@ import { getConfigVersions, publishConfigVersion, } from '@/features/config-versions/api/config-versions'; +import { ConfigVersionSnapshotModal } from '@/features/config-versions/components/config-version-snapshot-modal'; import type { + ConfigOptionDiffItem, ConfigDiffResult, ConfigPreviewResult, ConfigVersionItem, @@ -121,84 +122,46 @@ function SupportFilesList({ files }: { files: SupportFile[] }) { ); } -function SnapshotModal({ - version, - onClose, -}: { - version: ConfigVersionItem | null; - onClose: () => void; -}) { - return ( - - - 关闭 - -
- } - > - {version ? ( -
-
-
-

- Checksum -

-

- {version.checksum} -

-
-
-

- 创建人 -

-

- {version.created_by || '系统'} -

-
-
-

- 创建时间 -

-

- {formatDateTime(version.created_at)} -

-
-
+function renderOptionValue(value: string) { + return value === '' ? '空' : value; +} -
-

- 快照 JSON -

- - {version.snapshot_json} - -
-
-

- 主配置 -

- - {version.main_config} - -
-
-

- 路由配置 -

- - {version.rendered_config} - -
-
- ) : null} - +function OptionDiffTable({ items }: { items: ConfigOptionDiffItem[] }) { + if (items.length === 0) { + return ( +

+ 当前无 OpenResty 性能参数变化。 +

+ ); + } + + return ( +
+ + + + + + + + + + {items.map((item) => ( + + + + + + ))} + +
参数当前激活值待发布值
+ {item.key} + + {renderOptionValue(item.previous_value)} + + {renderOptionValue(item.current_value)} +
+
); } @@ -295,22 +258,7 @@ function PublishPreviewCard({ variant={diff.changed_option_keys.length > 0 ? 'info' : 'warning'} />
- {diff.changed_option_keys.length > 0 ? ( -
- {diff.changed_option_keys.map((item) => ( - - {item} - - ))} -
- ) : ( -

- 当前无 OpenResty 性能参数变化。 -

- )} +
@@ -579,7 +527,7 @@ export function ConfigVersionsPage() {
- setSelectedVersionId(null)} /> diff --git a/atsf_server/web/features/config-versions/types.ts b/atsf_server/web/features/config-versions/types.ts index 73b5c902..cc7c3306 100644 --- a/atsf_server/web/features/config-versions/types.ts +++ b/atsf_server/web/features/config-versions/types.ts @@ -33,4 +33,11 @@ export interface ConfigDiffResult { modified_domains: string[]; main_config_changed: boolean; changed_option_keys: string[]; + changed_option_details: ConfigOptionDiffItem[]; +} + +export interface ConfigOptionDiffItem { + key: string; + previous_value: string; + current_value: string; } diff --git a/atsf_server/web/features/nodes/components/node-detail-page.tsx b/atsf_server/web/features/nodes/components/node-detail-page.tsx index 2ad3046c..7f8f6862 100644 --- a/atsf_server/web/features/nodes/components/node-detail-page.tsx +++ b/atsf_server/web/features/nodes/components/node-detail-page.tsx @@ -16,6 +16,9 @@ import { PageHeader } from '@/components/layout/page-header'; import { AppModal } from '@/components/ui/app-modal'; import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; +import { getConfigVersions } from '@/features/config-versions/api/config-versions'; +import { ConfigVersionSnapshotModal } from '@/features/config-versions/components/config-version-snapshot-modal'; +import type { ConfigVersionItem } from '@/features/config-versions/types'; import { getApplyLogs } from '@/features/apply-logs/api/apply-logs'; import { deleteNode, @@ -97,6 +100,7 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { const [feedback, setFeedback] = useState(null); const [isEditorOpen, setIsEditorOpen] = useState(false); const [isAgentUpdateModalOpen, setIsAgentUpdateModalOpen] = useState(false); + const [isTargetSnapshotOpen, setIsTargetSnapshotOpen] = useState(false); const [selectedReleaseChannel, setSelectedReleaseChannel] = useState('stable'); const [agentUpdateFeedback, setAgentUpdateFeedback] = @@ -144,6 +148,12 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { refetchInterval: 5000, }); + const configVersionsQuery = useQuery({ + queryKey: ['config-versions'], + queryFn: getConfigVersions, + refetchInterval: 5000, + }); + useEffect(() => { if (typeof window !== 'undefined' && !serverUrl) { setServerUrl(window.location.origin); @@ -279,6 +289,12 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { } }; + const activeConfigVersion = useMemo(() => { + return ( + (configVersionsQuery.data ?? []).find((item) => item.is_active) ?? null + ); + }, [configVersionsQuery.data]); + if (nodesQuery.isLoading) { return ; } @@ -320,6 +336,9 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { ? previewAgentReleaseQuery.isFetching : stableAgentReleaseQuery.isFetching; const applyLogs = applyLogsQuery.data ?? []; + const isTargetVersionApplied = + activeConfigVersion !== null && + activeConfigVersion.version === node.current_version; const handleOpenAgentUpdateModal = () => { setAgentUpdateFeedback(null); @@ -353,6 +372,9 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { queryClient.invalidateQueries({ queryKey: ['apply-logs', node.node_id], }), + queryClient.invalidateQueries({ + queryKey: ['config-versions'], + }), ]); }; @@ -460,10 +482,95 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { )} · ${formatDateTime(node.latest_apply_at)}` : '暂无应用记录'}

+ {node.latest_apply_checksum ? ( +
+

目标 Checksum:{node.latest_apply_checksum}

+

支持文件:{node.latest_support_file_count}

+
+ ) : null}
+ setIsTargetSnapshotOpen(true)} + > + 查看目标快照 + + ) : null + } + > + {configVersionsQuery.isLoading ? ( + + ) : configVersionsQuery.isError ? ( + + ) : activeConfigVersion ? ( +
+
+

+ 追平状态 +

+
+ +
+

+ {isTargetVersionApplied + ? '当前节点已应用全局激活配置。' + : '当前节点版本落后于全局激活配置,可结合应用记录定位原因。'} +

+
+ +
+
+

+ 目标版本 +

+

+ {activeConfigVersion.version} +

+
+
+

+ Target Checksum +

+

+ {activeConfigVersion.checksum} +

+
+
+

+ 激活时间 +

+

+ {formatDateTime(activeConfigVersion.created_at)} +

+
+
+
+ ) : ( + + )} +
+
版本 结果Checksum 时间 消息
+ {log.checksum ? `${log.checksum.slice(0, 12)}...` : '—'} + {formatRelativeTime(log.created_at)} ·{' '} {formatDateTime(log.created_at)} -

- {log.message || '—'} -

+
+

+ {log.main_config_checksum + ? `主配置:${log.main_config_checksum}` + : ''} +

+

+ {log.route_config_checksum + ? `路由配置:${log.route_config_checksum}` + : ''} +

+

+ {log.support_file_count + ? `支持文件:${log.support_file_count}` + : ''} +

+

{log.message || '—'}

+