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.
This commit is contained in:
parent
e67ee23805
commit
9350b71b48
14 changed files with 141 additions and 53 deletions
|
|
@ -74,7 +74,7 @@ func NewDomainController(
|
|||
func (dc *DomainController) ListDomains(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
if user != nil {
|
||||
apidc := controller.NewDomainController(dc.domainService, dc.remoteZoneImporter, dc.zoneImporter)
|
||||
apidc := controller.NewDomainController(dc.domainService, dc.remoteZoneImporter, dc.zoneImporter, nil)
|
||||
apidc.GetDomains(c)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import (
|
|||
"github.com/miekg/dns"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/api/middleware"
|
||||
checkerUC "git.happydns.org/happyDomain/internal/usecase/checker"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
|
|
@ -37,13 +38,15 @@ type DomainController struct {
|
|||
domainService happydns.DomainUsecase
|
||||
remoteZoneImporter happydns.RemoteZoneImporterUsecase
|
||||
zoneImporter happydns.ZoneImporterUsecase
|
||||
checkStatusUC *checkerUC.CheckStatusUsecase
|
||||
}
|
||||
|
||||
func NewDomainController(domainService happydns.DomainUsecase, remoteZoneImporter happydns.RemoteZoneImporterUsecase, zoneImporter happydns.ZoneImporterUsecase) *DomainController {
|
||||
func NewDomainController(domainService happydns.DomainUsecase, remoteZoneImporter happydns.RemoteZoneImporterUsecase, zoneImporter happydns.ZoneImporterUsecase, checkStatusUC *checkerUC.CheckStatusUsecase) *DomainController {
|
||||
return &DomainController{
|
||||
domainService: domainService,
|
||||
remoteZoneImporter: remoteZoneImporter,
|
||||
zoneImporter: zoneImporter,
|
||||
checkStatusUC: checkStatusUC,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +59,7 @@ func NewDomainController(domainService happydns.DomainUsecase, remoteZoneImporte
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {array} happydns.Domain
|
||||
// @Success 200 {array} happydns.DomainWithCheckStatus
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Failure 404 {object} happydns.ErrorResponse "Unable to retrieve user's domains"
|
||||
// @Router /domains [get]
|
||||
|
|
@ -73,7 +76,25 @@ func (dc *DomainController) GetDomains(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, domains)
|
||||
var statusByDomain map[string]*happydns.Status
|
||||
if dc.checkStatusUC != nil {
|
||||
var err error
|
||||
statusByDomain, err = dc.checkStatusUC.GetWorstDomainStatuses(user.Id)
|
||||
if err != nil {
|
||||
log.Printf("GetWorstDomainStatuses: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]*happydns.DomainWithCheckStatus, 0, len(domains))
|
||||
for _, d := range domains {
|
||||
entry := &happydns.DomainWithCheckStatus{Domain: d}
|
||||
if statusByDomain != nil {
|
||||
entry.LastCheckStatus = statusByDomain[d.Id.String()]
|
||||
}
|
||||
result = append(result, entry)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// AddDomain appends a new domain to those managed.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
|
||||
"git.happydns.org/happyDomain/internal/api/controller"
|
||||
"git.happydns.org/happyDomain/internal/api/middleware"
|
||||
checkerUC "git.happydns.org/happyDomain/internal/usecase/checker"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
|
|
@ -40,11 +41,13 @@ func DeclareDomainRoutes(
|
|||
zoneServiceUC happydns.ZoneServiceUsecase,
|
||||
serviceUC happydns.ServiceUsecase,
|
||||
cc *controller.CheckerController,
|
||||
checkStatusUC *checkerUC.CheckStatusUsecase,
|
||||
) {
|
||||
dc := controller.NewDomainController(
|
||||
domainUC,
|
||||
remoteZoneImporter,
|
||||
zoneImporter,
|
||||
checkStatusUC,
|
||||
)
|
||||
|
||||
router.GET("/domains", dc.GetDomains)
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ func DeclareRoutes(cfg *happydns.Options, router *gin.RouterGroup, dep Dependenc
|
|||
dep.ZoneService,
|
||||
dep.Service,
|
||||
cc,
|
||||
dep.CheckStatusUC,
|
||||
)
|
||||
DeclareProviderRoutes(apiAuthRoutes, dep.Provider)
|
||||
DeclareProviderSettingsRoutes(apiAuthRoutes, dep.ProviderSettings)
|
||||
|
|
|
|||
|
|
@ -174,6 +174,44 @@ func (u *CheckStatusUsecase) DeleteExecutionsByChecker(checkerID string, target
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -104,6 +104,13 @@ type DomainWithZoneMetadata struct {
|
|||
ZoneMeta map[string]*ZoneMeta `json:"zone_meta"`
|
||||
}
|
||||
|
||||
type DomainWithCheckStatus struct {
|
||||
*Domain
|
||||
// LastCheckStatus is the worst status across the most recent result of each
|
||||
// checker that has run on this domain. Nil if no results exist yet.
|
||||
LastCheckStatus *Status `json:"last_check_status,omitempty"`
|
||||
}
|
||||
|
||||
type Subdomain string
|
||||
type Origin string
|
||||
|
||||
|
|
|
|||
|
|
@ -27,21 +27,25 @@ import {
|
|||
deleteDomainsByDomainId,
|
||||
getDomainsByDomainIdLogs,
|
||||
} from "$lib/api-base/sdk.gen";
|
||||
import type { HappydnsDomainUpdateInput } from "$lib/api-base/types.gen";
|
||||
import type {
|
||||
HappydnsDomainUpdateInput,
|
||||
HappydnsDomainWithCheckStatus,
|
||||
HappydnsDomainWithZoneMetadata,
|
||||
} from "$lib/api-base/types.gen";
|
||||
import type { Domain, DomainLog } from "$lib/model/domain";
|
||||
import type { Provider } from "$lib/model/provider";
|
||||
import { unwrapSdkResponse, unwrapEmptyResponse } from "./errors";
|
||||
|
||||
export async function listDomains(): Promise<Array<Domain>> {
|
||||
return unwrapSdkResponse(await getDomains()) as Array<Domain>;
|
||||
export async function listDomains(): Promise<Array<HappydnsDomainWithCheckStatus>> {
|
||||
return unwrapSdkResponse(await getDomains()) as Array<HappydnsDomainWithCheckStatus>;
|
||||
}
|
||||
|
||||
export async function getDomain(id: string): Promise<Domain> {
|
||||
export async function getDomain(id: string): Promise<HappydnsDomainWithZoneMetadata> {
|
||||
return unwrapSdkResponse(
|
||||
await getDomainsByDomainId({
|
||||
path: { domainId: id },
|
||||
}),
|
||||
) as Domain;
|
||||
) as HappydnsDomainWithZoneMetadata;
|
||||
}
|
||||
|
||||
export async function addDomain(domain: string, provider: Provider | undefined): Promise<Domain> {
|
||||
|
|
|
|||
|
|
@ -25,13 +25,14 @@
|
|||
import { Badge, Button, ButtonGroup, Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import ProviderLink from "$lib/components/providers/ProviderLink.svelte";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import type { HappydnsDomainWithCheckStatus } from "$lib/api-base/types.gen";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
import { t } from "$lib/translations";
|
||||
import { getStatusColor, getStatusIcon } from "$lib/utils/checkers";
|
||||
|
||||
interface Props {
|
||||
domain: Domain;
|
||||
ondelete: (event: Event, domain: Domain) => void;
|
||||
domain: HappydnsDomainWithCheckStatus;
|
||||
ondelete: (event: Event, domain: HappydnsDomainWithCheckStatus) => void;
|
||||
}
|
||||
|
||||
let { domain, ondelete }: Props = $props();
|
||||
|
|
@ -47,7 +48,15 @@
|
|||
<ProviderLink id_provider={domain.id_provider} onclick={(e) => e.stopPropagation()} />
|
||||
</td>
|
||||
<td>
|
||||
<Badge color="success">OK</Badge>
|
||||
<a
|
||||
href="/domains/{encodeURIComponent(domain.domain)}/checks"
|
||||
class="text-decoration-none"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Badge color={getStatusColor(domain.last_check_status)}>
|
||||
<Icon name={getStatusIcon(domain.last_check_status)} />
|
||||
</Badge>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<ButtonGroup size="sm">
|
||||
|
|
|
|||
|
|
@ -28,14 +28,14 @@
|
|||
|
||||
import { deleteDomain } from "$lib/api/domains";
|
||||
import DomainTableRow from "$lib/components/domains/DomainTableRow.svelte";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import type { HappydnsDomainWithCheckStatus } from "$lib/api-base/types.gen";
|
||||
import { refreshDomains } from "$lib/stores/domains";
|
||||
import { providersSpecs, refreshProvidersSpecs } from "$lib/stores/providers";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
interface Props {
|
||||
class?: ClassValue;
|
||||
items: Array<Domain>;
|
||||
items: Array<HappydnsDomainWithCheckStatus>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
if (!$providersSpecs) refreshProvidersSpecs();
|
||||
|
||||
async function delDomain(event: Event, item: Domain) {
|
||||
async function delDomain(event: Event, item: HappydnsDomainWithCheckStatus) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!confirm($t("domains.alert.remove", { domain: item.domain }))) return;
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@
|
|||
<Input
|
||||
type="select"
|
||||
value={domain.group}
|
||||
on:change={(event) => changeGroup(event, domain.id, domain)}
|
||||
onchange={(event) => changeGroup(event, domain.id, domain)}
|
||||
>
|
||||
<option value="">{$t("domaingroups.no-group")}</option>
|
||||
{#each mygroups as group}
|
||||
|
|
|
|||
|
|
@ -22,21 +22,24 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import FilterDomainInput from "$lib/components/pages/home/FilterDomainInput.svelte";
|
||||
import CardImportableDomains from "$lib/components/providers/CardImportableDomains.svelte";
|
||||
import ZoneList from "$lib/components/zones/ZoneList.svelte";
|
||||
import type { HappydnsDomainWithCheckStatus } from "$lib/api-base/types.gen";
|
||||
import { fqdnCompare } from "$lib/dns";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import { domains } from "$lib/stores/domains";
|
||||
import { filteredGroup, filteredName, filteredProvider } from "$lib/stores/home";
|
||||
import { t } from "$lib/translations";
|
||||
import { getStatusColor, getStatusIcon } from "$lib/utils/checkers";
|
||||
|
||||
let noDomainsList = $state(false);
|
||||
|
||||
let filteredDomains: Array<Domain> = $derived(refreshFilteredDomains());
|
||||
let filteredDomains: Array<HappydnsDomainWithCheckStatus> = $derived(refreshFilteredDomains());
|
||||
|
||||
function refreshFilteredDomains(): Array<Domain> {
|
||||
let myDomains: Array<Domain> = [];
|
||||
function refreshFilteredDomains(): Array<HappydnsDomainWithCheckStatus> {
|
||||
let myDomains: Array<HappydnsDomainWithCheckStatus> = [];
|
||||
|
||||
if ($domains) {
|
||||
myDomains = $domains.filter(
|
||||
|
|
@ -58,7 +61,16 @@
|
|||
<FilterDomainInput class="mb-3" />
|
||||
|
||||
{#if filteredDomains.length}
|
||||
<ZoneList button display_by_groups domains={filteredDomains} links show_empty_groups />
|
||||
<ZoneList button display_by_groups domains={filteredDomains} links show_empty_groups>
|
||||
{#snippet badges({ domain })}
|
||||
<a
|
||||
href="/domains/{encodeURIComponent(domain.domain)}/checks"
|
||||
class={"text-" + getStatusColor(domain.last_check_status)}
|
||||
>
|
||||
<Icon name={getStatusIcon(domain.last_check_status)} />
|
||||
</a>
|
||||
{/snippet}
|
||||
</ZoneList>
|
||||
{:else}
|
||||
<div class="my-4 text-center text-muted">
|
||||
{$t("domains.filtered-no-result")}
|
||||
|
|
|
|||
|
|
@ -21,34 +21,27 @@
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" generics="T extends HappydnsDomain = HappydnsDomain">
|
||||
import { createEventDispatcher, type Snippet } from "svelte";
|
||||
|
||||
import { Badge } from "@sveltestrap/sveltestrap";
|
||||
import { ListGroup } from "@sveltestrap/sveltestrap";
|
||||
import DomainWithProvider from "$lib/components/domains/DomainWithProvider.svelte";
|
||||
import { updateDomain } from "$lib/api/domains";
|
||||
import type { HappydnsDomain } from "$lib/api-base/types.gen";
|
||||
import { domains_idx, newlyGroups, refreshDomains } from "$lib/stores/domains";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
interface ZoneListDomain {
|
||||
id: string;
|
||||
domain: string;
|
||||
id_provider: string;
|
||||
group?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
flush?: boolean;
|
||||
links?: boolean;
|
||||
display_by_groups?: boolean;
|
||||
show_empty_groups?: boolean;
|
||||
domains?: Array<ZoneListDomain>;
|
||||
domains?: Array<T>;
|
||||
no_domain?: Snippet;
|
||||
badges?: Snippet<[{ domain: ZoneListDomain }]>;
|
||||
badges?: Snippet<[{ domain: T }]>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +57,7 @@
|
|||
}: Props = $props();
|
||||
|
||||
function genGroups(
|
||||
domains: Array<ZoneListDomain>,
|
||||
domains: Array<T>,
|
||||
display_by_groups: boolean,
|
||||
show_empty_groups: boolean,
|
||||
extraGroups: Array<string>,
|
||||
|
|
@ -73,7 +66,7 @@
|
|||
return { "": domains };
|
||||
}
|
||||
|
||||
const groups: Record<string, Array<ZoneListDomain>> = {};
|
||||
const groups: Record<string, Array<T>> = {};
|
||||
|
||||
for (const domain of domains) {
|
||||
const group = domain.group ?? "";
|
||||
|
|
@ -91,13 +84,13 @@
|
|||
return groups;
|
||||
}
|
||||
|
||||
let localDomains: Array<ZoneListDomain> = $derived([...domains]);
|
||||
let localDomains: Array<T> = $derived([...domains]);
|
||||
|
||||
let groups: Record<string, Array<ZoneListDomain>> = $derived(
|
||||
let groups: Record<string, Array<T>> = $derived(
|
||||
genGroups(localDomains, display_by_groups, show_empty_groups, $newlyGroups),
|
||||
);
|
||||
|
||||
let draggedDomain: ZoneListDomain | null = $state(null);
|
||||
let draggedDomain: T | null = $state(null);
|
||||
let dragOverGroup: string | null = $state(null);
|
||||
|
||||
async function handleDrop(targetGroup: string) {
|
||||
|
|
@ -129,19 +122,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
function getDomainHref(domain: ZoneListDomain): string | undefined {
|
||||
if (links && !domain.href) {
|
||||
function getDomainHref(domain: T): string | undefined {
|
||||
if (links) {
|
||||
if ($domains_idx[domain.domain]) {
|
||||
return "/domains/" + encodeURIComponent(domain.domain);
|
||||
} else {
|
||||
return "/domains/" + encodeURIComponent(domain.id);
|
||||
}
|
||||
}
|
||||
return domain.href;
|
||||
return undefined;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet domainRow(domain: ZoneListDomain)}
|
||||
{#snippet domainRow(domain: T)}
|
||||
<DomainWithProvider {domain} />
|
||||
{#if badges}{@render badges({ domain })}{:else}
|
||||
<Badge color="success">OK</Badge>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@
|
|||
|
||||
import { get, derived, writable, type Writable } from "svelte/store";
|
||||
import { listDomains } from "$lib/api/domains";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import type { HappydnsDomainWithCheckStatus } from "$lib/api-base/types.gen";
|
||||
|
||||
export const domains: Writable<Array<Domain> | undefined> = writable(undefined);
|
||||
export const domains: Writable<Array<HappydnsDomainWithCheckStatus> | undefined> = writable(undefined);
|
||||
export const newlyGroups: Writable<Array<string>> = writable([]);
|
||||
|
||||
export async function refreshDomains() {
|
||||
|
|
@ -35,7 +35,7 @@ export async function refreshDomains() {
|
|||
return data;
|
||||
}
|
||||
|
||||
export const groups = derived(domains, ($domains: Array<Domain> | undefined) => {
|
||||
export const groups = derived(domains, ($domains: Array<HappydnsDomainWithCheckStatus> | undefined) => {
|
||||
if (!$domains) return [];
|
||||
|
||||
const groups = new Set<string>();
|
||||
|
|
@ -51,8 +51,8 @@ export const groups = derived(domains, ($domains: Array<Domain> | undefined) =>
|
|||
});
|
||||
});
|
||||
|
||||
export const domains_idx = derived(domains, ($domains: Array<Domain> | undefined) => {
|
||||
const idx: Record<string, Domain> = {};
|
||||
export const domains_idx = derived(domains, ($domains: Array<HappydnsDomainWithCheckStatus> | undefined) => {
|
||||
const idx: Record<string, HappydnsDomainWithCheckStatus> = {};
|
||||
|
||||
if (!$domains) return idx;
|
||||
|
||||
|
|
@ -75,8 +75,8 @@ export const domains_idx = derived(domains, ($domains: Array<Domain> | undefined
|
|||
return idx;
|
||||
});
|
||||
|
||||
export const domains_by_name = derived(domains, ($domains: Array<Domain> | undefined) => {
|
||||
const idx: Record<string, Array<Domain>> = {};
|
||||
export const domains_by_name = derived(domains, ($domains: Array<HappydnsDomainWithCheckStatus> | undefined) => {
|
||||
const idx: Record<string, Array<HappydnsDomainWithCheckStatus>> = {};
|
||||
|
||||
if (!$domains) return idx;
|
||||
|
||||
|
|
@ -91,8 +91,8 @@ export const domains_by_name = derived(domains, ($domains: Array<Domain> | undef
|
|||
return idx;
|
||||
});
|
||||
|
||||
export const domains_by_groups = derived(domains, ($domains: Array<Domain> | undefined) => {
|
||||
const groups: Record<string, Array<Domain>> = {};
|
||||
export const domains_by_groups = derived(domains, ($domains: Array<HappydnsDomainWithCheckStatus> | undefined) => {
|
||||
const groups: Record<string, Array<HappydnsDomainWithCheckStatus>> = {};
|
||||
|
||||
if ($domains === undefined) {
|
||||
return groups;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { error, type Load } from "@sveltejs/kit";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import type { HappydnsDomainWithZoneMetadata } from "$lib/api-base/types.gen";
|
||||
import { domains, domains_idx, refreshDomains } from "$lib/stores/domains";
|
||||
|
||||
export const load: Load = async ({ parent, params }) => {
|
||||
|
|
@ -15,7 +15,7 @@ export const load: Load = async ({ parent, params }) => {
|
|||
});
|
||||
}
|
||||
|
||||
const domain: Domain | null = get(domains_idx)[params.dn];
|
||||
const domain: HappydnsDomainWithZoneMetadata | null = get(domains_idx)[params.dn];
|
||||
|
||||
if (!domain) {
|
||||
error(404, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue