happyDomain/internal/usecase/checker/check_status_usecase.go
Pierre-Olivier Mercier 9350b71b48 checkers: show worst check status badge on domain list
Add DomainWithCheckStatus model and GetWorstDomainStatuses usecase to
compute the most critical checker status per domain. The GET /domains
endpoint now returns status alongside each domain. The frontend domain
store, list components, and table row display dynamic status badges
with color and icon instead of a hardcoded "OK".

ZoneList is made generic (T extends HappydnsDomain) so the badges
snippet preserves the caller's concrete type without unsafe casts.
2026-04-05 11:58:00 +07:00

370 lines
12 KiB
Go

// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package checker
import (
"encoding/json"
"slices"
checkerPkg "git.happydns.org/happyDomain/internal/checker"
"git.happydns.org/happyDomain/model"
)
// CheckStatusUsecase handles aggregation of checker statuses and evaluation/execution queries.
type CheckStatusUsecase struct {
planStore CheckPlanStorage
evalStore CheckEvaluationStorage
execStore ExecutionStorage
snapStore ObservationSnapshotStorage
plannedProvider PlannedJobProvider
}
// NewCheckStatusUsecase creates a new CheckStatusUsecase.
func NewCheckStatusUsecase(planStore CheckPlanStorage, evalStore CheckEvaluationStorage, execStore ExecutionStorage, snapStore ObservationSnapshotStorage) *CheckStatusUsecase {
return &CheckStatusUsecase{
planStore: planStore,
evalStore: evalStore,
execStore: execStore,
snapStore: snapStore,
}
}
// SetPlannedJobProvider attaches an optional scheduler for planned execution queries.
func (u *CheckStatusUsecase) SetPlannedJobProvider(p PlannedJobProvider) {
u.plannedProvider = p
}
// ListPlannedExecutions returns synthetic Execution records for upcoming scheduled jobs.
// Returns nil if no PlannedJobProvider is configured.
func (u *CheckStatusUsecase) ListPlannedExecutions(checkerID string, target happydns.CheckTarget) []*happydns.Execution {
if u.plannedProvider == nil {
return nil
}
jobs := u.plannedProvider.GetPlannedJobsForChecker(checkerID, target)
result := make([]*happydns.Execution, 0, len(jobs))
for _, job := range jobs {
exec := &happydns.Execution{
CheckerID: job.CheckerID,
PlanID: job.PlanID,
Target: job.Target,
Trigger: happydns.TriggerInfo{Type: happydns.TriggerSchedule},
StartedAt: job.NextRun,
Status: happydns.ExecutionPending,
}
result = append(result, exec)
}
return result
}
// ListCheckerStatuses aggregates checkers, plans, and latest evaluations into a status list.
func (u *CheckStatusUsecase) ListCheckerStatuses(target happydns.CheckTarget) ([]happydns.CheckerStatus, error) {
checkers := checkerPkg.GetCheckers()
plans, err := u.planStore.ListCheckPlansByTarget(target)
if err != nil {
return nil, err
}
planByChecker := make(map[string]*happydns.CheckPlan)
for _, p := range plans {
planByChecker[p.CheckerID] = p
}
var result []happydns.CheckerStatus
for _, def := range checkers {
switch target.Scope() {
case happydns.CheckScopeDomain:
if !def.Availability.ApplyToDomain {
continue
}
case happydns.CheckScopeService:
if !def.Availability.ApplyToService {
continue
}
if len(def.Availability.LimitToServices) > 0 && target.ServiceType != "" {
if !slices.Contains(def.Availability.LimitToServices, target.ServiceType) {
continue
}
}
}
status := happydns.CheckerStatus{
CheckerDefinition: def,
Plan: planByChecker[def.ID],
Enabled: true,
}
enabledRules := make(map[string]bool, len(def.Rules))
for _, rule := range def.Rules {
enabledRules[rule.Name()] = true
}
if status.Plan != nil {
status.Enabled = !status.Plan.IsFullyDisabled()
for ruleName := range enabledRules {
enabledRules[ruleName] = status.Plan.IsRuleEnabled(ruleName)
}
}
status.EnabledRules = enabledRules
execs, err := u.execStore.ListExecutionsByChecker(def.ID, target, 1)
if err == nil && len(execs) > 0 {
status.LatestExecution = execs[0]
}
result = append(result, status)
}
if result == nil {
result = []happydns.CheckerStatus{}
}
return result, nil
}
// GetExecution returns a specific execution by ID.
func (u *CheckStatusUsecase) GetExecution(execID happydns.Identifier) (*happydns.Execution, error) {
return u.execStore.GetExecution(execID)
}
// ListExecutionsByChecker returns executions for a checker on a target, up to limit.
func (u *CheckStatusUsecase) ListExecutionsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]*happydns.Execution, error) {
return u.execStore.ListExecutionsByChecker(checkerID, target, limit)
}
// GetObservationsByExecution returns the observation snapshot for an execution.
func (u *CheckStatusUsecase) GetObservationsByExecution(execID happydns.Identifier) (*happydns.ObservationSnapshot, error) {
exec, err := u.execStore.GetExecution(execID)
if err != nil {
return nil, err
}
if exec.EvaluationID == nil {
return nil, happydns.ErrCheckEvaluationNotFound
}
eval, err := u.evalStore.GetEvaluation(*exec.EvaluationID)
if err != nil {
return nil, err
}
return u.snapStore.GetSnapshot(eval.SnapshotID)
}
// DeleteExecution deletes an execution record by ID.
func (u *CheckStatusUsecase) DeleteExecution(execID happydns.Identifier) error {
return u.execStore.DeleteExecution(execID)
}
// DeleteExecutionsByChecker deletes all executions for a checker on a target.
func (u *CheckStatusUsecase) DeleteExecutionsByChecker(checkerID string, target happydns.CheckTarget) error {
return u.execStore.DeleteExecutionsByChecker(checkerID, target)
}
// GetWorstDomainStatuses fetches all executions for a user and returns the worst
// (most critical) status per domain. It keeps only the latest execution per
// (domain, checker) pair and reports the worst status among them.
func (u *CheckStatusUsecase) GetWorstDomainStatuses(userId happydns.Identifier) (map[string]*happydns.Status, error) {
execs, err := u.execStore.ListExecutionsByUser(userId, 0)
if err != nil {
return nil, err
}
type key struct {
domainId string
checker string
}
latest := map[key]*happydns.Execution{}
for _, exec := range execs {
if exec.Target.DomainId == nil || exec.Status != happydns.ExecutionDone {
continue
}
k := key{domainId: exec.Target.DomainId.String(), checker: exec.CheckerID}
if prev, ok := latest[k]; !ok || exec.StartedAt.After(prev.StartedAt) {
latest[k] = exec
}
}
worst := map[string]*happydns.Status{}
for k, exec := range latest {
s := exec.Result.Status
if s == happydns.StatusUnknown {
continue
}
if prev, ok := worst[k.domainId]; !ok || s > *prev {
worst[k.domainId] = &s
}
}
return worst, nil
}
// GetWorstServiceStatuses returns the worst check status for each service in the zone.
// It iterates all services and all registered checkers, fetching the latest execution
// for each (service, checker) pair, and returns the worst status per service.
func (u *CheckStatusUsecase) GetWorstServiceStatuses(userId happydns.Identifier, domainId happydns.Identifier, zone *happydns.Zone) (map[string]*happydns.Status, error) {
checkers := checkerPkg.GetCheckers()
if len(checkers) == 0 {
return nil, nil
}
result := make(map[string]*happydns.Status)
for subdomain := range zone.Services {
for _, svc := range zone.Services[subdomain] {
target := happydns.CheckTarget{
UserId: &userId,
DomainId: &domainId,
ServiceId: &svc.Id,
}
var worst *happydns.Status
for _, def := range checkers {
execs, err := u.execStore.ListExecutionsByChecker(def.ID, target, 1)
if err != nil || len(execs) == 0 {
continue
}
s := execs[0].Result.Status
if worst == nil || s > *worst {
worst = &s
}
}
if worst != nil {
result[svc.Id.String()] = worst
}
}
}
if len(result) == 0 {
return nil, nil
}
return result, nil
}
// GetResultsByExecution returns the evaluation (with per-rule states) for an execution.
func (u *CheckStatusUsecase) GetResultsByExecution(execID happydns.Identifier) (*happydns.CheckEvaluation, error) {
exec, err := u.execStore.GetExecution(execID)
if err != nil {
return nil, err
}
if exec.EvaluationID == nil {
return nil, happydns.ErrCheckEvaluationNotFound
}
return u.evalStore.GetEvaluation(*exec.EvaluationID)
}
// extractMetricsFromExecution extracts metrics from a single execution's snapshot.
func (u *CheckStatusUsecase) extractMetricsFromExecution(exec *happydns.Execution) ([]happydns.CheckMetric, error) {
if exec.Status != happydns.ExecutionDone || exec.EvaluationID == nil {
return nil, nil
}
eval, err := u.evalStore.GetEvaluation(*exec.EvaluationID)
if err != nil {
return nil, err
}
snap, err := u.snapStore.GetSnapshot(eval.SnapshotID)
if err != nil {
return nil, err
}
return checkerPkg.GetAllMetrics(snap)
}
// extractMetricsFromExecutions extracts metrics from a list of executions.
func (u *CheckStatusUsecase) extractMetricsFromExecutions(execs []*happydns.Execution) ([]happydns.CheckMetric, error) {
var allMetrics []happydns.CheckMetric
for _, exec := range execs {
metrics, err := u.extractMetricsFromExecution(exec)
if err != nil {
continue
}
allMetrics = append(allMetrics, metrics...)
}
return allMetrics, nil
}
// GetMetricsByExecution extracts metrics from a single execution's snapshot.
func (u *CheckStatusUsecase) GetMetricsByExecution(execID happydns.Identifier) ([]happydns.CheckMetric, error) {
exec, err := u.execStore.GetExecution(execID)
if err != nil {
return nil, err
}
return u.extractMetricsFromExecution(exec)
}
// GetMetricsByChecker extracts metrics from recent executions of a checker on a target.
func (u *CheckStatusUsecase) GetMetricsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]happydns.CheckMetric, error) {
execs, err := u.execStore.ListExecutionsByChecker(checkerID, target, limit)
if err != nil {
return nil, err
}
return u.extractMetricsFromExecutions(execs)
}
// GetMetricsByUser extracts metrics from recent executions for a user across all checkers.
func (u *CheckStatusUsecase) GetMetricsByUser(userId happydns.Identifier, limit int) ([]happydns.CheckMetric, error) {
execs, err := u.execStore.ListExecutionsByUser(userId, limit)
if err != nil {
return nil, err
}
return u.extractMetricsFromExecutions(execs)
}
// GetMetricsByDomain extracts metrics from recent executions for a domain (including services).
func (u *CheckStatusUsecase) GetMetricsByDomain(userId happydns.Identifier, domainId happydns.Identifier, limit int) ([]happydns.CheckMetric, error) {
execs, err := u.execStore.ListExecutionsByUser(userId, limit)
if err != nil {
return nil, err
}
// Filter to executions matching this domain (any service under it).
var filtered []*happydns.Execution
for _, exec := range execs {
if exec.Target.DomainId != nil && exec.Target.DomainId.Equals(domainId) {
filtered = append(filtered, exec)
}
}
return u.extractMetricsFromExecutions(filtered)
}
// GetSnapshotByExecution returns the snapshot for a given observation key from an execution.
func (u *CheckStatusUsecase) GetSnapshotByExecution(execID happydns.Identifier, obsKey string) (json.RawMessage, error) {
exec, err := u.execStore.GetExecution(execID)
if err != nil {
return nil, err
}
if exec.EvaluationID == nil {
return nil, happydns.ErrCheckEvaluationNotFound
}
eval, err := u.evalStore.GetEvaluation(*exec.EvaluationID)
if err != nil {
return nil, err
}
snap, err := u.snapStore.GetSnapshot(eval.SnapshotID)
if err != nil {
return nil, err
}
val, ok := snap.Data[obsKey]
if !ok {
return nil, happydns.ErrSnapshotNotFound
}
return json.Marshal(val)
}