Compare commits

..

28 commits

Author SHA1 Message Date
6e91c779bd checker: also consider WIP zone for tidy, scheduler, and auto-fill
All checks were successful
continuous-integration/drone/push Build is passing
Tidy and scheduler now check both the WIP zone ([0]) and the latest
published zone ([1]) so that services being drafted are not cleaned up
or ignored by the scheduler.  Auto-fill searches WIP first for the
best user experience when configuring new services.
2026-04-16 04:44:37 +07:00
b8b02ea125 fix: use latest published zone instead of oldest in checker subsystem
ZoneHistory is ordered [WIP, newest-published, ..., oldest-published].
The tidy, scheduler, and auto-fill code was using ZoneHistory[len-1]
(the oldest zone) instead of ZoneHistory[1] (the latest published).

This caused the scheduler to enumerate services from the oldest zone
snapshot, tidy to check service existence against outdated data, and
auto-fill to resolve from the wrong zone.
2026-04-16 04:44:37 +07:00
6133ee0321 checker: pause scheduling for paused or inactive users
Add a job-level gate to the scheduler. When set, the gate is consulted
on every popped job; if it returns false, the job is skipped and
re-enqueued for its next interval without invoking the engine.

A new UserGater builds such a gate from a user resolver and an
inactivity threshold:

  - users with UserQuota.SchedulingPaused are always blocked (admin
    kill switch);
  - users whose LastSeen is older than their effective inactivity
    horizon (UserQuota.InactivityPauseDays, falling back to a system
    default) are blocked until they log in again;
  - lookups are cached for 5 minutes so the scheduler hot path stays
    cheap, with an Invalidate hook for use on user updates.

This addresses the "free trial then forgotten" failure mode described
in the design notes.
2026-04-16 04:44:37 +07:00
40a92f97dd checker: add Janitor goroutine to enforce retention policy
The Janitor periodically walks every CheckPlan, loads its executions,
and deletes the ones that the tiered RetentionPolicy says to drop.

Per-user overrides are honoured: if a user's UserQuota.RetentionDays
is set, that horizon replaces the system default for the user's plans.
User lookups are cached per sweep to avoid repeated storage hits.

The janitor is the long-tail counterpart of the (still TODO) cheap
hard cap that will be applied at execution-creation time. It runs
immediately on Start() and then every configured interval (default 6h).
2026-04-16 04:44:37 +07:00
59c50d12c0 checker: keep 1 report per hour after the first day
Insert an hourly tier between the full-detail window and the daily
bucket so users still get sub-day resolution for the first week:

  0..1 day  -> all
  1..7 days -> 1 per hour
  7..30     -> 2 per day
  ...
2026-04-16 04:44:37 +07:00
c06cbfa8cd checker: add tiered RetentionPolicy
Introduce a pure RetentionPolicy.Decide function that partitions check
executions into keep/drop sets according to a tiered policy:

  - 0..7 days   -> every execution
  - 7..30 days  -> 2 per day per (checker, target)
  - 30..D/2     -> 1 per week per (checker, target)
  - D/2..D days -> 1 per month per (checker, target)
  - > D days    -> dropped

The function is intentionally storage-agnostic so the upcoming janitor
goroutine can call it on any execution slice and so it can be unit
tested directly. All thresholds are configurable to allow per-user
overrides via UserQuota.
2026-04-16 04:44:37 +07:00
7a1e74e836 model: add UserQuota struct for admin-controlled per-user limits
Introduce a UserQuota field on the User model to hold admin-controlled
limits and flags that the user cannot modify. Only checker-related
fields are defined for now (max checks per day, retention days,
inactivity pause days, scheduling kill switch); future paid-plan
attributes will be added here later.

