Feat v0.6.4 admin health/metrics tab + cluster merge
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m40s
CI/CD / test-sqlite (pull_request) Successful in 2m46s
CI/CD / build-and-deploy (pull_request) Successful in 1m1s

Admin Health tab under Monitoring with auto-refreshing runtime, DB,
cluster, and extension metrics panels. New GET /api/v1/admin/metrics
endpoint. Fattened heartbeat JSONB with stack, GC CPU%, extension
count, sandbox stats, trigger fires. Sandbox runner and trigger engine
now track cumulative execution counters. Event bus tracks publish/deliver
counts. Health endpoints consolidated via shared builder. Block renderers
(mermaid, katex, csv-table, diff-viewer) no longer require chat.
cluster-dashboard package retired — merged into admin Health tab.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 13:06:09 +00:00
parent 3d4228f868
commit 4bc11c2f4e
22 changed files with 917 additions and 281 deletions

View File

@@ -56,6 +56,7 @@ func main() {
}
// ── Server startup ──────────────────────
startTime := time.Now()
cfg := config.Load()
// Structured logging — must be first so all subsequent
@@ -213,6 +214,24 @@ func main() {
HeartbeatInterval: cfg.ClusterHeartbeatInterval,
StaleThreshold: cfg.ClusterStaleThreshold,
})
clusterReg.SetSandboxStats(sandbox.SandboxStats)
clusterReg.SetTriggerFireCount(triggerEngine.FireCount)
clusterReg.SetExtensionCount(func() int {
if stores.Packages == nil {
return 0
}
pkgs, err := stores.Packages.List(context.Background())
if err != nil {
return 0
}
count := 0
for _, p := range pkgs {
if p.Status == "active" {
count++
}
}
return count
})
if err := clusterReg.Start(); err != nil {
log.Printf("⚠ Cluster registry failed to start: %v", err)
clusterReg = nil
@@ -255,7 +274,7 @@ func main() {
}
// Health check (k8s probes hit this directly)
base.GET("/health", func(c *gin.Context) {
buildHealthResponse := func() gin.H {
info := gin.H{
"status": "ok",
"version": Version,
@@ -263,8 +282,15 @@ func main() {
"database_name": database.Name(),
"schema_version": database.SchemaVersion(),
}
if database.IsConnected() {
info["registration_enabled"] = handlers.IsRegistrationEnabled(stores)
}
appendClusterHealth(info, clusterReg, stores)
c.JSON(200, info)
return info
}
base.GET("/health", func(c *gin.Context) {
c.JSON(200, buildHealthResponse())
})
// Liveness: process is alive and serving (no dependency checks).
@@ -352,20 +378,9 @@ func main() {
api := base.Group("/api/v1")
{
// Health (routable through ingress)
// Health (routable through ingress — same shape as /health)
api.GET("/health", func(c *gin.Context) {
info := gin.H{
"status": "ok",
"version": Version,
"schema_version": database.SchemaVersion(),
"database": database.IsConnected(),
"database_name": database.Name(),
}
if database.IsConnected() {
info["registration_enabled"] = handlers.IsRegistrationEnabled(stores)
}
appendClusterHealth(info, clusterReg, stores)
c.JSON(200, info)
c.JSON(200, buildHealthResponse())
})
authGroup := api.Group("/auth")
@@ -794,6 +809,16 @@ func main() {
admin.GET("/cluster", clusterH.ListNodes)
}
// ── Metrics ─────────────────
metricsCollector := metrics.NewCollector(
database.DB, hub, bus, stores,
sandbox.SandboxStats,
triggerEngine.FireCount,
startTime,
)
metricsH := handlers.NewMetricsHandler(metricsCollector)
admin.GET("/metrics", metricsH.GetMetrics)
// ── Backup/Restore ─────────
backupH := handlers.NewBackupHandler(stores, packagesDir, cfg.StoragePath)
admin.POST("/backup", backupH.CreateBackup)