Files
kandev/apps/backend/internal/system/system.go
T
José Almeida 041a19f060 feat: service-managed UI self-update (#1210)
* feat: add service-managed self-update

* chore: add temporary self-update test scripts

* chore: simplify temporary script markdown fences

* chore: run frontend in temporary self-update scripts

* fix: show self-update job progress after apply

* chore: add real npm self-update test script

* fix: isolate npm self-update prefix

* fix: detect npm service installs from bin shim

* chore: reset service before real npm update test

* chore: expand real npm status diagnostics

* fix: preserve service home during self-update

* fix(cli): tee self-update helper output to a log file

* fix(cli): avoid launchd bootstrap race when reloading the macOS service

* fix(backend): run macOS self-update helper once via transient launchd plist

* fix(web): show self-update progress and lock the apply button until the update lands

* fix(cli): reinstall via the kandev wrapper on Homebrew self-update

* chore: remove temp self-update test scripts

* fix(web): drop unused apply path from useUpdates hook

* fix(backend): snapshot install state once in apply preflight

* docs: align self-update docs with transient LaunchAgent flow

* chore(release): run cli bundle from apps workspace root

* fix(backend): guard against concurrent self-update applies

* fix(backend): check scheme and helper paths in self-update apply

* fix(backend): drop duplicate binary name from npx manual command

* fix(cli): allow --help for service self-update without --intent

* fix(cli): harden self-update dir perms and darwin uid resolution

* fix(web): set required POST fields last in applyUpdate

* docs: use JSON body notation for the apply endpoint

* fix(backend): expire self-update guard so a stuck helper cannot wedge apply

* refactor(cli): collapse redundant writeSync branch in self-update log
2026-06-01 17:49:15 +01:00

135 lines
4.7 KiB
Go

// Package system composes the System-pages backend: a top-level
// Provide that constructs the info, disk, database, backups, logs,
// updates, and jobs sub-services and registers the corresponding
// HTTP route group under /api/v1/system.
//
// The existing internal/health package continues to own
// GET /api/v1/system/health independently — this composer does not
// replace it. The wiring layer (cmd/kandev) registers health alongside
// this package.
package system
import (
"context"
"path/filepath"
"github.com/gin-gonic/gin"
"github.com/kandev/kandev/internal/common/config"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/db"
"github.com/kandev/kandev/internal/events/bus"
"github.com/kandev/kandev/internal/system/backups"
"github.com/kandev/kandev/internal/system/database"
"github.com/kandev/kandev/internal/system/disk"
"github.com/kandev/kandev/internal/system/info"
"github.com/kandev/kandev/internal/system/jobs"
"github.com/kandev/kandev/internal/system/logs"
"github.com/kandev/kandev/internal/system/updates"
)
// BuildInfo holds the ldflag-injected build metadata that cmd/kandev
// passes to Provide.
type BuildInfo struct {
Version string
Commit string
BuildTime string
}
// Wiring holds the runtime hooks the destructive endpoints need —
// currently only OrchestratorShutdown, which stops in-flight agent
// executions before factory reset wipes their backing data. The frontend
// dialog prompts the user to relaunch after a successful reset/restore;
// no in-process re-exec is performed.
type Wiring struct {
OrchestratorShutdown func()
}
// Service exposes the composed system sub-services. Each field is
// addressable so the cmd/kandev wiring can attach callbacks (Restart)
// after construction.
type Service struct {
Info *info.Service
Jobs *jobs.Tracker
Disk *disk.Service
Database *database.Service
Backups *backups.Service
Logs *logs.Service
Updates *updates.Service
}
// Provide constructs the composed Service. The HTTP routes are
// registered separately via RegisterRoutes and the updates poller is
// started via StartBackground, so callers can opt out (in tests, in
// CLI subcommands).
func Provide(cfg *config.Config, log *logger.Logger, pool *db.Pool, eventBus bus.EventBus, build BuildInfo, wiring Wiring) *Service {
tracker := jobs.NewTracker(eventBus, log)
dataDir := cfg.ResolvedDataDir()
homeDir := cfg.ResolvedHomeDir()
resetDirs := database.ResetDirs{
Worktrees: filepath.Join(homeDir, "worktrees"),
Repos: filepath.Join(homeDir, "repos"),
Sessions: filepath.Join(homeDir, "sessions"),
Tasks: filepath.Join(homeDir, "tasks"),
QuickChat: filepath.Join(homeDir, "quick-chat"),
}
dbSvc := database.NewService(pool, dataDir, resetDirs, tracker, log)
dbSvc.OrchestratorShutdown = wiring.OrchestratorShutdown
backupsSvc := backups.NewService(dataDir, pool, tracker, log)
logDir := log.LogDirectory()
logFile := log.LogFilename()
return &Service{
Info: info.NewService(build.Version, build.Commit, build.BuildTime),
Jobs: tracker,
Disk: disk.NewService(homeDir, tracker, log),
Database: dbSvc,
Backups: backupsSvc,
Logs: logs.NewService(logDir, logFile, log),
Updates: updates.NewService(pool, build.Version, nil, log, updates.WithHomeDir(homeDir), updates.WithJobs(tracker)),
}
}
// RegisterRoutes mounts every system endpoint under /api/v1/system.
// The health endpoint is mounted by internal/health.RegisterRoutes,
// not here, to keep the existing package's surface unchanged.
func (s *Service) RegisterRoutes(router *gin.Engine, log *logger.Logger) {
g := router.Group("/api/v1/system")
g.GET("/info", info.Handler(s.Info))
g.GET("/disk-usage", disk.HandleGet(s.Disk))
g.POST("/disk-usage/refresh", disk.HandleRefresh(s.Disk))
g.POST("/disk-usage/open", disk.HandleOpenFolder(s.Disk))
g.GET("/database", database.HandleStats(s.Database))
g.POST("/database/vacuum", database.HandleVacuum(s.Database))
g.POST("/database/optimize", database.HandleOptimize(s.Database))
g.POST("/database/reset", database.HandleReset(s.Database))
backups.RegisterRoutes(g, s.Backups)
g.GET("/logs", logs.HandleList(s.Logs))
g.GET("/logs/tail", logs.HandleTail(s.Logs))
g.GET("/logs/:name/download", logs.HandleDownload(s.Logs))
g.GET("/updates", updates.HandleGet(s.Updates))
g.POST("/updates/check", updates.HandleCheck(s.Updates))
g.POST("/updates/apply", updates.HandleApply(s.Updates))
g.GET("/jobs/:id", jobs.HandleGet(s.Jobs))
log.Debug("Registered System routes (HTTP)")
}
// StartBackground kicks off the updates poller goroutine. The poller
// stops when ctx is cancelled.
func (s *Service) StartBackground(ctx context.Context) {
if s.Updates != nil {
s.Updates.StartPoller(ctx)
}
}