The user-facing API only exposes settings updates and account deletion,
so Quota cannot be written through it. Updates go through the existing
admin user PUT endpoint, with a new editor card in the admin UI under
/users/[uid].
2026-04-16 04:44:37 +07:00
7e762df161 New checker: Matrix federation 2026-04-16 04:44:37 +07:00
6705dc8d56 New checker: zonemaster 2026-04-16 04:44:37 +07:00
ee6b7a8763 New checker: ICMP ping checker with RTT and packet loss metrics 2026-04-16 04:44:37 +07:00
865b4db24f checkers: add HTTP transport layer
Introduce a transport abstraction so observation providers can run either
locally or be delegated to a remote HTTP endpoint. When an admin sets the
"endpoint" option, the engine substitutes the local provider with an
HTTPObservationProvider that POSTs to {endpoint}/collect.
2026-04-16 04:44:37 +07:00
6a36cbd7c0 checkers: add incremental scheduler updates on domain/zone changes
Instead of rebuilding the entire scheduler queue, incrementally add or
remove jobs when domains are created/deleted or zones are
imported/published. A wake channel interrupts the run loop so new jobs
are picked up immediately. A jobKeys index prevents duplicate entries.

Hook points: domain creation, domain deletion, zone import, and zone
publish (correction apply) all notify the scheduler via the narrow
SchedulerDomainNotifier interface, wired through setter methods to
avoid initialization ordering issues.
2026-04-16 04:44:37 +07:00
a6aa363fa5 checkers: store observations as json.RawMessage with cross-checker reuse
Refactor observation data pipeline to serialize once after collection and
keep json.RawMessage throughout storage and API responses. This eliminates
double-serialization and makes DB round-trips lossless.
2026-04-16 04:44:37 +07:00
7d422df708 checkers: add NoOverride field support for checker options
Prevent more specific scopes from overriding option values locked at a
higher scope (e.g. admin). Includes defense-in-depth stripping on
Set/Add operations, merge-time preservation, and frontend filtering.
2026-04-16 04:44:37 +07:00
444949ded6 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-16 04:44:37 +07:00
0633538e88 checkers: show children checkers on domain page and hide scheduling for non-domain checkers
Add a separate section on the domain checks page to display zone and
service-level checkers that can be configured but won't produce results
at the domain scope. Hide the scheduling and rules cards when configuring
a non-domain checker from the domain context.
2026-04-16 04:44:37 +07:00
faa2cbb4d7 checkers: integrate rules as a view tab in execution detail page
Remove separate /rules pages and display rules as a tab alongside
metrics, HTML, and JSON views. Rules become the default view when
no metrics or HTML report is available. Status is now shown as a
colored badge in the rules table.
2026-04-16 04:44:37 +07:00
7ff5fa7a58 checkers: add frontend metrics chart on execution pages
Add Chart.js-based line chart for checker metrics. The chart appears
on the executions list page (aggregated) and on individual execution
detail pages. Metrics view mode is selectable via the sidebar alongside
HTML report and raw JSON views.
2026-04-16 04:44:37 +07:00
f8e1bfac03 checkers: add metrics export in JSON format
Observation providers can now implement CheckerMetricsReporter to
extract time-series metrics from their stored data. The controller
returns the metrics as a JSON array.

Routes: user-level (/api/checkers/metrics), domain-level, per-checker,
and per-execution.
2026-04-16 04:35:40 +07:00
7331d4dfbe checkers: add HTML report rendering for observation providers
Introduce CheckerHTMLReporter interface that observation providers can
implement to render rich HTML documents from their data. The Zonemaster
provider implements it with collapsible accordions and severity badges.

