slice 8C Phase B.2 + C.1/C.2: retire disk subsystem + rewire disk mgmt to agent

Retired (~12.3k LOC): internal/storage/* (scan/format/attach/migrate/safety),
backup restic/crossdrive/restore_drives/disk_layout/local_infra/restore_scan/
paths + restore_app, report/infra_backup*/infra_pull, setup/scanner,
monitor/watchdog+pinger, web/storage_handlers+handler_restore. Surgically split
backup.Manager to app-data only (DB dumps + volume tars + app restore; dropped
restic + cross-drive + snapshot history). Fixed router/main/web wiring.
Added agent-backed disk API (web/agent_disk_handlers.go): /api/disks list/
assign/eject/format proxying agentapi; data-bearing format refusal -> HTTP 409
'operator authorization required'. report/config_pull.go keeps the setup
fresh-install config download. go build + go test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 13:57:27 +02:00
parent 0294513906
commit abe4e8e619
47 changed files with 404 additions and 12317 deletions
-120
View File
@@ -1,120 +0,0 @@
package monitor
import (
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
)
// Pinger sends health check pings to a Healthchecks.io-compatible server.
type Pinger struct {
baseURL string
httpClient *http.Client
logger *log.Logger
enabled bool
debug bool
}
// NewPinger creates a new Pinger from monitoring config.
func NewPinger(cfg *config.MonitoringConfig, logger *log.Logger) *Pinger {
return &Pinger{
baseURL: strings.TrimRight(cfg.HealthchecksBase, "/"),
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
logger: logger,
enabled: cfg.Enabled,
}
}
// SetDebug enables or disables debug logging for the pinger.
func (p *Pinger) SetDebug(debug bool) {
p.debug = debug
}
// Ping sends a success signal with optional diagnostic body.
func (p *Pinger) Ping(uuid string, body string) error {
if p.debug {
p.logger.Printf("[DEBUG] [pinger] Ping uuid=%s body_len=%d", uuid, len(body))
}
return p.send(uuid, "", body)
}
// Fail sends a failure signal with diagnostic body.
func (p *Pinger) Fail(uuid string, body string) error {
if p.debug {
p.logger.Printf("[DEBUG] [pinger] Fail uuid=%s body=%q", uuid, body)
}
return p.send(uuid, "/fail", body)
}
// Start sends a "job started" signal (for duration tracking).
func (p *Pinger) Start(uuid string) error {
if p.debug {
p.logger.Printf("[DEBUG] [pinger] Start uuid=%s", uuid)
}
return p.send(uuid, "/start", "")
}
func (p *Pinger) send(uuid, suffix, body string) error {
if !p.enabled {
return nil
}
if uuid == "" || strings.HasPrefix(uuid, "CHANGEME") {
return nil
}
url := fmt.Sprintf("%s/ping/%s%s", p.baseURL, uuid, suffix)
if p.debug {
p.logger.Printf("[DEBUG] [pinger] send url=%s", url)
}
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
if attempt > 0 {
if p.debug {
p.logger.Printf("[DEBUG] [pinger] retry attempt=%d uuid=%s", attempt+1, uuid)
}
time.Sleep(2 * time.Second)
}
var bodyReader io.Reader
if body != "" {
bodyReader = strings.NewReader(body)
}
req, err := http.NewRequest(http.MethodPost, url, bodyReader)
if err != nil {
lastErr = err
continue
}
resp, err := p.httpClient.Do(req)
if err != nil {
lastErr = err
continue
}
resp.Body.Close()
if p.debug {
p.logger.Printf("[DEBUG] [pinger] response status=%d uuid=%s", resp.StatusCode, uuid)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
if p.debug {
p.logger.Printf("[DEBUG] [pinger] success uuid=%s", uuid)
}
return nil
}
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
}
p.logger.Printf("[WARN] [monitor] Health ping failed after 3 attempts (%s): %v", uuid, lastErr)
return nil // Never let ping failures affect the caller
}