Adds API endpoint GET .../observations/:obsKey/report, frontend stores
for view mode switching (HTML/JSON), and wires the sidebar toggle buttons.
2026-04-16 04:35:40 +07:00
1e9e7b2161 checkers: add frontend UI components and routes
Add all checker UI pages and components:
- Checker list, config, schedule, and rules pages
- Execution list, detail, results, and rules pages
- Sidebar components for domain/service checker status
- Run check modal with option overrides and rule selection
- Domain-scoped and service-scoped check routes
- Admin pages for checker configuration and scheduler management
- Header navigation link for checkers section
2026-04-16 04:35:40 +07:00
34788c145f checkers: add frontend API client, stores, and utilities
Add the frontend infrastructure for the checker UI:
- API client with scoped helpers for domain/service-level operations
- Svelte stores for checker state (currentExecution, currentCheckInfo)
- Utility functions for status colors, icons, i18n keys, date formatting
- Shared helpers: withInheritedPlaceholders, downloadBlob, collectAllOptionDocs
- English translations for all checker UI strings
- Zone model and form types extended for checker support
2026-04-16 04:27:07 +07:00
640c848c98 checkers: add API controllers, routes, and app wiring
Wire up the checker system to the HTTP layer:
- API controllers for checker operations, options, plans, and results
- Scoped routes at domain and service level
- Admin controllers for checker config and scheduler management
- App initialization: create usecases, start/stop scheduler
- Zone controller updated to include per-service check status
2026-04-16 04:27:07 +07:00
6424be8f6b checkers: add usecases, engine, and scheduler
Implement the checker business logic:
- CheckerOptionsUsecase: scope-based option resolution, validation,
  auto-fill from execution context (domain, zone, service)
- CheckPlanUsecase: CRUD for user scheduling configurations
- CheckStatusUsecase: aggregated status queries, execution history
- CheckerEngine: full execution pipeline (observe, evaluate, aggregate)
- Scheduler: background job executor with auto-discovery, min-heap
  queue, worker pool, and jitter-based scheduling
2026-04-16 04:27:07 +07:00
d9468df619 checkers: add storage interfaces, implementations, and tidy
Add the persistence layer for the checker system:
- Storage interfaces (CheckPlanStorage, CheckerOptionsStorage,
  CheckEvaluationStorage, ExecutionStorage, ObservationSnapshotStorage,
  SchedulerStateStorage) in the usecase/checker package
- KV-based implementations for LevelDB/Oracle NoSQL/InMemory backends
- Integrate checker storage into the main Storage interface
- Add tidy methods for checker entities (plans, configurations,
  evaluations, executions, snapshots, observation cache) and
  secondary index cleanup
2026-04-16 04:27:07 +07:00
5afb2797b4 checkers: add map-based option validation for checker fields
Add ValidateMapValues() to the forms package for validating
checker option maps against field documentation (required fields,
allowed choices, type checking).
2026-04-16 04:27:06 +07:00
e7753a84b2 checkers: load external checker plugins from .so files
All checks were successful
continuous-integration/drone/push Build is passing
Scan -plugins-directory paths at startup, open each .so via plugin.Open,
look up the NewCheckerPlugin symbol from checker-sdk-go, and register the
returned definition and observation provider in the global checker
registries. A pluginLoader indirection keeps the door open for future
plugin kinds.
2026-04-16 04:27:06 +07:00
5726b267e6 checkers: introduce checker subsystem foundation
Add the checker-sdk-go dependency and build the core checker
infrastructure:
- Domain model types: CheckTarget, CheckPlan, Execution,
  CheckEvaluation, CheckerDefinition, CheckerOptions,
  ObservationSnapshot, and associated interfaces
- Observation collection engine with concurrent per-key gathering
- Checker and observation provider registries (wrapping checker-sdk-go)
- WorstStatusAggregator for combining rule evaluation results
2026-04-16 04:27:06 +07:00
13 changed files with 104 additions and 195 deletions

2
go.sum
View file

@ -12,8 +12,6 @@ git.happydns.org/checker-matrix v0.0.0-20260407211824-2bb91d33d489 h1:pTGfGq88Dj
git.happydns.org/checker-matrix v0.0.0-20260407211824-2bb91d33d489/go.mod h1:fQjY1yWYFucu+Ebn5uYM7ZWTJNQIgjMENI/8tqlaR98=
git.happydns.org/checker-ping v0.0.0-20260407194626-a2ebf17774fc h1:jKEOx2NDbHHxjCy1fUkcn1RgpzOKbE+bGRsF+ITNigI=
git.happydns.org/checker-ping v0.0.0-20260407194626-a2ebf17774fc/go.mod h1:wphWmslFhKcpWfJTrHdChv8DkhUP9jwis7V2jy7vOX0=
git.happydns.org/checker-sdk-go v0.4.0 h1:MDUnzdIy+o4yXQkcGl6QRXVprwlERmIJ9nuO7cspUBs=
git.happydns.org/checker-sdk-go v0.4.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
git.happydns.org/checker-sdk-go v0.5.0 h1:wpFIK/vxanrAYf1OlewSnSCYc7KOJKdu88uUWB7HIQI=
git.happydns.org/checker-sdk-go v0.5.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
git.happydns.org/checker-zonemaster v0.0.0-20260407202727-979757b5a8fc h1:y5xjoqLA/WztFWhEUifOwnJ6POjl+Udw6bWjzQ2afOw=

View file

@ -64,9 +64,11 @@
let inheritedValues = $state<Record<string, unknown>>({});
let savingOptions = $state(false);
let checkerDef = $derived($checkers?.[checkerId]);
let intervalSpec = $derived(checkerDef?.interval);
let plan = $state<HappydnsCheckPlanWritable>({
enabled: {},
interval: 3600,
});
$effect(() => {
@ -142,7 +144,7 @@
<Row class="mb-4">
{#if showSchedule}
<Col md={6}>
<CheckerScheduleCard {scope} {checkerId} bind:plan />
<CheckerScheduleCard {scope} {checkerId} bind:plan {intervalSpec} />
{#if status.rules && status.rules.length > 0}
<CheckerRulesCard

View file

@ -33,7 +33,7 @@
Input,
Label,
} from "@sveltestrap/sveltestrap";
import type { HappydnsCheckPlan, HappydnsCheckPlanWritable } from "$lib/api-base/types.gen";
import type { CheckerCheckIntervalSpec, HappydnsCheckPlan, HappydnsCheckPlanWritable } from "$lib/api-base/types.gen";
import { t } from "$lib/translations";
import { toasts } from "$lib/stores/toasts";
import type { CheckerScope } from "$lib/api/checkers";
@ -43,17 +43,31 @@
updateScopedCheckPlan,
} from "$lib/api/checkers";
const NS_PER_MINUTE = 60_000_000_000;
const NS_PER_HOUR = 3_600_000_000_000;
interface Props {
scope: CheckerScope;
checkerId: string;
plan: HappydnsCheckPlan | HappydnsCheckPlanWritable;
intervalSpec?: CheckerCheckIntervalSpec;
}
let { scope, checkerId, plan = $bindable() }: Props = $props();
let { scope, checkerId, plan = $bindable(), intervalSpec }: Props = $props();
let existingPlanId = $state<string | undefined>(undefined);
let saving = $state(false);
// Determine whether to use minutes or hours as the UI unit.
let useMinutes = $derived(
intervalSpec != null && intervalSpec.min != null && intervalSpec.min < NS_PER_HOUR
);
let unitNs = $derived(useMinutes ? NS_PER_MINUTE : NS_PER_HOUR);
let defaultIntervalNs = $derived(intervalSpec?.default ?? NS_PER_HOUR);
let minNs = $derived(intervalSpec?.min ?? NS_PER_HOUR);
let maxNs = $derived(intervalSpec?.max ?? 24 * NS_PER_HOUR);
let schedulesPromise = $derived(getScopedCheckPlans(scope, checkerId));
$effect(() => {
@ -63,7 +77,7 @@
existingPlanId = s.id;
plan = {
enabled: s.enabled ?? {},
interval: s.interval ?? 3600,
interval: s.interval ?? defaultIntervalNs,
};
}
});
@ -97,12 +111,31 @@
}
}
function intervalHours(): number {
return Math.round((plan.interval ?? 3600) / 3600);
function intervalDisplayValue(): number {
return Math.round((plan.interval ?? defaultIntervalNs) / unitNs);
}
function setIntervalHours(hours: number) {
plan.interval = Math.max(3600, hours * 3600);
function setIntervalValue(val: number) {
const clamped = Math.max(minNs, Math.min(maxNs, val * unitNs));
plan.interval = clamped;
}
const NS_PER_DAY = 24 * NS_PER_HOUR;
const NS_PER_WEEK = 7 * NS_PER_DAY;
function formatDuration(ns: number): string {
if (ns >= NS_PER_WEEK && ns % NS_PER_WEEK === 0) {
return `${ns / NS_PER_WEEK}w`;
}
if (ns >= NS_PER_DAY && ns % NS_PER_DAY === 0) {
return `${ns / NS_PER_DAY}d`;
}
if (ns >= NS_PER_HOUR) {
const h = Math.round(ns / NS_PER_HOUR);
return `${h}h`;
}
const m = Math.round(ns / NS_PER_MINUTE);
return `${m}min`;
}
</script>
@ -125,15 +158,22 @@
<div class="d-flex align-items-center gap-2">
<Input
type="number"
min={1}
value={intervalHours()}
min={Math.round(minNs / unitNs)}
max={Math.round(maxNs / unitNs)}
value={intervalDisplayValue()}
oninput={(e: Event) =>
setIntervalHours(parseInt((e.target as HTMLInputElement).value) || 1)}
setIntervalValue(parseInt((e.target as HTMLInputElement).value) || 1)}
style="width: 100px"
/>
<span>{$t("checkers.schedule.hours")}</span>
<span>{useMinutes ? $t("checkers.schedule.minutes") : $t("checkers.schedule.hours")}</span>
</div>
<small class="text-muted">{$t("checkers.schedule.interval-hint")}</small>
<small class="text-muted">
{$t("checkers.schedule.interval-hint", {
intervalMin: formatDuration(minNs),
intervalMax: formatDuration(maxNs),
intervalDefault: formatDuration(defaultIntervalNs),
})}
</small>
</FormGroup>
</form>

View file

@ -27,13 +27,22 @@
import { t } from "$lib/translations";
import type { CheckerScope, CheckMetric } from "$lib/api/checkers";
import type { HappydnsCheckEvaluation } from "$lib/api-base/types.gen";
import {
getScopedExecution,
getScopedExecutionObservations,
getScopedExecutionMetrics,
getScopedExecutionResults,
getCheckStatus,
} from "$lib/api/checkers";
import { currentExecution, currentCheckInfo, currentObservations, reportViewMode, cachedHTMLReport } from "$lib/stores/checkers";
import {
currentExecution,
currentCheckInfo,
currentObservations,
reportViewMode,
cachedHTMLReport,
} from "$lib/stores/checkers";
import ExecutionResultsCard from "./ExecutionResultsCard.svelte";
import ObservationReportCard from "./ObservationReportCard.svelte";
interface Props {
@ -48,11 +57,13 @@
let loading = $state(true);
let error = $state<string | undefined>(undefined);
let metricsData = $state<CheckMetric[] | null>(null);
let evaluationData = $state<HappydnsCheckEvaluation | null>(null);
$effect(() => {
loading = true;
error = undefined;
metricsData = null;
evaluationData = null;
cachedHTMLReport.set(null);
Promise.all([
@ -65,7 +76,11 @@
currentCheckInfo.set(checkerInfo);
currentObservations.set(observations);
checkerName = checkerInfo.name ?? checkerId;
// Default to metrics view if supported, then HTML, then JSON
// Load rules data
getScopedExecutionResults(scope, checkerId, execId)
.then((e) => (evaluationData = e))
.catch((e) => console.warn("Failed to load execution results", e));
// Default to metrics view if supported, then HTML, then rules, then JSON
if (checkerInfo.has_metrics) {
reportViewMode.set("metrics");
getScopedExecutionMetrics(scope, checkerId, execId)
@ -74,7 +89,7 @@
} else if (checkerInfo.has_html_report) {
reportViewMode.set("html");
} else {
reportViewMode.set("json");
reportViewMode.set("rules");
}
loading = false;
},
@ -114,6 +129,10 @@
{$t("checkers.result.error-loading", { error })}
</Alert>
</Container>
{:else if $reportViewMode === "rules" && evaluationData}
<Container class="flex-fill d-flex flex-column mt-3">
<ExecutionResultsCard evaluation={evaluationData} />
</Container>
{:else if $currentObservations}
<ObservationReportCard
observations={$currentObservations}

View file

@ -57,6 +57,7 @@
let metricsData = $state<CheckMetric[] | null>(null);
$effect(() => {
metricsData = null;
getCheckStatus(checkerId).then((s) => {
resolvedName = s.name ?? checkerId;
if (s.has_metrics) {

View file

@ -22,10 +22,11 @@
-->
<script lang="ts">
import { Card, CardBody, CardHeader, Table } from "@sveltestrap/sveltestrap";
import { Badge, Card, CardBody, CardHeader, Table } from "@sveltestrap/sveltestrap";
import { t } from "$lib/translations";
import type { HappydnsCheckEvaluation } from "$lib/api-base/types.gen";
import { getStatusColor, getStatusI18nKey } from "$lib/utils";
interface Props {
evaluation: HappydnsCheckEvaluation;
@ -40,10 +41,11 @@
</CardHeader>
<CardBody>
{#if evaluation.states && evaluation.states.length > 0}
<Table size="sm" borderless>
<Table class="mb-0" size="sm" borderless hover>
<thead>
<tr>
<th>{$t("checkers.result.field.rule")}</th>
<th>{$t("checkers.result.field.status")}</th>
<th>{$t("checkers.result.field.message")}</th>
</tr>
</thead>
@ -51,6 +53,7 @@
{#each evaluation.states as state}
<tr>
<td><code>{state.code ?? ""}</code></td>
<td><Badge color={getStatusColor(state.status)}>{$t(getStatusI18nKey(state.status))}</Badge></td>
<td>{state.message ?? ""}</td>
</tr>
{/each}

View file

@ -1,79 +0,0 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-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/>.
-->
<script lang="ts">
import { Alert, Icon } from "@sveltestrap/sveltestrap";
import { t } from "$lib/translations";
import type { CheckerScope } from "$lib/api/checkers";
import { getCheckStatus, getScopedExecutionResults } from "$lib/api/checkers";
import PageTitle from "$lib/components/PageTitle.svelte";
import ExecutionResultsCard from "./ExecutionResultsCard.svelte";
interface Props {
scope: CheckerScope;
checkerId: string;
execId: string;
domainName: string;
}
let { scope, checkerId, execId, domainName }: Props = $props();
let resultsPromise = $derived(getScopedExecutionResults(scope, checkerId, execId));
let checkerName = $state<string>("");
$effect(() => {
getCheckStatus(checkerId).then((s) => {
checkerName = s.name ?? checkerId;
});
});
</script>
<svelte:head>
<title>{$t("checkers.detail.check-rules")} - {checkerName || checkerId} - happyDomain</title>
</svelte:head>
<div class="flex-fill mt-1 mb-5">
<PageTitle title={$t("checkers.detail.check-rules")} subtitle={checkerName} domain={domainName} />
{#await resultsPromise}
<p class="text-center">
<span class="spinner-border spinner-border-sm me-2"></span>
{$t("checkers.result.loading")}
</p>
{:then evaluation}
{#if evaluation}
<ExecutionResultsCard {evaluation} />
{:else}
<Alert color="info">
<Icon name="info-circle" />
{$t("checkers.result.no-results")}
</Alert>
{/if}
{:catch error}
<Alert color="danger">
<Icon name="exclamation-triangle-fill" />
{$t("checkers.result.error-loading", { error: error.message })}
</Alert>
{/await}
</div>

View file

@ -161,17 +161,10 @@
{/if}
<tr>
<th>{$t("checkers.result.field.status")}</th>
<td class="d-flex gap-2 align-items-center">
<td>
<Badge color={getStatusColor($currentExecution.result?.status)}>
{$t(getStatusI18nKey($currentExecution.result?.status))}
</Badge>
<a
href="{checksBase}/{encodeURIComponent(
checkerId,
)}/executions/{encodeURIComponent(execId)}/rules"
>
{$t("checkers.detail.check-rules")}
</a>
</td>
</tr>
{#if $currentExecution.result?.message}
@ -232,6 +225,18 @@
{$t("checkers.result.view-html")}
</Button>
{/if}
<Button
size="sm"
color="secondary"
outline
active={$reportViewMode === "rules"}
onclick={() => {
reportViewMode.set("rules");
}}
>
<Icon name="list-check"></Icon>
{$t("checkers.detail.check-rules")}
</Button>
<Button
size="sm"
color="secondary"

View file

@ -609,8 +609,9 @@
"schedule": {
"card-title": "Automatic scheduling",
"interval-label": "Check interval",
"minutes": "minutes",
"hours": "hours",
"interval-hint": "Minimum 1 hour. The check will run once per interval.",
"interval-hint": "Between {{intervalMin}} and {{intervalMax}}. Default: {{intervalDefault}}. The check will run once per interval.",
"no-schedule-yet": "No schedule created yet. Save to create one.",
"save": "Save",
"save-failed": "Failed to save schedule",
@ -711,7 +712,7 @@
"name": "Name:",
"availability": "Availability:",
"loading-options": "Loading options...",
"check-rules": "Check Individual Rules",
"check-rules": "Rules",
"admin-options": "Admin Options",
"configuration": "Configuration",
"save": "Save",

View file

@ -41,7 +41,7 @@ export const currentCheckInfo: Writable<CheckerCheckerDefinition | undefined> =
export const currentObservations: Writable<ObservationSnapshotWithData | undefined> = writable(undefined);
// Report view mode: which panel the main area shows
export type ReportViewMode = "json" | "html" | "metrics";
export type ReportViewMode = "json" | "html" | "metrics" | "rules";
export const reportViewMode: Writable<ReportViewMode> = writable("json");
export const showHTMLReport: Readable<boolean> = derived(reportViewMode, ($m) => $m === "html");

View file

@ -47,6 +47,9 @@ interface Params {
error?: string;
options?: string;
key?: string;
intervalMin?: string;
intervalMax?: string;
intervalDefault?: string;
// add more parameters that are used here
}

View file

@ -1,44 +0,0 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-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/>.
-->
<script lang="ts">
import { page } from "$app/state";
import type { Domain } from "$lib/model/domain";
import { fqdn } from "$lib/dns";
import ExecutionRulesPage from "$lib/components/checkers/ExecutionRulesPage.svelte";
let domain: Domain = $derived(page.data.domain);
let zoneId: string = $derived(page.data.zoneId);
let subdomain: string = $derived(page.data.subdomain);
let serviceid: string = $derived(page.data.serviceid);
let checkerId = $derived(page.params.checkerId!);
let execId = $derived(page.params.execId!);
</script>
<ExecutionRulesPage
scope={{ domainId: domain.id, zoneId, subdomain, serviceId: serviceid }}
{checkerId}
{execId}
domainName={fqdn(subdomain, domain.domain)}
/>

View file

@ -1,40 +0,0 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-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/>.
-->
<script lang="ts">
import { page } from "$app/state";
import type { Domain } from "$lib/model/domain";
import ExecutionRulesPage from "$lib/components/checkers/ExecutionRulesPage.svelte";
let domain: Domain = $derived(page.data.domain);
let checkerId = $derived(page.params.checkerId!);
let execId = $derived(page.params.execId!);
</script>
<ExecutionRulesPage
scope={{ domainId: domain.id }}
{checkerId}
{execId}
domainName={domain.domain}
/>