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
This commit is contained in:
parent
8c0c387969
commit
28bed1cb46
49 changed files with 4821 additions and 60 deletions
|
|
@ -101,6 +101,12 @@
|
|||
<NavItem>
|
||||
<NavLink href="/sessions" active={page && page.url.pathname.startsWith('/sessions')}>Sessions</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="/checkers" active={page && page.url.pathname.startsWith('/checkers')}>Checkers</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="/scheduler" active={page && page.url.pathname.startsWith('/scheduler')}>Scheduler</NavLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
|
|
|
|||
141
web-admin/src/routes/checkers/+page.svelte
Normal file
141
web-admin/src/routes/checkers/+page.svelte
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<!--
|
||||
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 {
|
||||
Card,
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Table,
|
||||
Row,
|
||||
Badge,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { getCheckers } from "$lib/api-base";
|
||||
|
||||
let checkersQ = $state(getCheckers());
|
||||
|
||||
let searchQuery = $state("");
|
||||
|
||||
function availabilityBadges(
|
||||
availability: { applyToDomain?: boolean; applyToZone?: boolean; applyToService?: boolean } | undefined,
|
||||
): { label: string; color: string }[] {
|
||||
if (!availability) return [];
|
||||
const badges = [];
|
||||
if (availability.applyToDomain) badges.push({ label: "Domain", color: "success" });
|
||||
if (availability.applyToZone) badges.push({ label: "Zone", color: "info" });
|
||||
if (availability.applyToService) badges.push({ label: "Service", color: "warning" });
|
||||
return badges;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container class="flex-fill my-5">
|
||||
<Row class="mb-4">
|
||||
<Col md={8}>
|
||||
<h1 class="display-5">
|
||||
<Icon name="puzzle-fill"></Icon>
|
||||
Checkers
|
||||
</h1>
|
||||
<p class="d-flex gap-3 align-items-center text-muted">
|
||||
<span class="lead"> Manage all checkers </span>
|
||||
{#await checkersQ then checkersR}
|
||||
<span>Total: {Object.keys(checkersR.data ?? {}).length} checkers</span>
|
||||
{/await}
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row class="mb-4">
|
||||
<Col md={8} lg={6}>
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="search"></Icon>
|
||||
</InputGroupText>
|
||||
<Input type="text" placeholder="Search checker..." bind:value={searchQuery} />
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{#await checkersQ}
|
||||
Please wait...
|
||||
{:then checkersR}
|
||||
{@const checkers = checkersR.data}
|
||||
<div class="table-responsive">
|
||||
<Table hover bordered>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plugin Name</th>
|
||||
<th>Availability</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if !checkers || Object.keys(checkers).length == 0}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted py-2">
|
||||
No checkers available
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each Object.entries(checkers ?? {}).filter(([name, _info]) => name
|
||||
.toLowerCase()
|
||||
.indexOf(searchQuery.toLowerCase()) > -1) as [checkerId, checkerInfo]}
|
||||
<tr>
|
||||
<td><strong>{checkerInfo.name || checkerId}</strong></td>
|
||||
<td>
|
||||
{#if availabilityBadges(checkerInfo.availability).length > 0}
|
||||
{#each availabilityBadges(checkerInfo.availability) as badge}
|
||||
<Badge color={badge.color} class="me-1">{badge.label}</Badge>
|
||||
{/each}
|
||||
{:else}
|
||||
<Badge color="secondary">General</Badge>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
href="/checkers/{checkerId}"
|
||||
class="btn btn-sm btn-primary"
|
||||
>
|
||||
<Icon name="gear-fill"></Icon>
|
||||
Manage
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
{:catch error}
|
||||
<Card body color="danger">
|
||||
<p class="mb-0">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
Error loading checkers: {error.message}
|
||||
</p>
|
||||
</Card>
|
||||
{/await}
|
||||
</Container>
|
||||
452
web-admin/src/routes/checkers/[checkerId]/+page.svelte
Normal file
452
web-admin/src/routes/checkers/[checkerId]/+page.svelte
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
<!--
|
||||
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,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Col,
|
||||
Container,
|
||||
Form,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Row,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { page } from "$app/state";
|
||||
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
import {
|
||||
getCheckersByCheckerId,
|
||||
getCheckersByCheckerIdOptions,
|
||||
putCheckersByCheckerIdOptions,
|
||||
} from "$lib/api-base";
|
||||
import type { HappydnsCheckerOptionDocumentation } from "$lib/api-base";
|
||||
import ResourceInput from "$lib/components/inputs/Resource.svelte";
|
||||
import { formatDuration } from "$lib/utils";
|
||||
|
||||
let checkerId = $derived(page.params.checkerId!);
|
||||
|
||||
let checkerQ = $derived(getCheckersByCheckerId({ path: { checkerId } }));
|
||||
let checkerOptionsQ = $derived(getCheckersByCheckerIdOptions({ path: { checkerId } }));
|
||||
let optionValues = $state<Record<string, unknown>>({});
|
||||
let saving = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
checkerOptionsQ.then((optionsR) => {
|
||||
optionValues = { ...((optionsR.data as Record<string, unknown>) || {}) };
|
||||
});
|
||||
});
|
||||
|
||||
async function saveOptions() {
|
||||
saving = true;
|
||||
try {
|
||||
await putCheckersByCheckerIdOptions({
|
||||
path: { checkerId },
|
||||
body: optionValues as any,
|
||||
});
|
||||
checkerOptionsQ = getCheckersByCheckerIdOptions({ path: { checkerId } });
|
||||
toasts.addToast({
|
||||
message: `Checker options updated successfully`,
|
||||
type: "success",
|
||||
timeout: 5000,
|
||||
});
|
||||
} catch (error) {
|
||||
toasts.addErrorToast({
|
||||
message: "Failed to update options: " + error,
|
||||
timeout: 10000,
|
||||
});
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanOrphanedOptions(adminOpts: HappydnsCheckerOptionDocumentation[]) {
|
||||
const validOptIds = new Set(adminOpts.map((opt) => opt.id));
|
||||
const cleanedOptions: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(optionValues)) {
|
||||
if (validOptIds.has(key)) {
|
||||
cleanedOptions[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
saving = true;
|
||||
try {
|
||||
await putCheckersByCheckerIdOptions({
|
||||
path: { checkerId },
|
||||
body: cleanedOptions as any,
|
||||
});
|
||||
checkerOptionsQ = getCheckersByCheckerIdOptions({ path: { checkerId } });
|
||||
toasts.addToast({
|
||||
message: `Orphaned options removed successfully`,
|
||||
type: "success",
|
||||
timeout: 5000,
|
||||
});
|
||||
} catch (error) {
|
||||
toasts.addErrorToast({
|
||||
message: "Failed to clean options: " + error,
|
||||
timeout: 10000,
|
||||
});
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getOrphanedOptions(adminOpts: HappydnsCheckerOptionDocumentation[]): string[] {
|
||||
const validOptIds = new Set(adminOpts.map((opt) => opt.id));
|
||||
return Object.keys(optionValues).filter((key) => !validOptIds.has(key));
|
||||
}
|
||||
|
||||
function availabilityBadges(
|
||||
availability:
|
||||
| {
|
||||
applyToDomain?: boolean;
|
||||
applyToZone?: boolean;
|
||||
applyToService?: boolean;
|
||||
limitToProviders?: string[];
|
||||
limitToServices?: string[];
|
||||
}
|
||||
| undefined,
|
||||
): { label: string; color: string }[] {
|
||||
if (!availability) return [];
|
||||
const badges = [];
|
||||
if (availability.applyToDomain) badges.push({ label: "Domain", color: "success" });
|
||||
if (availability.applyToZone) badges.push({ label: "Zone", color: "info" });
|
||||
if (availability.applyToService) badges.push({ label: "Service", color: "warning" });
|
||||
return badges;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container class="flex-fill my-5">
|
||||
<Row class="mb-4">
|
||||
<Col>
|
||||
<Button color="link" href="/checkers" class="mb-2">
|
||||
<Icon name="arrow-left"></Icon>
|
||||
Back to checkers
|
||||
</Button>
|
||||
<h1 class="display-5">
|
||||
<Icon name="puzzle-fill"></Icon>
|
||||
{checkerId}
|
||||
</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{#await checkerQ}
|
||||
<Card body>
|
||||
<p class="text-center mb-0">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
Loading checker status...
|
||||
</p>
|
||||
</Card>
|
||||
{:then checkerR}
|
||||
{@const checker = checkerR.data}
|
||||
{#if checker}
|
||||
<Row class="mb-4">
|
||||
<Col md={6}>
|
||||
<Card class="mb-3">
|
||||
<CardHeader>
|
||||
<strong>Checker Information</strong>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">Name:</dt>
|
||||
<dd class="col-sm-8">{checker.name}</dd>
|
||||
|
||||
<dt class="col-sm-4">Availability:</dt>
|
||||
<dd class="col-sm-8">
|
||||
{#if availabilityBadges(checker.availability).length > 0}
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{#each availabilityBadges(checker.availability) as badge}
|
||||
<Badge color={badge.color}
|
||||
>{badge.label}-level</Badge
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Badge color="secondary">General</Badge>
|
||||
{/if}
|
||||
{#if checker.availability?.limitToProviders?.length}
|
||||
<div class="mt-1 small text-muted">
|
||||
Providers: {checker.availability.limitToProviders.join(
|
||||
", ",
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
{#if checker.availability?.limitToServices?.length}
|
||||
<div class="mt-1 small text-muted">
|
||||
Services: {checker.availability.limitToServices.join(
|
||||
", ",
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
</dd>
|
||||
|
||||
{#if checker.interval}
|
||||
<dt class="col-sm-4">Interval:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span>default {formatDuration(checker.interval.default)}</span>
|
||||
<span class="text-muted small ms-2">
|
||||
(min {formatDuration(checker.interval.min)} / max {formatDuration(checker.interval.max)})
|
||||
</span>
|
||||
</dd>
|
||||
{/if}
|
||||
</dl>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{#if checker.rules && checker.rules.length > 0}
|
||||
<Card>
|
||||
<CardHeader class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<strong>Check Rules</strong>
|
||||
<Badge color="secondary" class="ms-2">
|
||||
{checker.rules.length}
|
||||
</Badge>
|
||||
</div>
|
||||
{#if checker.rules.reduce((acc, rule) => acc + rule.options?.adminOpts?.length, 0) > 0}
|
||||
<Button
|
||||
color="success"
|
||||
size="sm"
|
||||
onclick={saveOptions}
|
||||
disabled={saving}
|
||||
>
|
||||
{#if saving}
|
||||
<span class="spinner-border spinner-border-sm me-1"
|
||||
></span>
|
||||
{:else}
|
||||
<Icon name="check-circle"></Icon>
|
||||
{/if}
|
||||
Save
|
||||
</Button>
|
||||
{/if}
|
||||
</CardHeader>
|
||||
<ListGroup flush>
|
||||
{#each checker.rules as rule, i}
|
||||
{@const ruleOpts = rule.options?.adminOpts || []}
|
||||
<ListGroupItem>
|
||||
<div class="d-flex align-items-start gap-2 mb-1">
|
||||
<Icon
|
||||
name="check2-circle"
|
||||
class="text-success mt-1 flex-shrink-0"
|
||||
></Icon>
|
||||
<div class="flex-grow-1">
|
||||
<strong>{rule.name}</strong>
|
||||
{#if rule.description}
|
||||
<p class="text-muted small mb-0">
|
||||
{rule.description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if ruleOpts.length > 0}
|
||||
<div class="ms-4 mt-2">
|
||||
<Form onsubmit={saveOptions}>
|
||||
{#each ruleOpts as optDoc, index}
|
||||
{#if optDoc.id}
|
||||
<ResourceInput
|
||||
edit
|
||||
index={"" + index}
|
||||
specs={optDoc}
|
||||
type={optDoc.type || "string"}
|
||||
bind:value={optionValues[optDoc.id]}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</Form>
|
||||
</div>
|
||||
{/if}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
{/if}
|
||||
</Col>
|
||||
|
||||
<Col md={6}>
|
||||
{#await checkerOptionsQ}
|
||||
<Card>
|
||||
<CardBody>
|
||||
<p class="text-center mb-0">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
Loading options...
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{:then _optionsR}
|
||||
{@const adminOpts = checker.options?.adminOpts || []}
|
||||
{@const readOnlyOptGroups = [
|
||||
{
|
||||
key: "userOpts",
|
||||
label: "User Options",
|
||||
opts: checker.options?.userOpts || [],
|
||||
},
|
||||
{
|
||||
key: "domainOpts",
|
||||
label: "Domain Options",
|
||||
opts: checker.options?.domainOpts || [],
|
||||
},
|
||||
{
|
||||
key: "serviceOpts",
|
||||
label: "Service Options",
|
||||
opts: checker.options?.serviceOpts || [],
|
||||
},
|
||||
{
|
||||
key: "runOpts",
|
||||
label: "Run Options",
|
||||
opts: checker.options?.runOpts || [],
|
||||
},
|
||||
]}
|
||||
{@const rulesAdminOpts = (checker.rules || []).flatMap(
|
||||
(r) => r.options?.adminOpts || [],
|
||||
)}
|
||||
{@const allAdminOpts = [...adminOpts, ...rulesAdminOpts]}
|
||||
{@const hasAnyOpts =
|
||||
allAdminOpts.length > 0 ||
|
||||
readOnlyOptGroups.some((g) => g.opts.length > 0)}
|
||||
{@const orphanedOpts = getOrphanedOptions(allAdminOpts)}
|
||||
|
||||
{#if orphanedOpts.length > 0}
|
||||
<Alert color="warning" class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
<strong>Orphaned options detected:</strong>
|
||||
{orphanedOpts.join(", ")}
|
||||
</div>
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
onclick={() => cleanOrphanedOptions(allAdminOpts)}
|
||||
disabled={saving}
|
||||
>
|
||||
<Icon name="trash"></Icon>
|
||||
Clean Up
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if adminOpts.length > 0}
|
||||
<Card class="mb-3">
|
||||
<CardHeader
|
||||
class="d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<strong>Admin Options</strong>
|
||||
<Button
|
||||
form="adminoptsform"
|
||||
color="success"
|
||||
size="sm"
|
||||
onclick={saveOptions}
|
||||
disabled={saving}
|
||||
>
|
||||
{#if saving}
|
||||
<span class="spinner-border spinner-border-sm me-1"
|
||||
></span>
|
||||
{:else}
|
||||
<Icon name="check-circle"></Icon>
|
||||
{/if}
|
||||
Save
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Form id="adminoptsform" onsubmit={saveOptions}>
|
||||
{#each adminOpts as optDoc, index}
|
||||
{#if optDoc.id}
|
||||
<ResourceInput
|
||||
edit
|
||||
index={"" + index}
|
||||
specs={optDoc}
|
||||
type={optDoc.type || "string"}
|
||||
bind:value={optionValues[optDoc.id]}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{#each readOnlyOptGroups.filter((g) => g.opts.length > 0) as group}
|
||||
<Card class="mb-3">
|
||||
<CardHeader>
|
||||
<strong>{group.label}</strong>
|
||||
<Badge color="secondary" class="ms-2">read-only</Badge>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<dl class="row mb-0">
|
||||
{#each group.opts as opt}
|
||||
<dt class="col-sm-4">{opt.label || opt.id}</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="text-muted small"
|
||||
>{opt.type || "string"}</span
|
||||
>
|
||||
{#if opt.description}
|
||||
<div class="form-text">{opt.description}</div>
|
||||
{/if}
|
||||
</dd>
|
||||
{/each}
|
||||
</dl>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/each}
|
||||
|
||||
{#if !hasAnyOpts}
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Alert color="info" class="mb-0">
|
||||
<Icon name="info-circle"></Icon>
|
||||
This checker has no configurable options.
|
||||
</Alert>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Alert color="danger" class="mb-0">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
Error loading options: {error.message}
|
||||
</Alert>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/await}
|
||||
</Col>
|
||||
</Row>
|
||||
{:else}
|
||||
<Alert color="danger">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
Error: checker data not found
|
||||
</Alert>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<Alert color="danger">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
Error loading checker: {error.message}
|
||||
</Alert>
|
||||
{/await}
|
||||
</Container>
|
||||
262
web-admin/src/routes/scheduler/+page.svelte
Normal file
262
web-admin/src/routes/scheduler/+page.svelte
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
<!--
|
||||
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 { onMount } from "svelte";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
Row,
|
||||
Spinner,
|
||||
Table,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import {
|
||||
getScheduler,
|
||||
postSchedulerEnable,
|
||||
postSchedulerDisable,
|
||||
postSchedulerRescheduleUpcoming,
|
||||
} from "$lib/api-admin";
|
||||
import type { CheckerSchedulerStatus } from "$lib/api-admin";
|
||||
import { formatDuration, formatRelative } from "$lib/utils/datetime";
|
||||
|
||||
let status = $state<CheckerSchedulerStatus | null>(null);
|
||||
let loading = $state(true);
|
||||
let toggling = $state(false);
|
||||
let rescheduling = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
async function fetchStatus() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const { data, error: err } = await getScheduler();
|
||||
if (err) throw new Error(String(err));
|
||||
status = data ?? null;
|
||||
} catch (e: any) {
|
||||
error = e.message ?? "Unknown error";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleScheduler() {
|
||||
if (!status) return;
|
||||
toggling = true;
|
||||
error = null;
|
||||
try {
|
||||
const fn = status.running ? postSchedulerDisable : postSchedulerEnable;
|
||||
const { data, error: err } = await fn();
|
||||
if (err) throw new Error(String(err));
|
||||
status = data ?? null;
|
||||
} catch (e: any) {
|
||||
error = e.message ?? "Unknown error";
|
||||
} finally {
|
||||
toggling = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildQueue() {
|
||||
rescheduling = true;
|
||||
error = null;
|
||||
try {
|
||||
const { error: err } = await postSchedulerRescheduleUpcoming();
|
||||
if (err) throw new Error(String(err));
|
||||
await fetchStatus();
|
||||
} catch (e: any) {
|
||||
error = e.message ?? "Unknown error";
|
||||
} finally {
|
||||
rescheduling = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(fetchStatus);
|
||||
</script>
|
||||
|
||||
<Container class="flex-fill my-5">
|
||||
<Row class="mb-4">
|
||||
<Col>
|
||||
<h1 class="display-5">
|
||||
<Icon name="clock-history"></Icon>
|
||||
Scheduler
|
||||
</h1>
|
||||
<p class="text-muted lead">Monitor and control the checker scheduler</p>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{#if error}
|
||||
<Card color="danger" body class="mb-4">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
{error}
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<Spinner size="sm" />
|
||||
<span>Loading scheduler status...</span>
|
||||
</div>
|
||||
{:else if status}
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<Icon name="info-circle-fill"></Icon>
|
||||
Scheduler Status
|
||||
</span>
|
||||
<div class="d-flex gap-2">
|
||||
<Button size="sm" color="secondary" outline onclick={fetchStatus}>
|
||||
<Icon name="arrow-clockwise"></Icon> Refresh
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color={status.running ? "warning" : "success"}
|
||||
disabled={toggling}
|
||||
onclick={toggleScheduler}
|
||||
>
|
||||
{#if toggling}
|
||||
<Spinner size="sm" />
|
||||
{:else if status.running}
|
||||
<Icon name="stop-fill"></Icon> Stop
|
||||
{:else}
|
||||
<Icon name="play-fill"></Icon> Start
|
||||
{/if}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
outline
|
||||
disabled={rescheduling}
|
||||
onclick={rebuildQueue}
|
||||
>
|
||||
{#if rescheduling}
|
||||
<Spinner size="sm" />
|
||||
{:else}
|
||||
<Icon name="calendar2-check"></Icon> Rebuild queue
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="d-flex gap-4 align-items-center">
|
||||
<div>
|
||||
<small class="text-muted d-block">Status</small>
|
||||
{#if status.running}
|
||||
<Badge color="success"><Icon name="play-fill"></Icon> Running</Badge>
|
||||
{:else}
|
||||
<Badge color="secondary"><Icon name="stop-fill"></Icon> Stopped</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<small class="text-muted d-block">Jobs in queue</small>
|
||||
<strong>{status.job_count ?? 0}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon name="list-ol"></Icon>
|
||||
Next scheduled jobs
|
||||
<Badge color="secondary" class="ms-2">{status.next_jobs?.length ?? 0}</Badge>
|
||||
</CardHeader>
|
||||
<CardBody class="p-0">
|
||||
<div class="table-responsive">
|
||||
<Table hover class="mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Checker</th>
|
||||
<th>Target</th>
|
||||
<th>Interval</th>
|
||||
<th>Next run</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if !status.next_jobs || status.next_jobs.length === 0}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-3">
|
||||
No jobs scheduled
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each status.next_jobs as job}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{job.checkerID ?? "—"}</code>
|
||||
</td>
|
||||
<td>
|
||||
{#if job.target?.domainId}
|
||||
<Badge
|
||||
href={"/domains/" + job.target?.domainId}
|
||||
color="info"
|
||||
class="me-1"
|
||||
>
|
||||
domain
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if job.target?.serviceId}
|
||||
<Badge
|
||||
href={"/service/" + job.target?.serviceId}
|
||||
color="warning"
|
||||
class="me-1"
|
||||
>
|
||||
service
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if job.target?.userId}
|
||||
<Badge
|
||||
href={"/users/" + job.target?.userId}
|
||||
color="secondary"
|
||||
class="me-1"
|
||||
>
|
||||
user
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if !job.target?.domainId && !job.target?.serviceId && !job.target?.userId}
|
||||
<span class="text-muted">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>{formatDuration(job.interval)}</td>
|
||||
<td>
|
||||
<span title={job.nextRun}
|
||||
>{formatRelative(job.nextRun)}</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
</Container>
|
||||
|
|
@ -54,6 +54,7 @@ import {
|
|||
import type {
|
||||
HappydnsCheckEvaluation,
|
||||
HappydnsCheckPlan,
|
||||
HappydnsCheckPlanWritable,
|
||||
HappydnsCheckerDefinition,
|
||||
HappydnsCheckerOptions,
|
||||
HappydnsCheckerOptionsPositional,
|
||||
|
|
@ -85,7 +86,7 @@ export async function updateCheckOptions(
|
|||
options: HappydnsCheckerOptions,
|
||||
): Promise<HappydnsCheckerOptions> {
|
||||
return unwrapSdkResponse(
|
||||
await putCheckersByCheckerIdOptions({ path: { checkerId }, body: options as any }),
|
||||
await putCheckersByCheckerIdOptions({ path: { checkerId }, body: options }),
|
||||
) as HappydnsCheckerOptions;
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +94,7 @@ export async function updateCheckOptions(
|
|||
|
||||
export async function listDomainCheckers(domain: string): Promise<HappydnsCheckerStatus[]> {
|
||||
return (unwrapSdkResponse(
|
||||
await getDomainsByDomainCheckers({ path: { domain } } as any),
|
||||
await getDomainsByDomainCheckers({ path: { domain } }),
|
||||
) as HappydnsCheckerStatus[]) ?? [];
|
||||
}
|
||||
|
||||
|
|
@ -106,10 +107,10 @@ export async function listDomainExecutions(
|
|||
await getDomainsByDomainCheckersByCheckerIdExecutions({
|
||||
path: { domain, checkerId },
|
||||
query: {
|
||||
...(options?.includePlanned ? { include_planned: "true" } : {}),
|
||||
...(options?.limit ? { limit: String(options.limit) } : {}),
|
||||
...(options?.includePlanned ? { include_planned: true } : {}),
|
||||
...(options?.limit ? { limit: options.limit } : {}),
|
||||
},
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsExecution[]) ?? [];
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +123,7 @@ export async function triggerDomainCheck(
|
|||
await postDomainsByDomainCheckersByCheckerIdExecutions({
|
||||
path: { domain, checkerId },
|
||||
body: request,
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsExecution;
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +135,7 @@ export async function getDomainExecution(
|
|||
return unwrapSdkResponse(
|
||||
await getDomainsByDomainCheckersByCheckerIdExecutionsByExecutionId({
|
||||
path: { domain, checkerId, executionId },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsExecution;
|
||||
}
|
||||
|
||||
|
|
@ -146,7 +147,7 @@ export async function deleteDomainExecution(
|
|||
return unwrapEmptyResponse(
|
||||
await deleteDomainsByDomainCheckersByCheckerIdExecutionsByExecutionId({
|
||||
path: { domain, checkerId, executionId },
|
||||
} as any),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +158,7 @@ export async function deleteAllDomainExecutions(
|
|||
return unwrapEmptyResponse(
|
||||
await deleteDomainsByDomainCheckersByCheckerIdExecutions({
|
||||
path: { domain, checkerId },
|
||||
} as any),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +170,7 @@ export async function getDomainExecutionResults(
|
|||
return unwrapSdkResponse(
|
||||
await getDomainsByDomainCheckersByCheckerIdExecutionsByExecutionIdResults({
|
||||
path: { domain, checkerId, executionId },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsCheckEvaluation;
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +182,7 @@ export async function getDomainExecutionObservations(
|
|||
return unwrapSdkResponse(
|
||||
await getDomainsByDomainCheckersByCheckerIdExecutionsByExecutionIdObservations({
|
||||
path: { domain, checkerId, executionId },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsObservationSnapshot;
|
||||
}
|
||||
|
||||
|
|
@ -192,7 +193,7 @@ export async function getDomainCheckOptions(
|
|||
return (unwrapSdkResponse(
|
||||
await getDomainsByDomainCheckersByCheckerIdOptions({
|
||||
path: { domain, checkerId },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsCheckerOptionsPositional[]) ?? [];
|
||||
}
|
||||
|
||||
|
|
@ -204,8 +205,8 @@ export async function updateDomainCheckOptions(
|
|||
return unwrapSdkResponse(
|
||||
await putDomainsByDomainCheckersByCheckerIdOptions({
|
||||
path: { domain, checkerId },
|
||||
body: options as any,
|
||||
} as any),
|
||||
body: options,
|
||||
}),
|
||||
) as HappydnsCheckerOptions;
|
||||
}
|
||||
|
||||
|
|
@ -216,20 +217,20 @@ export async function getDomainCheckPlans(
|
|||
return (unwrapSdkResponse(
|
||||
await getDomainsByDomainCheckersByCheckerIdPlans({
|
||||
path: { domain, checkerId },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsCheckPlan[]) ?? [];
|
||||
}
|
||||
|
||||
export async function createDomainCheckPlan(
|
||||
domain: string,
|
||||
checkerId: string,
|
||||
plan: HappydnsCheckPlan,
|
||||
plan: HappydnsCheckPlan | HappydnsCheckPlanWritable,
|
||||
): Promise<HappydnsCheckPlan> {
|
||||
return unwrapSdkResponse(
|
||||
await postDomainsByDomainCheckersByCheckerIdPlans({
|
||||
path: { domain, checkerId },
|
||||
body: plan as any,
|
||||
} as any),
|
||||
body: plan as HappydnsCheckPlanWritable,
|
||||
}),
|
||||
) as HappydnsCheckPlan;
|
||||
}
|
||||
|
||||
|
|
@ -237,13 +238,13 @@ export async function updateDomainCheckPlan(
|
|||
domain: string,
|
||||
checkerId: string,
|
||||
planId: string,
|
||||
plan: HappydnsCheckPlan,
|
||||
plan: HappydnsCheckPlan | HappydnsCheckPlanWritable,
|
||||
): Promise<HappydnsCheckPlan> {
|
||||
return unwrapSdkResponse(
|
||||
await putDomainsByDomainCheckersByCheckerIdPlansByPlanId({
|
||||
path: { domain, checkerId, planId },
|
||||
body: plan as any,
|
||||
} as any),
|
||||
body: plan as HappydnsCheckPlanWritable,
|
||||
}),
|
||||
) as HappydnsCheckPlan;
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +259,7 @@ export async function listServiceCheckers(
|
|||
return (unwrapSdkResponse(
|
||||
await getDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckers({
|
||||
path: { domain, zoneid, subdomain, serviceid },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsCheckerStatus[]) ?? [];
|
||||
}
|
||||
|
||||
|
|
@ -274,10 +275,10 @@ export async function listServiceExecutions(
|
|||
await getDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdExecutions({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId },
|
||||
query: {
|
||||
...(options?.includePlanned ? { include_planned: "true" } : {}),
|
||||
...(options?.limit ? { limit: String(options.limit) } : {}),
|
||||
...(options?.includePlanned ? { include_planned: true } : {}),
|
||||
...(options?.limit ? { limit: options.limit } : {}),
|
||||
},
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsExecution[]) ?? [];
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +294,7 @@ export async function triggerServiceCheck(
|
|||
await postDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdExecutions({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId },
|
||||
body: request,
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsExecution;
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +309,7 @@ export async function getServiceExecution(
|
|||
return unwrapSdkResponse(
|
||||
await getDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdExecutionsByExecutionId({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId, executionId },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsExecution;
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +324,7 @@ export async function deleteServiceExecution(
|
|||
return unwrapEmptyResponse(
|
||||
await deleteDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdExecutionsByExecutionId({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId, executionId },
|
||||
} as any),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +338,7 @@ export async function deleteAllServiceExecutions(
|
|||
return unwrapEmptyResponse(
|
||||
await deleteDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdExecutions({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId },
|
||||
} as any),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -352,7 +353,7 @@ export async function getServiceExecutionResults(
|
|||
return unwrapSdkResponse(
|
||||
await getDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdExecutionsByExecutionIdResults({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId, executionId },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsCheckEvaluation;
|
||||
}
|
||||
|
||||
|
|
@ -367,7 +368,7 @@ export async function getServiceExecutionObservations(
|
|||
return unwrapSdkResponse(
|
||||
await getDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdExecutionsByExecutionIdObservations({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId, executionId },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsObservationSnapshot;
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +382,7 @@ export async function getServiceCheckOptions(
|
|||
return (unwrapSdkResponse(
|
||||
await getDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdOptions({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsCheckerOptionsPositional[]) ?? [];
|
||||
}
|
||||
|
||||
|
|
@ -396,8 +397,8 @@ export async function updateServiceCheckOptions(
|
|||
return unwrapSdkResponse(
|
||||
await putDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdOptions({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId },
|
||||
body: options as any,
|
||||
} as any),
|
||||
body: options,
|
||||
}),
|
||||
) as HappydnsCheckerOptions;
|
||||
}
|
||||
|
||||
|
|
@ -411,7 +412,7 @@ export async function getServiceCheckPlans(
|
|||
return (unwrapSdkResponse(
|
||||
await getDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdPlans({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId },
|
||||
} as any),
|
||||
}),
|
||||
) as HappydnsCheckPlan[]) ?? [];
|
||||
}
|
||||
|
||||
|
|
@ -421,13 +422,13 @@ export async function createServiceCheckPlan(
|
|||
subdomain: string,
|
||||
serviceid: string,
|
||||
checkerId: string,
|
||||
plan: HappydnsCheckPlan,
|
||||
plan: HappydnsCheckPlan | HappydnsCheckPlanWritable,
|
||||
): Promise<HappydnsCheckPlan> {
|
||||
return unwrapSdkResponse(
|
||||
await postDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdPlans({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId },
|
||||
body: plan as any,
|
||||
} as any),
|
||||
body: plan as HappydnsCheckPlanWritable,
|
||||
}),
|
||||
) as HappydnsCheckPlan;
|
||||
}
|
||||
|
||||
|
|
@ -438,13 +439,13 @@ export async function updateServiceCheckPlan(
|
|||
serviceid: string,
|
||||
checkerId: string,
|
||||
planId: string,
|
||||
plan: HappydnsCheckPlan,
|
||||
plan: HappydnsCheckPlan | HappydnsCheckPlanWritable,
|
||||
): Promise<HappydnsCheckPlan> {
|
||||
return unwrapSdkResponse(
|
||||
await putDomainsByDomainZoneByZoneidBySubdomainServicesByServiceidCheckersByCheckerIdPlansByPlanId({
|
||||
path: { domain, zoneid, subdomain, serviceid, checkerId, planId },
|
||||
body: plan as any,
|
||||
} as any),
|
||||
body: plan as HappydnsCheckPlanWritable,
|
||||
}),
|
||||
) as HappydnsCheckPlan;
|
||||
}
|
||||
|
||||
|
|
@ -580,7 +581,7 @@ export async function getScopedCheckPlans(
|
|||
export async function createScopedCheckPlan(
|
||||
scope: CheckerScope,
|
||||
checkerId: string,
|
||||
plan: HappydnsCheckPlan,
|
||||
plan: HappydnsCheckPlan | HappydnsCheckPlanWritable,
|
||||
): Promise<HappydnsCheckPlan> {
|
||||
if (isServiceScope(scope)) {
|
||||
return createServiceCheckPlan(scope.domainId, scope.zoneId, scope.subdomain, scope.serviceId, checkerId, plan);
|
||||
|
|
@ -592,7 +593,7 @@ export async function updateScopedCheckPlan(
|
|||
scope: CheckerScope,
|
||||
checkerId: string,
|
||||
planId: string,
|
||||
plan: HappydnsCheckPlan,
|
||||
plan: HappydnsCheckPlan | HappydnsCheckPlanWritable,
|
||||
): Promise<HappydnsCheckPlan> {
|
||||
if (isServiceScope(scope)) {
|
||||
return updateServiceCheckPlan(scope.domainId, scope.zoneId, scope.subdomain, scope.serviceId, checkerId, planId, plan);
|
||||
|
|
|
|||
|
|
@ -138,6 +138,14 @@
|
|||
>
|
||||
{$t("menu.dns-resolver")}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
active={page.route &&
|
||||
(page.route.id == "/checkers" ||
|
||||
page.route.id?.startsWith("/checkers/"))}
|
||||
href="/checkers"
|
||||
>
|
||||
{$t("menu.checkers")}
|
||||
</DropdownItem>
|
||||
<DropdownItem divider />
|
||||
<DropdownItem active={page.route && page.route.id == "/me"} href="/me">
|
||||
{$t("menu.my-account")}
|
||||
|
|
|
|||
276
web/src/lib/components/checkers/CheckResultSidebar.svelte
Normal file
276
web/src/lib/components/checkers/CheckResultSidebar.svelte
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<!--
|
||||
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">
|
||||
// SvelteKit imports
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
// Component imports
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Icon,
|
||||
Spinner,
|
||||
Table,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
// Store imports
|
||||
import { currentExecution, currentCheckInfo, reportViewMode, showHTMLReport } from "$lib/stores/checkers";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
|
||||
// API imports
|
||||
import {
|
||||
triggerDomainCheck,
|
||||
triggerServiceCheck,
|
||||
} from "$lib/api/checkers";
|
||||
|
||||
// Utility imports
|
||||
import { getStatusColor, getStatusI18nKey, formatCheckDate, downloadBlob } from "$lib/utils";
|
||||
import { formatDuration } from "$lib/utils/datetime";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
// Model imports
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
domain: Domain;
|
||||
checkerId: string;
|
||||
rid: string;
|
||||
checksBase: string;
|
||||
serviceContext?: {
|
||||
zoneId: string;
|
||||
subdomain: string;
|
||||
serviceid: string;
|
||||
};
|
||||
}
|
||||
|
||||
let { domain, checkerId, rid, checksBase, serviceContext }: Props = $props();
|
||||
|
||||
// Local state
|
||||
let isRelaunching = $state(false);
|
||||
|
||||
// Functions
|
||||
async function handleRelaunch() {
|
||||
if (!$currentExecution) return;
|
||||
isRelaunching = true;
|
||||
try {
|
||||
if (serviceContext) {
|
||||
await triggerServiceCheck(
|
||||
domain.id,
|
||||
serviceContext.zoneId,
|
||||
serviceContext.subdomain,
|
||||
serviceContext.serviceid,
|
||||
checkerId,
|
||||
);
|
||||
} else {
|
||||
await triggerDomainCheck(domain.id, checkerId);
|
||||
}
|
||||
navigate(`${checksBase}/${encodeURIComponent(checkerId)}/executions`);
|
||||
} catch (error: any) {
|
||||
toasts.addErrorToast({
|
||||
message: error.message || $t("checkers.result.relaunch-failed"),
|
||||
});
|
||||
} finally {
|
||||
isRelaunching = false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Delete endpoint not yet available for executions
|
||||
async function handleDelete() {
|
||||
toasts.addErrorToast({ message: "Delete not yet available" });
|
||||
}
|
||||
|
||||
// TODO: HTML report download endpoint not yet available in new API
|
||||
async function downloadHTML() {
|
||||
toasts.addErrorToast({ message: "HTML report download not yet available" });
|
||||
}
|
||||
|
||||
function downloadJSON() {
|
||||
if (!$currentExecution) return;
|
||||
downloadBlob(
|
||||
JSON.stringify($currentExecution.result, null, 2),
|
||||
`${checkerId}-${rid}.json`,
|
||||
"application/json",
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $currentExecution}
|
||||
<Card class="mt-3">
|
||||
<CardHeader class="px-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong class="text-truncate">{$currentCheckInfo?.name || checkerId}</strong>
|
||||
{#if $currentExecution.planId}
|
||||
<Badge color="info" class="flex-shrink-0">
|
||||
<Icon name="clock"></Icon>
|
||||
{$t("checkers.result.type.scheduled")}
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge color="secondary" class="flex-shrink-0">
|
||||
<Icon name="hand-index"></Icon>
|
||||
{$t("checkers.result.type.manual")}
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div class="overflow-x-auto rounded-2">
|
||||
<Table borderless size="sm" class="mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width: 80px; white-space: nowrap">
|
||||
{$t("checkers.result.field.executed-at")}
|
||||
</th>
|
||||
<td>{formatCheckDate($currentExecution.startedAt)}</td>
|
||||
</tr>
|
||||
{#if $currentExecution.endedAt && $currentExecution.startedAt}
|
||||
<tr>
|
||||
<th>{$t("checkers.result.field.duration")}</th>
|
||||
<td>{formatDuration(($currentExecution.endedAt.getTime() - $currentExecution.startedAt.getTime()) * 1e6)}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if $currentExecution.result}
|
||||
<tr>
|
||||
<th>{$t("checkers.result.field.status")}</th>
|
||||
<td>
|
||||
<Badge color={getStatusColor($currentExecution.result.status)}>
|
||||
{$t(getStatusI18nKey($currentExecution.result.status))}
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
{#if $currentExecution.result.message}
|
||||
<tr>
|
||||
<th>{$t("checkers.result.field.status-message")}</th>
|
||||
<td class="text-truncate" style="max-width: 0">
|
||||
{$currentExecution.result.message}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if $currentExecution.error}
|
||||
<tr>
|
||||
<th>{$t("checkers.result.field.error")}</th>
|
||||
<td class="text-danger text-truncate" style="max-width: 0">
|
||||
{$currentExecution.error}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div class="my-3 flex-fill"></div>
|
||||
|
||||
{#if $currentCheckInfo?.has_html_report || $currentCheckInfo?.has_metrics || $currentExecution.result != null}
|
||||
{#if $currentCheckInfo?.has_metrics || $currentCheckInfo?.has_html_report}
|
||||
<ButtonGroup class="w-100 mb-2">
|
||||
{#if $currentCheckInfo?.has_metrics}
|
||||
<Button
|
||||
size="sm"
|
||||
color="secondary"
|
||||
outline
|
||||
active={$reportViewMode === "metrics"}
|
||||
onclick={() => {
|
||||
reportViewMode.set("metrics");
|
||||
showHTMLReport.set(false);
|
||||
}}
|
||||
>
|
||||
<Icon name="graph-up"></Icon>
|
||||
{$t("checkers.result.view-metrics")}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if $currentCheckInfo?.has_html_report}
|
||||
<Button
|
||||
size="sm"
|
||||
color="secondary"
|
||||
outline
|
||||
active={$reportViewMode === "html" ||
|
||||
(!$currentCheckInfo?.has_metrics && $showHTMLReport)}
|
||||
onclick={() => {
|
||||
reportViewMode.set("html");
|
||||
showHTMLReport.set(true);
|
||||
}}
|
||||
>
|
||||
<Icon name="file-earmark-richtext"></Icon>
|
||||
{$t("checkers.result.view-html")}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
size="sm"
|
||||
color="secondary"
|
||||
outline
|
||||
active={$reportViewMode === "json" ||
|
||||
(!$currentCheckInfo?.has_metrics && !$currentCheckInfo?.has_html_report)}
|
||||
onclick={() => {
|
||||
reportViewMode.set("json");
|
||||
showHTMLReport.set(false);
|
||||
}}
|
||||
>
|
||||
<Icon name="braces"></Icon>
|
||||
{$t("checkers.result.view-json")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{/if}
|
||||
<ButtonGroup class="w-100">
|
||||
{#if $currentCheckInfo?.has_html_report}
|
||||
<Button size="sm" color="outline-secondary" onclick={downloadHTML}>
|
||||
<Icon name="download"></Icon>
|
||||
{$t("checkers.result.download-html")}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if $currentExecution.result != null}
|
||||
<Button size="sm" color="outline-secondary" onclick={downloadJSON}>
|
||||
<Icon name="download"></Icon>
|
||||
{$t("checkers.result.download-json")}
|
||||
</Button>
|
||||
{/if}
|
||||
</ButtonGroup>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex-fill"></div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 d-flex gap-2">
|
||||
<Button
|
||||
class="flex-fill"
|
||||
color="primary"
|
||||
outline
|
||||
onclick={handleRelaunch}
|
||||
disabled={!$currentExecution || isRelaunching}
|
||||
>
|
||||
{#if isRelaunching}
|
||||
<Spinner size="sm" />
|
||||
{:else}
|
||||
<Icon name="arrow-repeat"></Icon>
|
||||
{/if}
|
||||
{$t("checkers.result.relaunch")}
|
||||
</Button>
|
||||
<Button color="danger" outline onclick={handleDelete} disabled={!$currentExecution}>
|
||||
<Icon name="trash"></Icon>
|
||||
</Button>
|
||||
</div>
|
||||
187
web/src/lib/components/checkers/CheckerConfigPage.svelte
Normal file
187
web/src/lib/components/checkers/CheckerConfigPage.svelte
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
<!--
|
||||
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, Button, Card, Col, Icon, Row } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { t } from "$lib/translations";
|
||||
import { checkers } from "$lib/stores/checkers";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
import type {
|
||||
HappydnsCheckPlan,
|
||||
HappydnsCheckPlanWritable,
|
||||
HappydnsCheckerOptionsPositional,
|
||||
} from "$lib/api-base/types.gen";
|
||||
import type { CheckerScope } from "$lib/api/checkers";
|
||||
import {
|
||||
getCheckStatus,
|
||||
getScopedCheckOptions,
|
||||
updateScopedCheckOptions,
|
||||
} from "$lib/api/checkers";
|
||||
import PageTitle from "$lib/components/PageTitle.svelte";
|
||||
import CheckerScheduleCard from "./CheckerScheduleCard.svelte";
|
||||
import CheckerRulesCard from "./CheckerRulesCard.svelte";
|
||||
import CheckerOptionsPanel from "./CheckerOptionsPanel.svelte";
|
||||
|
||||
interface Props {
|
||||
scope: CheckerScope;
|
||||
checksBase: string;
|
||||
checkerId: string;
|
||||
domainName: string;
|
||||
editableGroups: (status: any) => { label: string; opts: any[] }[];
|
||||
readOnlyGroups: (status: any) => { key: string; label: string; opts: any[] }[];
|
||||
}
|
||||
|
||||
let { scope, checksBase, checkerId, domainName, editableGroups, readOnlyGroups }: Props = $props();
|
||||
|
||||
let checkStatusPromise = $derived(getCheckStatus(checkerId));
|
||||
let checkOptionsPromise = $derived(getScopedCheckOptions(scope, checkerId));
|
||||
|
||||
let resolvedStatus = $state<any>(null);
|
||||
let optionValues = $state<Record<string, unknown>>({});
|
||||
let inheritedValues = $state<Record<string, unknown>>({});
|
||||
let savingOptions = $state(false);
|
||||
|
||||
let plan = $state<HappydnsCheckPlanWritable>({
|
||||
enabled: {},
|
||||
interval: 3600,
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
checkStatusPromise.then((status) => {
|
||||
resolvedStatus = status;
|
||||
if (status?.rules && Object.keys(plan.enabled ?? {}).length === 0) {
|
||||
const enabled: Record<string, boolean> = {};
|
||||
for (const rule of status.rules) {
|
||||
if (rule.name) enabled[rule.name] = true;
|
||||
}
|
||||
plan.enabled = enabled;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
checkOptionsPromise.then((positionals: HappydnsCheckerOptionsPositional[]) => {
|
||||
const current =
|
||||
positionals.length > 0 ? (positionals[positionals.length - 1]?.options ?? {}) : {};
|
||||
const inherited: Record<string, unknown> = {};
|
||||
for (let i = 0; i < positionals.length - 1; i++) {
|
||||
for (const [k, v] of Object.entries(positionals[i].options ?? {})) {
|
||||
inherited[k] = v;
|
||||
}
|
||||
}
|
||||
optionValues = { ...current };
|
||||
inheritedValues = inherited;
|
||||
});
|
||||
});
|
||||
|
||||
async function saveOptions() {
|
||||
savingOptions = true;
|
||||
try {
|
||||
await updateScopedCheckOptions(scope, checkerId, optionValues);
|
||||
checkOptionsPromise = getScopedCheckOptions(scope, checkerId);
|
||||
toasts.addToast({
|
||||
message: $t("checkers.messages.options-updated"),
|
||||
type: "success",
|
||||
timeout: 5000,
|
||||
});
|
||||
} catch (error) {
|
||||
toasts.addErrorToast({
|
||||
message: $t("checkers.messages.update-failed", { error: String(error) }),
|
||||
timeout: 10000,
|
||||
});
|
||||
} finally {
|
||||
savingOptions = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{resolvedStatus?.name ?? checkerId} - {domainName} - happyDomain</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex-fill mt-1 mb-5">
|
||||
<PageTitle title={resolvedStatus?.name ?? checkerId} domain={domainName}>
|
||||
{#if $checkers && (!$checkers[checkerId]?.availability || $checkers[checkerId].availability.applyToDomain || $checkers[checkerId].availability.applyToZone)}
|
||||
<Button
|
||||
color="info"
|
||||
href={`${checksBase}/${encodeURIComponent(checkerId)}/executions`}
|
||||
>
|
||||
<Icon name="bar-chart-fill"></Icon>
|
||||
{$t("checkers.list.view-results")}
|
||||
</Button>
|
||||
{/if}
|
||||
</PageTitle>
|
||||
|
||||
{#await checkStatusPromise}
|
||||
<Card body>
|
||||
<p class="text-center mb-0">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
{$t("checkers.loading-info")}
|
||||
</p>
|
||||
</Card>
|
||||
{:then status}
|
||||
{#if status}
|
||||
{@const editable = editableGroups(status)}
|
||||
{@const readOnly = readOnlyGroups(status)}
|
||||
<Row class="mb-4">
|
||||
<Col md={6}>
|
||||
<CheckerScheduleCard {scope} {checkerId} bind:plan />
|
||||
|
||||
{#if status.rules && status.rules.length > 0}
|
||||
<CheckerRulesCard
|
||||
rules={status.rules}
|
||||
bind:optionValues
|
||||
{inheritedValues}
|
||||
saving={savingOptions}
|
||||
onsave={saveOptions}
|
||||
bind:plan
|
||||
/>
|
||||
{/if}
|
||||
</Col>
|
||||
|
||||
<Col md={6}>
|
||||
<CheckerOptionsPanel
|
||||
{checkOptionsPromise}
|
||||
editableGroups={editable}
|
||||
readOnlyGroups={readOnly}
|
||||
bind:optionValues
|
||||
{inheritedValues}
|
||||
saving={savingOptions}
|
||||
onsave={saveOptions}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{:else}
|
||||
<Alert color="danger">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
{$t("checkers.checker-info-not-found")}
|
||||
</Alert>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<Alert color="danger">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
{$t("checkers.error-loading-checker", { error: error.message })}
|
||||
</Alert>
|
||||
{/await}
|
||||
</div>
|
||||
181
web/src/lib/components/checkers/CheckerListPage.svelte
Normal file
181
web/src/lib/components/checkers/CheckerListPage.svelte
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<!--
|
||||
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,
|
||||
Badge,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Icon,
|
||||
Table,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { t } from "$lib/translations";
|
||||
import type { CheckerScope } from "$lib/api/checkers";
|
||||
import { listScopedCheckers } from "$lib/api/checkers";
|
||||
import { checkers } from "$lib/stores/checkers";
|
||||
import type { HappydnsCheckerStatus } from "$lib/api-base/types.gen";
|
||||
import { getStatusColor, getStatusI18nKey, formatCheckDate } from "$lib/utils";
|
||||
import CheckersAvailabilityTable from "./CheckersAvailabilityTable.svelte";
|
||||
import PageTitle from "$lib/components/PageTitle.svelte";
|
||||
|
||||
interface Props {
|
||||
scope: CheckerScope;
|
||||
checksBase: string;
|
||||
title: string;
|
||||
domainName: string;
|
||||
filterAvailability: "applyToDomain" | "applyToService";
|
||||
}
|
||||
|
||||
let { scope, checksBase, title, domainName, filterAvailability }: Props = $props();
|
||||
|
||||
let checkersPromise = $derived(listScopedCheckers(scope));
|
||||
|
||||
function getConfiguredCheckerIds(statuses: HappydnsCheckerStatus[]): Set<string> {
|
||||
return new Set(statuses.map((s) => s.id).filter((id): id is string => !!id));
|
||||
}
|
||||
|
||||
function getUnconfiguredCheckers(configuredIds: Set<string>): [string, any][] {
|
||||
if (!$checkers) return [];
|
||||
return Object.entries($checkers).filter(
|
||||
([id, def]) => !configuredIds.has(id) && def.availability?.[filterAvailability],
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t("checkers.list.title")}{domainName} - happyDomain</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex-fill mt-1 mb-5">
|
||||
<PageTitle title={title} domain={domainName}></PageTitle>
|
||||
|
||||
{#await checkersPromise}
|
||||
<Card body>
|
||||
<p class="text-center mb-0">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
{$t("checkers.list.loading")}
|
||||
</p>
|
||||
</Card>
|
||||
{:then checkerStatuses}
|
||||
{#if checkerStatuses.length > 0}
|
||||
<div class="table-responsive">
|
||||
<Table hover class="mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$t("checkers.list.table.checker")}</th>
|
||||
<th>{$t("checkers.list.table.status")}</th>
|
||||
<th>{$t("checkers.list.table.last-run")}</th>
|
||||
<th>{$t("checkers.list.table.schedule")}</th>
|
||||
<th>{$t("checkers.list.table.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each checkerStatuses as checker}
|
||||
{@const status = checker.latestExecution?.result?.status}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{checker.name || checker.id}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{#if checker.latestExecution}
|
||||
<Badge color={getStatusColor(status)}>
|
||||
{$t(getStatusI18nKey(status))}
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge color="secondary">
|
||||
{$t("checkers.status.not-run")}
|
||||
</Badge>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if checker.latestExecution?.startedAt}
|
||||
{formatCheckDate(checker.latestExecution.startedAt)}
|
||||
{:else}
|
||||
{$t("checkers.never")}
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if checker.enabled}
|
||||
<Badge color="success">
|
||||
{$t("checkers.list.schedule.enabled")}
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge color="secondary">
|
||||
{$t("checkers.list.schedule.disabled")}
|
||||
</Badge>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<a
|
||||
href="{checksBase}/{checker.id}"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
>
|
||||
{$t("checkers.list.configure")}
|
||||
</a>
|
||||
<a
|
||||
href="{checksBase}/{checker.id}/executions"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
>
|
||||
{$t("checkers.list.view-results")}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
{:else}
|
||||
<Alert color="info" class="mb-4">
|
||||
<Icon name="info-circle" />
|
||||
{$t("checkers.list.no-checks")}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{@const configuredIds = getConfiguredCheckerIds(checkerStatuses)}
|
||||
{@const unconfigured = getUnconfiguredCheckers(configuredIds)}
|
||||
{#if unconfigured.length > 0}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<strong>{$t("checkers.other-checkers.title")}</strong>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<p class="text-muted">{$t("checkers.other-checkers.description")}</p>
|
||||
<CheckersAvailabilityTable
|
||||
checkers={unconfigured}
|
||||
basePath={checksBase}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<Alert color="danger">
|
||||
<Icon name="exclamation-triangle-fill" />
|
||||
{$t("checkers.list.error-loading", { error: error.message })}
|
||||
</Alert>
|
||||
{/await}
|
||||
</div>
|
||||
70
web/src/lib/components/checkers/CheckerOptionsGroups.svelte
Normal file
70
web/src/lib/components/checkers/CheckerOptionsGroups.svelte
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<!--
|
||||
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 { Badge, Card, CardBody, CardHeader } from "@sveltestrap/sveltestrap";
|
||||
import type { HappydnsCheckerOptionDocumentation } from "$lib/api-base/types.gen";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
let {
|
||||
groups,
|
||||
}: {
|
||||
groups: { key: string; label: string; opts: HappydnsCheckerOptionDocumentation[] }[];
|
||||
} = $props();
|
||||
|
||||
function autoFillLabel(key: string): string {
|
||||
const knownKeys: Record<string, string> = {
|
||||
domain_name: $t("checkers.auto-fill.domain_name"),
|
||||
subdomain: $t("checkers.auto-fill.subdomain"),
|
||||
service_type: $t("checkers.auto-fill.service_type"),
|
||||
};
|
||||
return knownKeys[key] || $t("checkers.auto-fill.generic", { key });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each groups.filter((g) => g.opts.length > 0) as group}
|
||||
<Card class="mb-3">
|
||||
<CardHeader>
|
||||
<strong>{group.label}</strong>
|
||||
<Badge color="secondary" class="ms-2">{$t("checkers.detail.read-only")}</Badge>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<dl class="row mb-0">
|
||||
{#each group.opts as opt}
|
||||
<dt class="col-sm-4">
|
||||
{opt.label || opt.id}
|
||||
{#if opt.autoFill}
|
||||
<Badge color="info" class="ms-1">{autoFillLabel(opt.autoFill)}</Badge>
|
||||
{/if}
|
||||
</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="text-muted small">{opt.type || "string"}</span>
|
||||
{#if opt.description}
|
||||
<div class="form-text">{opt.description}</div>
|
||||
{/if}
|
||||
</dd>
|
||||
{/each}
|
||||
</dl>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/each}
|
||||
197
web/src/lib/components/checkers/CheckerOptionsPanel.svelte
Normal file
197
web/src/lib/components/checkers/CheckerOptionsPanel.svelte
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<!--
|
||||
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,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Form,
|
||||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import type {
|
||||
HappydnsCheckerOptionDocumentation,
|
||||
HappydnsCheckerOptionsPositional,
|
||||
} from "$lib/api-base/types.gen";
|
||||
import { t } from "$lib/translations";
|
||||
import { withInheritedPlaceholders } from "$lib/utils/checkers";
|
||||
import ResourceInput from "$lib/components/inputs/Resource.svelte";
|
||||
import CheckerOptionsGroups from "./CheckerOptionsGroups.svelte";
|
||||
|
||||
interface EditableGroup {
|
||||
label: string;
|
||||
opts: HappydnsCheckerOptionDocumentation[];
|
||||
}
|
||||
|
||||
interface ReadOnlyGroup {
|
||||
key: string;
|
||||
label: string;
|
||||
opts: HappydnsCheckerOptionDocumentation[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
checkOptionsPromise: Promise<HappydnsCheckerOptionsPositional[]>;
|
||||
editableGroups: EditableGroup[];
|
||||
readOnlyGroups: ReadOnlyGroup[];
|
||||
optionValues: Record<string, unknown>;
|
||||
inheritedValues: Record<string, unknown>;
|
||||
saving: boolean;
|
||||
onsave: () => void;
|
||||
orphanedOpts?: string[];
|
||||
onclean?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
checkOptionsPromise,
|
||||
editableGroups,
|
||||
readOnlyGroups,
|
||||
optionValues = $bindable(),
|
||||
inheritedValues,
|
||||
saving,
|
||||
onsave,
|
||||
orphanedOpts = [],
|
||||
onclean,
|
||||
}: Props = $props();
|
||||
|
||||
// Filter out auto-fill fields from editable groups (they are system-provided).
|
||||
let filteredEditableGroups = $derived(
|
||||
editableGroups.map((g) => ({
|
||||
...g,
|
||||
opts: g.opts.filter((opt) => !opt.autoFill),
|
||||
})),
|
||||
);
|
||||
|
||||
// Collect auto-fill fields into read-only groups for display.
|
||||
let autoFillOpts = $derived(
|
||||
editableGroups.flatMap((g) => g.opts.filter((opt) => opt.autoFill)),
|
||||
);
|
||||
|
||||
let hasAnyOpts = $derived(
|
||||
filteredEditableGroups.some((g) => g.opts.length > 0) ||
|
||||
readOnlyGroups.some((g) => g.opts.length > 0) ||
|
||||
autoFillOpts.length > 0,
|
||||
);
|
||||
</script>
|
||||
|
||||
{#await checkOptionsPromise}
|
||||
<Card>
|
||||
<CardBody>
|
||||
<p class="text-center mb-0">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
{$t("checkers.detail.loading-options")}
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{:then _options}
|
||||
{#if orphanedOpts.length > 0 && onclean}
|
||||
<Alert color="warning" class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
{$t("checkers.detail.orphaned-options", {
|
||||
options: orphanedOpts.join(", "),
|
||||
})}
|
||||
</div>
|
||||
<Button type="button" color="danger" size="sm" onclick={onclean} disabled={saving}>
|
||||
<Icon name="trash"></Icon>
|
||||
{$t("checkers.detail.clean-up")}
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#each filteredEditableGroups.filter((g) => g.opts.length > 0) as group, gid}
|
||||
<Card class="mb-3">
|
||||
<CardHeader class="d-flex align-items-center justify-content-between">
|
||||
<strong>{group.label}</strong>
|
||||
<Button
|
||||
color="success"
|
||||
form={"group-" + gid}
|
||||
size="sm"
|
||||
onclick={onsave}
|
||||
disabled={saving}
|
||||
>
|
||||
{#if saving}
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||
{:else}
|
||||
<Icon name="check-circle"></Icon>
|
||||
{/if}
|
||||
{$t("checkers.detail.save")}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Form id={"group-" + gid} onsubmit={onsave}>
|
||||
{#each withInheritedPlaceholders(group.opts, optionValues, inheritedValues) as optDoc, index}
|
||||
{#if optDoc.id}
|
||||
<ResourceInput
|
||||
edit
|
||||
index={"" + index}
|
||||
specs={optDoc}
|
||||
type={optDoc.type || "string"}
|
||||
bind:value={optionValues[optDoc.id]}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/each}
|
||||
|
||||
{#if autoFillOpts.length > 0}
|
||||
<CheckerOptionsGroups
|
||||
groups={[
|
||||
{
|
||||
key: "auto-fill",
|
||||
label: $t("checkers.detail.auto-fill"),
|
||||
opts: autoFillOpts,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<CheckerOptionsGroups groups={readOnlyGroups} />
|
||||
|
||||
{#if !hasAnyOpts}
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Alert color="info" class="mb-0">
|
||||
<Icon name="info-circle"></Icon>
|
||||
{$t("checkers.detail.no-configurable-options")}
|
||||
</Alert>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Alert color="danger" class="mb-0">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
{$t("checkers.detail.error-loading-options", {
|
||||
error: error.message,
|
||||
})}
|
||||
</Alert>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/await}
|
||||
165
web/src/lib/components/checkers/CheckerRulesCard.svelte
Normal file
165
web/src/lib/components/checkers/CheckerRulesCard.svelte
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<!--
|
||||
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 {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
Form,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import type { HappydnsCheckPlan, HappydnsCheckPlanWritable, HappydnsCheckRuleInfo } from "$lib/api-base/types.gen";
|
||||
import { t } from "$lib/translations";
|
||||
import { withInheritedPlaceholders } from "$lib/utils/checkers";
|
||||
import ResourceInput from "$lib/components/inputs/Resource.svelte";
|
||||
|
||||
interface Props {
|
||||
rules: HappydnsCheckRuleInfo[];
|
||||
optionValues: Record<string, unknown>;
|
||||
inheritedValues: Record<string, unknown>;
|
||||
saving: boolean;
|
||||
onsave: () => void;
|
||||
plan?: HappydnsCheckPlan | HappydnsCheckPlanWritable;
|
||||
}
|
||||
|
||||
let { rules, optionValues = $bindable(), inheritedValues, saving, onsave, plan = $bindable() }: Props = $props();
|
||||
|
||||
let hasRuleOpts = $derived(
|
||||
rules.some((r) => (r.options?.adminOpts?.length ?? 0) + (r.options?.userOpts?.length ?? 0) > 0),
|
||||
);
|
||||
|
||||
let allEnabled = $derived(
|
||||
plan && rules.length > 0 && rules.every((r) => r.name && plan!.enabled?.[r.name]),
|
||||
);
|
||||
|
||||
function toggleAll() {
|
||||
if (!plan) return;
|
||||
const newVal = !allEnabled;
|
||||
const enabled: Record<string, boolean> = {};
|
||||
for (const rule of rules) {
|
||||
if (rule.name) enabled[rule.name] = newVal;
|
||||
}
|
||||
plan.enabled = enabled;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<strong>{$t("checkers.detail.check-rules")}</strong>
|
||||
<Badge color="secondary" class="ms-2">{rules.length}</Badge>
|
||||
</div>
|
||||
{#if plan}
|
||||
<div class="d-flex gap-2">
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
checked={allEnabled}
|
||||
onchange={toggleAll}
|
||||
id="toggle-all-rules"
|
||||
/>
|
||||
<label class="form-check-label" for="toggle-all-rules">
|
||||
All
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{:else if hasRuleOpts}
|
||||
<Button
|
||||
type="button"
|
||||
color="success"
|
||||
size="sm"
|
||||
onclick={onsave}
|
||||
disabled={saving}
|
||||
>
|
||||
{#if saving}
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||
{:else}
|
||||
<Icon name="check-circle"></Icon>
|
||||
{/if}
|
||||
{$t("checkers.detail.save")}
|
||||
</Button>
|
||||
{/if}
|
||||
</CardHeader>
|
||||
<ListGroup flush>
|
||||
{#each rules as rule}
|
||||
{@const ruleOpts = [
|
||||
...(rule.options?.adminOpts || []),
|
||||
...(rule.options?.userOpts || []),
|
||||
]}
|
||||
<ListGroupItem>
|
||||
<div class="d-flex align-items-start gap-2 mb-1">
|
||||
{#if plan}
|
||||
<div class="form-check form-switch mt-1">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
checked={plan.enabled?.[rule.name ?? ""] ?? false}
|
||||
onchange={() => {
|
||||
if (rule.name && plan) {
|
||||
plan.enabled = {
|
||||
...plan.enabled,
|
||||
[rule.name]: !(plan.enabled?.[rule.name] ?? false),
|
||||
};
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<Icon
|
||||
name="check2-circle"
|
||||
class="text-success mt-1 flex-shrink-0"
|
||||
></Icon>
|
||||
{/if}
|
||||
<div class="flex-grow-1">
|
||||
<strong>{rule.name}</strong>
|
||||
{#if rule.description}
|
||||
<p class="text-muted small mb-0">{rule.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if ruleOpts.length > 0}
|
||||
<div class="ms-4 mt-2">
|
||||
<Form onsubmit={onsave}>
|
||||
{#each withInheritedPlaceholders(ruleOpts, optionValues, inheritedValues) as optDoc, index}
|
||||
{#if optDoc.id}
|
||||
<ResourceInput
|
||||
edit
|
||||
index={"" + index}
|
||||
specs={optDoc}
|
||||
type={optDoc.type || "string"}
|
||||
bind:value={optionValues[optDoc.id]}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</Form>
|
||||
</div>
|
||||
{/if}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
147
web/src/lib/components/checkers/CheckerScheduleCard.svelte
Normal file
147
web/src/lib/components/checkers/CheckerScheduleCard.svelte
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
<!--
|
||||
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,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
FormGroup,
|
||||
Icon,
|
||||
Input,
|
||||
Label,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import type { 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";
|
||||
import {
|
||||
getScopedCheckPlans,
|
||||
createScopedCheckPlan,
|
||||
updateScopedCheckPlan,
|
||||
} from "$lib/api/checkers";
|
||||
|
||||
interface Props {
|
||||
scope: CheckerScope;
|
||||
checkerId: string;
|
||||
plan: HappydnsCheckPlan | HappydnsCheckPlanWritable;
|
||||
}
|
||||
|
||||
let { scope, checkerId, plan = $bindable() }: Props = $props();
|
||||
|
||||
let existingPlanId = $state<string | undefined>(undefined);
|
||||
let saving = $state(false);
|
||||
|
||||
let schedulesPromise = $derived(getScopedCheckPlans(scope, checkerId));
|
||||
|
||||
$effect(() => {
|
||||
schedulesPromise.then((schedules: HappydnsCheckPlan[]) => {
|
||||
if (schedules.length > 0) {
|
||||
const s = schedules[0];
|
||||
existingPlanId = s.id;
|
||||
plan = {
|
||||
enabled: s.enabled ?? {},
|
||||
interval: s.interval ?? 3600,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function save() {
|
||||
saving = true;
|
||||
try {
|
||||
const planData: HappydnsCheckPlanWritable = {
|
||||
enabled: plan.enabled,
|
||||
interval: plan.interval,
|
||||
};
|
||||
if (existingPlanId) {
|
||||
await updateScopedCheckPlan(scope, checkerId, existingPlanId, planData);
|
||||
} else {
|
||||
const created = await createScopedCheckPlan(scope, checkerId, planData);
|
||||
existingPlanId = created.id;
|
||||
}
|
||||
toasts.addToast({
|
||||
message: $t("checkers.schedule.saved"),
|
||||
type: "success",
|
||||
timeout: 5000,
|
||||
});
|
||||
} catch (error) {
|
||||
toasts.addErrorToast({
|
||||
message: $t("checkers.schedule.save-failed") + ": " + String(error),
|
||||
timeout: 10000,
|
||||
});
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function intervalHours(): number {
|
||||
return Math.round((plan.interval ?? 3600) / 3600);
|
||||
}
|
||||
|
||||
function setIntervalHours(hours: number) {
|
||||
plan.interval = Math.max(3600, hours * 3600);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="mb-3">
|
||||
<CardHeader class="d-flex align-items-center justify-content-between">
|
||||
<strong>{$t("checkers.schedule.card-title")}</strong>
|
||||
<Button form="form-schedule" color="success" size="sm" onclick={save} disabled={saving}>
|
||||
{#if saving}
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||
{:else}
|
||||
<Icon name="check-circle"></Icon>
|
||||
{/if}
|
||||
{$t("checkers.schedule.save")}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<form id="form-schedule">
|
||||
<FormGroup>
|
||||
<Label>{$t("checkers.schedule.interval-label")}</Label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={intervalHours()}
|
||||
oninput={(e: Event) =>
|
||||
setIntervalHours(parseInt((e.target as HTMLInputElement).value) || 1)}
|
||||
style="width: 100px"
|
||||
/>
|
||||
<span>{$t("checkers.schedule.hours")}</span>
|
||||
</div>
|
||||
<small class="text-muted">{$t("checkers.schedule.interval-hint")}</small>
|
||||
</FormGroup>
|
||||
</form>
|
||||
|
||||
{#if !existingPlanId}
|
||||
<Alert color="info" class="mb-0 mt-2">
|
||||
<Icon name="info-circle" />
|
||||
{$t("checkers.schedule.no-schedule-yet")}
|
||||
</Alert>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
81
web/src/lib/components/checkers/CheckerSidebar.svelte
Normal file
81
web/src/lib/components/checkers/CheckerSidebar.svelte
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<!--
|
||||
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 { Icon, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { t } from "$lib/translations";
|
||||
import { checkers } from "$lib/stores/checkers";
|
||||
|
||||
let {
|
||||
currentCheckId,
|
||||
}: {
|
||||
currentCheckId: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="d-flex flex-column h-100">
|
||||
<a
|
||||
href="/checkers"
|
||||
class="sidebar-back d-flex align-items-center gap-1 mb-3 text-muted text-decoration-none fw-semibold"
|
||||
>
|
||||
<Icon name="chevron-left" />
|
||||
{$t("checkers.title")}
|
||||
</a>
|
||||
|
||||
{#if $checkers}
|
||||
<ul class="list-unstyled mb-0 flex-fill overflow-auto">
|
||||
{#each Object.entries($checkers) as [checkerId, checkerInfo]}
|
||||
<li>
|
||||
<a
|
||||
href="/checkers/{checkerId}"
|
||||
class="checker-item d-flex align-items-center gap-2 py-2 px-2 rounded text-decoration-none"
|
||||
class:active={checkerId === currentCheckId}
|
||||
>
|
||||
<span class="text-truncate">
|
||||
{checkerInfo.name || checkerId}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div class="d-flex gap-2 align-items-center justify-content-center my-3 text-muted">
|
||||
<Spinner size="sm" color="primary" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.checker-item {
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.checker-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.checker-item.active {
|
||||
background-color: rgba(var(--bs-primary-rgb), 0.1);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<!--
|
||||
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 { Badge, Table } from "@sveltestrap/sveltestrap";
|
||||
import type { HappydnsCheckerAvailability, HappydnsCheckerDefinition } from "$lib/api-base/types.gen";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
let {
|
||||
checkers,
|
||||
basePath = "/checkers",
|
||||
}: {
|
||||
checkers: [string, HappydnsCheckerDefinition][];
|
||||
basePath?: string;
|
||||
} = $props();
|
||||
|
||||
function availabilityBadges(availability: HappydnsCheckerAvailability | undefined): { label: string; color: string }[] {
|
||||
if (!availability) return [];
|
||||
const badges = [];
|
||||
if (availability.applyToDomain) badges.push({ label: $t("checkers.availability.domain-level"), color: "success" });
|
||||
if (availability.applyToZone) badges.push({ label: $t("checkers.availability.zone-level"), color: "info" });
|
||||
if (availability.applyToService) badges.push({ label: $t("checkers.availability.service-level"), color: "primary" });
|
||||
return badges;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="table-responsive">
|
||||
<Table hover bordered>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$t("checkers.table.name")}</th>
|
||||
<th>{$t("checkers.table.availability")}</th>
|
||||
<th>{$t("checkers.table.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each checkers as [checkerId, checkerInfo]}
|
||||
{@const badges = availabilityBadges(checkerInfo.availability)}
|
||||
<tr>
|
||||
<td><strong>{checkerInfo.name || checkerId}</strong></td>
|
||||
<td>
|
||||
{#if badges.length > 0}
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{#each badges as badge}
|
||||
<Badge color={badge.color}>
|
||||
{badge.label}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Badge color="secondary">{$t("checkers.availability.general")}</Badge>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{basePath}/{checkerId}" class="btn btn-sm btn-primary">
|
||||
{$t("checkers.table.manage")}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
103
web/src/lib/components/checkers/ChecksSidebarContent.svelte
Normal file
103
web/src/lib/components/checkers/ChecksSidebarContent.svelte
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<!--
|
||||
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 { Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import type { CheckerScope } from "$lib/api/checkers";
|
||||
import { t } from "$lib/translations";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import { thisZone } from "$lib/stores/thiszone";
|
||||
import DomainCheckerSidebar from "./DomainCheckerSidebar.svelte";
|
||||
import ExecutionSidebarContent from "./ExecutionSidebarContent.svelte";
|
||||
|
||||
interface Props {
|
||||
domain: Domain;
|
||||
checksBase: string;
|
||||
backHref: string;
|
||||
serviceContext?: {
|
||||
zoneId: string;
|
||||
subdomain: string;
|
||||
serviceid: string;
|
||||
};
|
||||
}
|
||||
|
||||
let { domain, checksBase, backHref, serviceContext }: Props = $props();
|
||||
|
||||
let scope: CheckerScope = $derived(
|
||||
serviceContext
|
||||
? { domainId: domain.id, zoneId: serviceContext.zoneId, subdomain: serviceContext.subdomain, serviceId: serviceContext.serviceid }
|
||||
: { domainId: domain.id },
|
||||
);
|
||||
|
||||
let serviceType = $derived.by(() => {
|
||||
if (!serviceContext) return undefined;
|
||||
const svcs =
|
||||
$thisZone?.services[serviceContext.subdomain == "@" ? "" : serviceContext.subdomain];
|
||||
const svc = svcs?.find((s) => s._id === serviceContext.serviceid);
|
||||
return svc?._svctype;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if page.params.execId}
|
||||
<a
|
||||
href={`${checksBase}/${encodeURIComponent(page.params.checkerId!)}/executions`}
|
||||
class="sidebar-back d-flex align-items-center gap-1 mt-3 text-muted text-decoration-none fw-semibold"
|
||||
>
|
||||
<Icon name="chevron-left" />
|
||||
{$t("zones.return-to-results")}
|
||||
</a>
|
||||
<ExecutionSidebarContent
|
||||
{domain}
|
||||
checkerId={page.params.checkerId!}
|
||||
execId={page.params.execId}
|
||||
{checksBase}
|
||||
{scope}
|
||||
/>
|
||||
{:else if page.params.checkerId}
|
||||
<a
|
||||
href={checksBase}
|
||||
class="sidebar-back d-flex align-items-center gap-1 mt-3 text-muted text-decoration-none fw-semibold"
|
||||
>
|
||||
<Icon name="chevron-left" />
|
||||
{$t("checkers.title")}
|
||||
</a>
|
||||
<DomainCheckerSidebar
|
||||
class="mt-3"
|
||||
domainName={domain.domain}
|
||||
currentCheckerName={page.params.checkerId}
|
||||
{checksBase}
|
||||
scope={serviceContext ? "service" : "domain"}
|
||||
{serviceType}
|
||||
/>
|
||||
<div class="flex-fill"></div>
|
||||
{:else}
|
||||
<a
|
||||
href={backHref}
|
||||
class="sidebar-back d-flex align-items-center gap-1 mt-3 text-muted text-decoration-none fw-semibold"
|
||||
>
|
||||
<Icon name="chevron-left" />
|
||||
{$t("zones.return-to")}
|
||||
</a>
|
||||
{/if}
|
||||
152
web/src/lib/components/checkers/DomainCheckerSidebar.svelte
Normal file
152
web/src/lib/components/checkers/DomainCheckerSidebar.svelte
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<!--
|
||||
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 { ClassValue } from "svelte/elements";
|
||||
import { Icon, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { checkers } from "$lib/stores/checkers";
|
||||
|
||||
interface Props {
|
||||
class?: ClassValue;
|
||||
domainName: string;
|
||||
currentCheckerName: string;
|
||||
checksBase?: string;
|
||||
scope?: "domain" | "service";
|
||||
serviceType?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = "",
|
||||
domainName,
|
||||
currentCheckerName,
|
||||
checksBase: checksBaseProp = undefined,
|
||||
scope = "domain",
|
||||
serviceType = undefined,
|
||||
}: Props = $props();
|
||||
|
||||
let checksBase = $derived(
|
||||
checksBaseProp ?? `/domains/${encodeURIComponent(domainName)}/checks`,
|
||||
);
|
||||
let onResults = $derived(page.route.id?.includes("/executions") === true && !page.params.execId);
|
||||
|
||||
function isCheckVisible(checkerInfo: NonNullable<typeof $checkers>[string]): boolean {
|
||||
const avail = checkerInfo.availability;
|
||||
if (!avail) return true;
|
||||
if (scope === "domain" && !avail.applyToDomain && !avail.applyToZone) return false;
|
||||
if (scope === "service") {
|
||||
if (!avail.applyToService) return false;
|
||||
if (avail.limitToServices && avail.limitToServices.length > 0) {
|
||||
if (!serviceType || !avail.limitToServices.includes(serviceType)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="checker-sidebar d-flex flex-column h-100 {className}">
|
||||
{#if !$checkers}
|
||||
<div class="d-flex gap-2 align-items-center justify-content-center my-3 text-muted">
|
||||
<Spinner size="sm" color="primary" />
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="list-unstyled mb-0 flex-fill overflow-auto">
|
||||
{#each Object.entries($checkers) as [checkerName, checkerInfo]}
|
||||
{#if isCheckVisible(checkerInfo)}
|
||||
{@const isActive = checkerName === currentCheckerName}
|
||||
<li>
|
||||
<div
|
||||
class="checker-item d-flex align-items-center gap-1 py-2 px-2 rounded {isActive
|
||||
? 'fw-bold text-primary active'
|
||||
: 'text-muted'}"
|
||||
>
|
||||
<a
|
||||
href="{checksBase}/{encodeURIComponent(checkerName)}{onResults
|
||||
? '/executions'
|
||||
: ''}"
|
||||
class="text-truncate flex-fill text-decoration-none {isActive
|
||||
? 'text-primary'
|
||||
: 'text-muted'}"
|
||||
>
|
||||
{checkerInfo.name || checkerName}
|
||||
</a>
|
||||
{#if onResults}
|
||||
<a
|
||||
href="{checksBase}/{encodeURIComponent(checkerName)}"
|
||||
class="checker-action text-muted"
|
||||
title="Configure"
|
||||
>
|
||||
<Icon name="gear" />
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href="{checksBase}/{encodeURIComponent(checkerName)}/executions"
|
||||
class="checker-action text-muted"
|
||||
title="Results"
|
||||
>
|
||||
<Icon name="bar-chart-fill" />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.checker-item {
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.checker-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.checker-action {
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.checker-action.always-visible {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.checker-item:hover .checker-action,
|
||||
.checker-item.active .checker-action {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.checker-action:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.checker-item.active {
|
||||
background-color: rgba(var(--bs-primary-rgb), 0.1);
|
||||
}
|
||||
</style>
|
||||
102
web/src/lib/components/checkers/ExecutionDetailPage.svelte
Normal file
102
web/src/lib/components/checkers/ExecutionDetailPage.svelte
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<!--
|
||||
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 { onDestroy } from "svelte";
|
||||
import { Alert, Card, Container, Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { t } from "$lib/translations";
|
||||
import type { CheckerScope } from "$lib/api/checkers";
|
||||
import {
|
||||
getScopedExecution,
|
||||
getScopedExecutionObservations,
|
||||
getCheckStatus,
|
||||
} from "$lib/api/checkers";
|
||||
import { currentExecution, currentCheckInfo, currentObservations } from "$lib/stores/checkers";
|
||||
import ObservationReportCard from "./ObservationReportCard.svelte";
|
||||
|
||||
interface Props {
|
||||
scope: CheckerScope;
|
||||
checkerId: string;
|
||||
execId: string;
|
||||
}
|
||||
|
||||
let { scope, checkerId, execId }: Props = $props();
|
||||
|
||||
let checkerName = $state<string>("");
|
||||
let loading = $state(true);
|
||||
let error = $state<string | undefined>(undefined);
|
||||
|
||||
$effect(() => {
|
||||
loading = true;
|
||||
error = undefined;
|
||||
|
||||
Promise.all([
|
||||
getScopedExecution(scope, checkerId, execId),
|
||||
getCheckStatus(checkerId),
|
||||
getScopedExecutionObservations(scope, checkerId, execId),
|
||||
]).then(
|
||||
([execution, checkerInfo, observations]) => {
|
||||
currentExecution.set(execution);
|
||||
currentCheckInfo.set(checkerInfo);
|
||||
currentObservations.set(observations);
|
||||
checkerName = checkerInfo.name ?? checkerId;
|
||||
loading = false;
|
||||
},
|
||||
(err) => {
|
||||
error = err.message;
|
||||
loading = false;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
currentExecution.set(undefined);
|
||||
currentCheckInfo.set(undefined);
|
||||
currentObservations.set(undefined);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t("checkers.execution.title")} - {checkerName || checkerId} - happyDomain</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if loading}
|
||||
<Container class="flex-fill d-flex align-items-start mt-5">
|
||||
<Card body>
|
||||
<p class="text-center mb-0">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
{$t("checkers.result.loading")}
|
||||
</p>
|
||||
</Card>
|
||||
</Container>
|
||||
{:else if error}
|
||||
<Container class="flex-fill d-flex align-items-start mt-5">
|
||||
<Alert class="flex-fill" color="danger">
|
||||
<Icon name="exclamation-triangle-fill" />
|
||||
{$t("checkers.result.error-loading", { error })}
|
||||
</Alert>
|
||||
</Container>
|
||||
{:else if $currentObservations}
|
||||
<ObservationReportCard observations={$currentObservations} />
|
||||
{/if}
|
||||
275
web/src/lib/components/checkers/ExecutionListPage.svelte
Normal file
275
web/src/lib/components/checkers/ExecutionListPage.svelte
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
<!--
|
||||
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, Badge, Button, Card, Icon, Table } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { t } from "$lib/translations";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
import type { HappydnsExecution } from "$lib/api-base/types.gen";
|
||||
import type { CheckerScope } from "$lib/api/checkers";
|
||||
import { listScopedExecutions, getCheckStatus, deleteScopedExecution, deleteAllScopedExecutions } from "$lib/api/checkers";
|
||||
import {
|
||||
getExecutionStatusColor,
|
||||
getExecutionStatusI18nKey,
|
||||
getStatusColor,
|
||||
getStatusI18nKey,
|
||||
formatCheckDate,
|
||||
} from "$lib/utils";
|
||||
import PageTitle from "$lib/components/PageTitle.svelte";
|
||||
import RunCheckModal from "$lib/components/modals/RunCheckModal.svelte";
|
||||
|
||||
interface Props {
|
||||
scope: CheckerScope;
|
||||
checksBase: string;
|
||||
checkerId: string;
|
||||
domainName: string;
|
||||
}
|
||||
|
||||
let { scope, checksBase, checkerId, domainName }: Props = $props();
|
||||
|
||||
let resolvedName = $state<string>("");
|
||||
let executions = $state<HappydnsExecution[]>([]);
|
||||
let executionsPromise: Promise<HappydnsExecution[]> = $derived(loadExecutions());
|
||||
let runCheckModal = $state<RunCheckModal>();
|
||||
|
||||
$effect(() => {
|
||||
getCheckStatus(checkerId).then((s) => {
|
||||
resolvedName = s.name ?? checkerId;
|
||||
});
|
||||
});
|
||||
|
||||
async function loadExecutions() {
|
||||
try {
|
||||
executions = await listScopedExecutions(scope, checkerId, { includePlanned: true });
|
||||
return executions;
|
||||
} catch (error) {
|
||||
toasts.addErrorToast({
|
||||
message: $t("checkers.executions.error-loading", { error: String(error) }),
|
||||
timeout: 10000,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteExecution(executionId: string) {
|
||||
try {
|
||||
await deleteScopedExecution(scope, checkerId, executionId);
|
||||
executions = executions.filter((e) => e.id !== executionId);
|
||||
} catch (error) {
|
||||
toasts.addErrorToast({
|
||||
message: $t("checkers.executions.error-deleting", { error: String(error) }),
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAllExecutions() {
|
||||
if (!confirm($t("checkers.executions.delete-all-confirm"))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteAllScopedExecutions(scope, checkerId);
|
||||
executions = executions.filter((e) => !e.id);
|
||||
toasts.addToast({
|
||||
message: $t("checkers.executions.deleted-all"),
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
toasts.addErrorToast({
|
||||
message: $t("checkers.executions.error-deleting", { error: String(error) }),
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pollForNewExecution() {
|
||||
const previousCount = executions.length;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 10;
|
||||
const intervalMs = 3000;
|
||||
|
||||
const timer = setInterval(async () => {
|
||||
attempts++;
|
||||
try {
|
||||
await loadExecutions();
|
||||
if (executions.length > previousCount || attempts >= maxAttempts) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
} catch {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, intervalMs);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{$t("checkers.executions.title", { count: executions.length })} - {resolvedName ||
|
||||
checkerId} - happyDomain
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex-fill mt-1 mb-5">
|
||||
<PageTitle
|
||||
title={$t("checkers.executions.title", { count: executions.length })}
|
||||
subtitle={resolvedName}
|
||||
domain={domainName}
|
||||
>
|
||||
<div class="d-flex gap-2">
|
||||
<Button color="dark" href="{checksBase}/{checkerId}">
|
||||
<Icon name="gear-fill"></Icon>
|
||||
{$t("checkers.executions.configure")}
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onclick={() => runCheckModal?.open(checkerId, resolvedName || checkerId)}
|
||||
>
|
||||
<Icon name="play-fill"></Icon>
|
||||
{$t("checkers.executions.run-check-now")}
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
outline
|
||||
disabled={executions.filter((e) => e.id).length === 0}
|
||||
onclick={deleteAllExecutions}
|
||||
>
|
||||
<Icon name="trash-fill"></Icon>
|
||||
{$t("checkers.executions.delete-all")}
|
||||
</Button>
|
||||
</div>
|
||||
</PageTitle>
|
||||
|
||||
{#await executionsPromise}
|
||||
<Card body>
|
||||
<p class="text-center mb-0">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
{$t("checkers.executions.loading")}
|
||||
</p>
|
||||
</Card>
|
||||
{:then _executions}
|
||||
{#if executions.length === 0}
|
||||
<Alert color="info">
|
||||
<Icon name="info-circle" />
|
||||
{$t("checkers.executions.no-results")}
|
||||
</Alert>
|
||||
{:else}
|
||||
<Table hover responsive>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$t("checkers.executions.table.executed-at")}</th>
|
||||
<th>{$t("checkers.executions.table.status")}</th>
|
||||
<th>{$t("checkers.executions.table.duration")}</th>
|
||||
<th>{$t("checkers.executions.table.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each executions.toSorted((a, b) => {
|
||||
const aTime = a.startedAt ? new Date(a.startedAt).getTime() : Infinity;
|
||||
const bTime = b.startedAt ? new Date(b.startedAt).getTime() : Infinity;
|
||||
return bTime - aTime;
|
||||
}) as execution}
|
||||
{@const isPending = !execution.id}
|
||||
{@const isRunning =
|
||||
execution.id && execution.startedAt && !execution.endedAt}
|
||||
{@const status = execution.status}
|
||||
{@const duration =
|
||||
execution.startedAt && execution.endedAt
|
||||
? Math.round(
|
||||
(new Date(execution.endedAt).getTime() -
|
||||
new Date(execution.startedAt).getTime()) /
|
||||
1000,
|
||||
)
|
||||
: null}
|
||||
<tr>
|
||||
<td>
|
||||
{#if !execution.startedAt}
|
||||
<span class="text-muted fst-italic">
|
||||
{$t("checkers.status.planned")}
|
||||
</span>
|
||||
{:else if isPending}
|
||||
<span class="text-muted fst-italic">
|
||||
{formatCheckDate(execution.startedAt)}
|
||||
</span>
|
||||
{:else}
|
||||
{formatCheckDate(execution.startedAt)}
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if isPending}
|
||||
<Badge color="secondary">{$t("checkers.status.planned")}</Badge>
|
||||
{:else if status == 2 && execution.result}
|
||||
<Badge color={getStatusColor(execution.result.status)}>
|
||||
{$t(getStatusI18nKey(execution.result.status))}
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge color={getExecutionStatusColor(status)}>
|
||||
{$t(getExecutionStatusI18nKey(status))}
|
||||
</Badge>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if isRunning}
|
||||
<span class="text-muted fst-italic">
|
||||
{$t("checkers.status.running")}
|
||||
</span>
|
||||
{:else if duration !== null}
|
||||
{duration}s
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<a
|
||||
href="{checksBase}/{checkerId}/executions/{execution.id}"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
class:disabled={!execution.id && !isRunning}
|
||||
>
|
||||
{$t("checkers.executions.view")}
|
||||
</a>
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
outline
|
||||
disabled={!!isPending || !!isRunning}
|
||||
onclick={() =>
|
||||
execution.id && deleteExecution(execution.id)}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<RunCheckModal
|
||||
{scope}
|
||||
onCheckTriggered={() => pollForNewExecution()}
|
||||
bind:this={runCheckModal}
|
||||
/>
|
||||
63
web/src/lib/components/checkers/ExecutionResultsCard.svelte
Normal file
63
web/src/lib/components/checkers/ExecutionResultsCard.svelte
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<!--
|
||||
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 { Card, CardBody, CardHeader, Table } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { t } from "$lib/translations";
|
||||
import type { HappydnsCheckEvaluation } from "$lib/api-base/types.gen";
|
||||
|
||||
interface Props {
|
||||
evaluation: HappydnsCheckEvaluation;
|
||||
}
|
||||
|
||||
let { evaluation }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<strong>{$t("checkers.detail.check-rules")}</strong>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{#if evaluation.states && evaluation.states.length > 0}
|
||||
<Table size="sm" borderless>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$t("checkers.result.field.rule")}</th>
|
||||
<th>{$t("checkers.result.field.message")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each evaluation.states as state}
|
||||
<tr>
|
||||
<td><code>{state.code ?? ""}</code></td>
|
||||
<td>{state.message ?? ""}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
{:else}
|
||||
<pre class="mb-0"><code>{JSON.stringify(evaluation, null, 2)}</code></pre>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
79
web/src/lib/components/checkers/ExecutionRulesPage.svelte
Normal file
79
web/src/lib/components/checkers/ExecutionRulesPage.svelte
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<!--
|
||||
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>
|
||||
253
web/src/lib/components/checkers/ExecutionSidebarContent.svelte
Normal file
253
web/src/lib/components/checkers/ExecutionSidebarContent.svelte
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
<!--
|
||||
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 {
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardHeader,
|
||||
Icon,
|
||||
Spinner,
|
||||
Table,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { navigate } from "$lib/stores/config";
|
||||
import { currentExecution, currentCheckInfo, currentObservations } from "$lib/stores/checkers";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
import type { CheckerScope } from "$lib/api/checkers";
|
||||
import {
|
||||
triggerScopedCheck,
|
||||
deleteScopedExecution,
|
||||
} from "$lib/api/checkers";
|
||||
import {
|
||||
getExecutionStatusColor,
|
||||
getExecutionStatusI18nKey,
|
||||
getStatusColor,
|
||||
getStatusI18nKey,
|
||||
formatCheckDate,
|
||||
downloadBlob,
|
||||
} from "$lib/utils";
|
||||
import { t } from "$lib/translations";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
|
||||
interface Props {
|
||||
domain: Domain;
|
||||
checkerId: string;
|
||||
execId: string;
|
||||
checksBase: string;
|
||||
scope: CheckerScope;
|
||||
}
|
||||
|
||||
let { domain, checkerId, execId, checksBase, scope }: Props = $props();
|
||||
|
||||
let isRelaunching = $state(false);
|
||||
|
||||
async function handleRelaunch() {
|
||||
isRelaunching = true;
|
||||
try {
|
||||
const execution = await triggerScopedCheck(scope, checkerId);
|
||||
toasts.addToast({
|
||||
message: $t("checkers.run-check.triggered-success", { id: execution.id ?? "" }),
|
||||
type: "success",
|
||||
timeout: 5000,
|
||||
});
|
||||
if (execution.id) {
|
||||
navigate(
|
||||
`${checksBase}/${encodeURIComponent(checkerId)}/executions/${execution.id}`,
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toasts.addErrorToast({
|
||||
message: error.message || $t("checkers.result.relaunch-failed"),
|
||||
});
|
||||
} finally {
|
||||
isRelaunching = false;
|
||||
}
|
||||
}
|
||||
|
||||
let isDeleting = $state(false);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!$currentExecution?.id) return;
|
||||
isDeleting = true;
|
||||
try {
|
||||
await deleteScopedExecution(scope, checkerId, $currentExecution.id);
|
||||
navigate(`${checksBase}/${encodeURIComponent(checkerId)}/executions`);
|
||||
} catch (error: any) {
|
||||
toasts.addErrorToast({
|
||||
message:
|
||||
error.message ||
|
||||
$t("checkers.executions.error-deleting", { error: String(error) }),
|
||||
});
|
||||
} finally {
|
||||
isDeleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadJSON() {
|
||||
if (!$currentObservations?.data) return;
|
||||
downloadBlob(
|
||||
JSON.stringify($currentObservations.data, null, 2),
|
||||
`${checkerId}-${execId}.json`,
|
||||
"application/json",
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $currentExecution}
|
||||
<Card class="mt-3">
|
||||
<CardHeader class="px-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong class="text-truncate">{$currentCheckInfo?.name || checkerId}</strong>
|
||||
<Badge
|
||||
color={getExecutionStatusColor($currentExecution.status)}
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
{$t(getExecutionStatusI18nKey($currentExecution.status))}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div class="overflow-x-auto rounded-2">
|
||||
<Table borderless size="sm" class="mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width: 80px; white-space: nowrap">
|
||||
{$t("checkers.result.field.executed-at")}
|
||||
</th>
|
||||
<td>{formatCheckDate($currentExecution.startedAt)}</td>
|
||||
</tr>
|
||||
{#if $currentExecution.endedAt}
|
||||
<tr>
|
||||
<th>{$t("checkers.execution.field.ended-at")}</th>
|
||||
<td>{formatCheckDate($currentExecution.endedAt)}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
<tr>
|
||||
<th>{$t("checkers.result.field.status")}</th>
|
||||
<td class="d-flex gap-2 align-items-center">
|
||||
<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}
|
||||
<tr>
|
||||
<th>{$t("checkers.result.field.status-message")}</th>
|
||||
<td class="text-truncate" style="max-width: 0">
|
||||
{$currentExecution.result.message}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if $currentExecution.error}
|
||||
<tr>
|
||||
<th>{$t("checkers.result.field.error")}</th>
|
||||
<td class="text-danger text-truncate" style="max-width: 0">
|
||||
{$currentExecution.error}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if $currentExecution.trigger}
|
||||
<tr>
|
||||
<th>{$t("checkers.execution.field.trigger")}</th>
|
||||
<td><code>{JSON.stringify($currentExecution.trigger)}</code></td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div class="my-3 flex-fill"></div>
|
||||
|
||||
<!-- TODO: Metrics and HTML report not yet implemented -->
|
||||
<ButtonGroup class="w-100 mb-2">
|
||||
<Button size="sm" color="secondary" outline disabled title="Not yet available">
|
||||
<Icon name="graph-up"></Icon>
|
||||
{$t("checkers.result.view-metrics")}
|
||||
</Button>
|
||||
<Button size="sm" color="secondary" outline disabled title="Not yet available">
|
||||
<Icon name="file-earmark-richtext"></Icon>
|
||||
{$t("checkers.result.view-html")}
|
||||
</Button>
|
||||
<Button size="sm" color="secondary" outline active>
|
||||
<Icon name="braces"></Icon>
|
||||
{$t("checkers.result.view-json")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup class="w-100">
|
||||
<!-- TODO: HTML report download not yet available -->
|
||||
<Button size="sm" color="outline-secondary" disabled title="Not yet available">
|
||||
<Icon name="download"></Icon>
|
||||
{$t("checkers.result.download-html")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="outline-secondary"
|
||||
onclick={downloadJSON}
|
||||
disabled={!$currentObservations?.data}
|
||||
>
|
||||
<Icon name="download"></Icon>
|
||||
{$t("checkers.result.download-json")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{:else}
|
||||
<div class="flex-fill"></div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 d-flex gap-2">
|
||||
<Button
|
||||
class="flex-fill"
|
||||
color="primary"
|
||||
outline
|
||||
onclick={handleRelaunch}
|
||||
disabled={!$currentExecution || isRelaunching}
|
||||
>
|
||||
{#if isRelaunching}
|
||||
<Spinner size="sm" />
|
||||
{:else}
|
||||
<Icon name="arrow-repeat"></Icon>
|
||||
{/if}
|
||||
{$t("checkers.result.relaunch")}
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
outline
|
||||
onclick={handleDelete}
|
||||
disabled={!$currentExecution?.id || isDeleting}
|
||||
>
|
||||
{#if isDeleting}
|
||||
<Spinner size="sm" />
|
||||
{:else}
|
||||
<Icon name="trash"></Icon>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
43
web/src/lib/components/checkers/ObservationReportCard.svelte
Normal file
43
web/src/lib/components/checkers/ObservationReportCard.svelte
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<!--
|
||||
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 type { HappydnsObservationSnapshot } from "$lib/api-base/types.gen";
|
||||
|
||||
interface Props {
|
||||
observations: HappydnsObservationSnapshot;
|
||||
}
|
||||
|
||||
let { observations }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if observations?.data && Object.keys(observations.data).length > 0}
|
||||
<div
|
||||
class="flex-fill"
|
||||
style="overflow: auto; padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x)"
|
||||
>
|
||||
<pre class="mb-0" style="width: 0; min-width: 100%"><code
|
||||
>{JSON.stringify(observations.data, null, 2)}</code
|
||||
></pre>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
import TableInput from "$lib/components/inputs/table.svelte";
|
||||
import type { Field } from "$lib/model/custom_form.svelte";
|
||||
import type { ServiceInfos } from "$lib/model/service_specs.svelte";
|
||||
import type { HappydnsCheckerOptionDocumentation } from "$lib/api-base/types.gen";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
|
@ -41,7 +42,7 @@
|
|||
noDecorate?: boolean;
|
||||
readonly?: boolean;
|
||||
showDescription?: boolean;
|
||||
specs?: Field | ServiceInfos;
|
||||
specs?: Field | ServiceInfos | HappydnsCheckerOptionDocumentation;
|
||||
type: string;
|
||||
value: any;
|
||||
}
|
||||
|
|
|
|||
326
web/src/lib/components/modals/RunCheckModal.svelte
Normal file
326
web/src/lib/components/modals/RunCheckModal.svelte
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
<!--
|
||||
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,
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
Icon,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Spinner,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { getCheckStatus, getScopedCheckOptions, triggerScopedCheck } from "$lib/api/checkers";
|
||||
import type { CheckerScope } from "$lib/api/checkers";
|
||||
import { collectAllOptionDocs } from "$lib/utils/checkers";
|
||||
import type {
|
||||
HappydnsCheckerDefinition,
|
||||
HappydnsCheckerOptionDocumentation,
|
||||
HappydnsCheckerOptions,
|
||||
HappydnsCheckerOptionsPositional,
|
||||
HappydnsCheckerRunRequest,
|
||||
} from "$lib/api-base/types.gen";
|
||||
import Resource from "$lib/components/inputs/Resource.svelte";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
interface Props {
|
||||
scope: CheckerScope;
|
||||
onCheckTriggered?: (execution_id: string) => void;
|
||||
}
|
||||
|
||||
let { scope, onCheckTriggered }: Props = $props();
|
||||
|
||||
let isOpen = $state(false);
|
||||
let checkName = $state<string>("");
|
||||
let checkDisplayName = $state<string>("");
|
||||
let checkStatusPromise = $state<Promise<any> | null>(null);
|
||||
let scopedOptionsPromise = $state<Promise<any> | null>(null);
|
||||
let resolvedStatus = $state<HappydnsCheckerDefinition | null>(null);
|
||||
let runOptions = $state<Record<string, any>>({});
|
||||
let scopedDefaults = $state<Record<string, any>>({});
|
||||
let triggering = $state(false);
|
||||
let showAdvanced = $state(false);
|
||||
let activeRules = $state<Record<number, boolean>>({});
|
||||
|
||||
const toggle = () => (isOpen = !isOpen);
|
||||
|
||||
export function open(name: string, displayName: string) {
|
||||
checkName = name;
|
||||
checkDisplayName = displayName;
|
||||
runOptions = {};
|
||||
scopedDefaults = {};
|
||||
showAdvanced = false;
|
||||
activeRules = {};
|
||||
resolvedStatus = null;
|
||||
checkStatusPromise = getCheckStatus(name);
|
||||
scopedOptionsPromise = getScopedCheckOptions(scope, name);
|
||||
isOpen = true;
|
||||
|
||||
Promise.all([checkStatusPromise, scopedOptionsPromise]).then(
|
||||
([status, options]: [
|
||||
HappydnsCheckerDefinition,
|
||||
HappydnsCheckerOptionsPositional[],
|
||||
]) => {
|
||||
resolvedStatus = status;
|
||||
scopedDefaults = Object.assign({}, ...(options || []).map((p) => p.options || {}));
|
||||
|
||||
// For select fields (choices), set the value directly since placeholders don't work on <select>.
|
||||
const allOpts = collectAllOptionDocs(status);
|
||||
for (const opt of allOpts) {
|
||||
if (opt.id && opt.choices?.length && opt.id in scopedDefaults) {
|
||||
runOptions[opt.id] = scopedDefaults[opt.id];
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function getActiveOptionIds(): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
if (!resolvedStatus) return ids;
|
||||
const addOpts = (opts: HappydnsCheckerOptionDocumentation[] | undefined) =>
|
||||
opts?.forEach((o) => o.id && ids.add(o.id));
|
||||
addOpts(resolvedStatus.options?.runOpts);
|
||||
addOpts(resolvedStatus.options?.adminOpts);
|
||||
addOpts(resolvedStatus.options?.userOpts);
|
||||
addOpts(resolvedStatus.options?.domainOpts);
|
||||
resolvedStatus.rules?.forEach((rule, idx) => {
|
||||
if (activeRules[idx] !== false) {
|
||||
if (rule.options) {
|
||||
addOpts(rule.options.runOpts);
|
||||
addOpts(rule.options.adminOpts);
|
||||
addOpts(rule.options.userOpts);
|
||||
addOpts(rule.options.domainOpts);
|
||||
}
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
function specsWithPlaceholder(
|
||||
optDoc: HappydnsCheckerOptionDocumentation,
|
||||
): HappydnsCheckerOptionDocumentation {
|
||||
if (optDoc.id && optDoc.id in scopedDefaults && scopedDefaults[optDoc.id] != null) {
|
||||
return { ...optDoc, placeholder: String(scopedDefaults[optDoc.id]) };
|
||||
}
|
||||
return optDoc;
|
||||
}
|
||||
|
||||
async function handleRunCheck() {
|
||||
triggering = true;
|
||||
try {
|
||||
const activeIds = getActiveOptionIds();
|
||||
const filteredOptions: HappydnsCheckerOptions = {};
|
||||
for (const [k, v] of Object.entries(runOptions)) {
|
||||
if (!resolvedStatus || activeIds.has(k)) filteredOptions[k] = v;
|
||||
}
|
||||
|
||||
// Build enabledRules map from activeRules (only if some rules are toggled off).
|
||||
const rules = resolvedStatus?.rules ?? [];
|
||||
let enabledRules: Record<string, boolean> | undefined;
|
||||
if (rules.length > 0) {
|
||||
const hasDisabled = rules.some((_r, idx) => activeRules[idx] === false);
|
||||
if (hasDisabled) {
|
||||
enabledRules = {};
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const name = rules[i].name;
|
||||
if (name) {
|
||||
enabledRules[name] = activeRules[i] !== false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const request: HappydnsCheckerRunRequest = {
|
||||
options: filteredOptions,
|
||||
...(enabledRules ? { enabledRules } : {}),
|
||||
};
|
||||
const result = await triggerScopedCheck(scope, checkName, request);
|
||||
toasts.addToast({
|
||||
message: $t("checkers.run-check.triggered-success", { id: result.id ?? "" }),
|
||||
type: "success",
|
||||
timeout: 5000,
|
||||
});
|
||||
isOpen = false;
|
||||
if (onCheckTriggered && result.id) {
|
||||
onCheckTriggered(result.id);
|
||||
}
|
||||
} catch (error) {
|
||||
toasts.addErrorToast({
|
||||
message: $t("checkers.run-check.trigger-failed", { error: String(error) }),
|
||||
timeout: 10000,
|
||||
});
|
||||
} finally {
|
||||
triggering = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {isOpen} {toggle} size="lg">
|
||||
<ModalHeader {toggle}>
|
||||
{$t("checkers.run-check.title")}: {checkDisplayName}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{#if checkStatusPromise && scopedOptionsPromise}
|
||||
{#await Promise.all([checkStatusPromise, scopedOptionsPromise])}
|
||||
<div class="text-center py-3">
|
||||
<Spinner />
|
||||
<p class="mt-2">{$t("checkers.run-check.loading-options")}</p>
|
||||
</div>
|
||||
{:then [status, _domainOpts]}
|
||||
{@const rules = status.rules || []}
|
||||
{@const activeRulesForOpts = rules.map(
|
||||
(r: HappydnsCheckerDefinition | null, i: number) =>
|
||||
activeRules[i] !== false ? r : null,
|
||||
)}
|
||||
{@const runOpts = [
|
||||
...(status.options?.runOpts || []),
|
||||
...activeRulesForOpts.flatMap((r: any) => r?.options?.runOpts || []),
|
||||
]}
|
||||
{@const otherOpts = [
|
||||
...(status.options?.adminOpts || []),
|
||||
...(status.options?.userOpts || []),
|
||||
...(status.options?.domainOpts || []),
|
||||
...activeRulesForOpts.flatMap((r: any) => [
|
||||
...(r?.options?.adminOpts || []),
|
||||
...(r?.options?.userOpts || []),
|
||||
...(r?.options?.domainOpts || []),
|
||||
]),
|
||||
].filter((o: any) => o.id)}
|
||||
<Form
|
||||
id="run-check-modal"
|
||||
onsubmit={(e: Event) => {
|
||||
e.preventDefault();
|
||||
handleRunCheck();
|
||||
}}
|
||||
>
|
||||
{#if runOpts.length > 0 || otherOpts.length > 0}
|
||||
<p>
|
||||
{#if runOpts.length > 0}
|
||||
{$t("checkers.run-check.configure-info")}
|
||||
{:else}
|
||||
<Icon name="info-circle"></Icon>
|
||||
{$t("checkers.run-check.no-run-options")}
|
||||
{/if}
|
||||
</p>
|
||||
{#each runOpts as optDoc}
|
||||
{#if optDoc.id}
|
||||
{@const optName = optDoc.id}
|
||||
<FormGroup>
|
||||
<Resource
|
||||
edit={true}
|
||||
index={optName}
|
||||
specs={specsWithPlaceholder(optDoc)}
|
||||
type={optDoc.type || "string"}
|
||||
readonly={!!optDoc.autoFill}
|
||||
bind:value={runOptions[optName]}
|
||||
/>
|
||||
</FormGroup>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if otherOpts.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link btn-sm px-0 mb-2 text-muted d-flex align-items-center gap-1 text-decoration-none"
|
||||
onclick={() => (showAdvanced = !showAdvanced)}
|
||||
>
|
||||
<Icon name={showAdvanced ? "chevron-down" : "chevron-right"} />
|
||||
{$t("checkers.run-check.advanced-options")}
|
||||
</button>
|
||||
{#if showAdvanced}
|
||||
{#each otherOpts as optDoc}
|
||||
{@const optName = (optDoc as any).id}
|
||||
<FormGroup>
|
||||
<Resource
|
||||
edit={true}
|
||||
index={optName}
|
||||
specs={specsWithPlaceholder(optDoc)}
|
||||
type={(optDoc as any).type || "string"}
|
||||
readonly={!!(optDoc as any).autoFill}
|
||||
bind:value={runOptions[optName]}
|
||||
/>
|
||||
</FormGroup>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<Alert color="info" class="mb-0">
|
||||
<Icon name="info-circle"></Icon>
|
||||
{$t("checkers.run-check.no-options")}
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if rules.length >= 1}
|
||||
<hr />
|
||||
<FormGroup>
|
||||
<Label>{$t("checkers.run-check.rules")}</Label>
|
||||
{#each rules as rule, idx}
|
||||
{@const isActive = activeRules[idx] !== false}
|
||||
<div class="form-check">
|
||||
<Input
|
||||
type="checkbox"
|
||||
id="run-check-rule-{idx}"
|
||||
label={rule.name ?? idx}
|
||||
checked={isActive}
|
||||
onchange={() => (activeRules[idx] = !isActive)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</FormGroup>
|
||||
{/if}
|
||||
</Form>
|
||||
{:catch error}
|
||||
<Alert color="danger">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
{$t("checkers.run-check.error-loading-options", { error: error.message })}
|
||||
</Alert>
|
||||
{/await}
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button type="button" color="secondary" onclick={toggle} disabled={triggering}>
|
||||
{$t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form="run-check-modal"
|
||||
color="primary"
|
||||
onclick={handleRunCheck}
|
||||
disabled={triggering}
|
||||
>
|
||||
{#if triggering}
|
||||
<Spinner size="sm" class="me-1" />
|
||||
{:else}
|
||||
<Icon name="play-fill"></Icon>
|
||||
{/if}
|
||||
{$t("checkers.run-check.run-button")}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
|
@ -46,6 +46,7 @@ interface Params {
|
|||
countdown?: string;
|
||||
error?: string;
|
||||
options?: string;
|
||||
key?: string;
|
||||
// add more parameters that are used here
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,11 +124,12 @@ export function downloadBlob(content: string, filename: string, mime: string) {
|
|||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function formatCheckDate(date: string | undefined): string {
|
||||
export function formatCheckDate(date: string | Date | undefined): string {
|
||||
if (!date) return "";
|
||||
try {
|
||||
if (date instanceof Date) return date.toLocaleString();
|
||||
return new Date(date).toLocaleString();
|
||||
} catch {
|
||||
return date;
|
||||
return String(date);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
web/src/routes/checkers/+layout.ts
Normal file
31
web/src/routes/checkers/+layout.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// 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/>.
|
||||
|
||||
import { type Load } from "@sveltejs/kit";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { checkers, refreshCheckers } from "$lib/stores/checkers";
|
||||
|
||||
export const load: Load = async ({ parent }) => {
|
||||
if (get(checkers) === undefined) refreshCheckers();
|
||||
|
||||
return await parent();
|
||||
};
|
||||
99
web/src/routes/checkers/+page.svelte
Normal file
99
web/src/routes/checkers/+page.svelte
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<!--
|
||||
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 {
|
||||
Card,
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Row,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import PageTitle from "$lib/components/PageTitle.svelte";
|
||||
import CheckersAvailabilityTable from "$lib/components/checkers/CheckersAvailabilityTable.svelte";
|
||||
import { t } from "$lib/translations";
|
||||
import { checkers } from "$lib/stores/checkers";
|
||||
|
||||
let searchQuery = $state("");
|
||||
|
||||
let filteredCheckers = $derived(
|
||||
$checkers
|
||||
? Object.entries($checkers).filter(([name]) =>
|
||||
name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
: [],
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t("checkers.title")} - happyDomain</title>
|
||||
</svelte:head>
|
||||
|
||||
<Container class="flex-fill my-5">
|
||||
<PageTitle title={$t("checkers.title")} subtitle={$t("checkers.description")}>
|
||||
{#if $checkers}
|
||||
{$t("checkers.available-count", {
|
||||
count: Object.keys($checkers).length,
|
||||
})}
|
||||
{/if}
|
||||
</PageTitle>
|
||||
|
||||
<Row class="mb-4 mt-3">
|
||||
<Col md={8} lg={6}>
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="search"></Icon>
|
||||
</InputGroupText>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={$t("checkers.search-placeholder")}
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{#if !$checkers}
|
||||
<Card body>
|
||||
<p class="text-center mb-0">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
{$t("checkers.loading")}
|
||||
</p>
|
||||
</Card>
|
||||
{:else}
|
||||
{#if Object.keys($checkers).length == 0}
|
||||
<p class="text-center text-muted py-4">
|
||||
{$t("checkers.no-checkers")}
|
||||
</p>
|
||||
{:else}
|
||||
<CheckersAvailabilityTable
|
||||
checkers={filteredCheckers}
|
||||
basePath="/checkers"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</Container>
|
||||
53
web/src/routes/checkers/[checkerId]/+layout.svelte
Normal file
53
web/src/routes/checkers/[checkerId]/+layout.svelte
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<!--
|
||||
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 { Col, Container, Row } from "@sveltestrap/sveltestrap";
|
||||
import { page } from "$app/state";
|
||||
|
||||
import CheckerSidebar from "$lib/components/checkers/CheckerSidebar.svelte";
|
||||
|
||||
let {
|
||||
children,
|
||||
}: {
|
||||
children?: import("svelte").Snippet;
|
||||
} = $props();
|
||||
|
||||
let checkerId = $derived(page.params.checkerId!);
|
||||
</script>
|
||||
|
||||
<Container fluid class="d-flex flex-column flex-fill">
|
||||
<Row class="flex-fill">
|
||||
<Col
|
||||
sm={4}
|
||||
md={3}
|
||||
class="py-3 sticky-top d-flex flex-column"
|
||||
style="background-color: #edf5f2; overflow-y: auto; max-height: 100vh; z-index: 0"
|
||||
>
|
||||
<CheckerSidebar currentCheckId={checkerId} />
|
||||
</Col>
|
||||
<Col sm={8} md={9} class="d-flex flex-column">
|
||||
{@render children?.()}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
239
web/src/routes/checkers/[checkerId]/+page.svelte
Normal file
239
web/src/routes/checkers/[checkerId]/+page.svelte
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
<!--
|
||||
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,
|
||||
Badge,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Col,
|
||||
Icon,
|
||||
Row,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { page } from "$app/state";
|
||||
|
||||
import { t } from "$lib/translations";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
import PageTitle from "$lib/components/PageTitle.svelte";
|
||||
import { getCheckStatus, getCheckOptions, updateCheckOptions } from "$lib/api/checkers";
|
||||
import type { HappydnsCheckerAvailability, HappydnsCheckerOptionDocumentation, HappydnsCheckerOptionsPositional } from "$lib/api-base/types.gen";
|
||||
import CheckerRulesCard from "$lib/components/checkers/CheckerRulesCard.svelte";
|
||||
import CheckerOptionsPanel from "$lib/components/checkers/CheckerOptionsPanel.svelte";
|
||||
|
||||
function getAvailBadges(availability: HappydnsCheckerAvailability | undefined): { label: string; color: string }[] {
|
||||
if (!availability) return [];
|
||||
const badges = [];
|
||||
if (availability.applyToDomain) badges.push({ label: $t("checkers.availability.domain-level"), color: "success" });
|
||||
if (availability.applyToZone) badges.push({ label: $t("checkers.availability.zone-level"), color: "info" });
|
||||
if (availability.applyToService) badges.push({ label: $t("checkers.availability.service-level"), color: "primary" });
|
||||
return badges;
|
||||
}
|
||||
|
||||
let checkerId = $derived(page.params.checkerId!);
|
||||
|
||||
let checkStatusPromise = $derived(getCheckStatus(checkerId));
|
||||
let checkOptionsPromise = $derived(getCheckOptions(checkerId));
|
||||
let optionValues = $state<Record<string, unknown>>({});
|
||||
let inheritedValues = $state<Record<string, unknown>>({});
|
||||
let resolvedStatus = $state<any>(null);
|
||||
let saving = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
checkStatusPromise.then((status) => {
|
||||
resolvedStatus = status;
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
checkOptionsPromise.then((positionals: HappydnsCheckerOptionsPositional[]) => {
|
||||
const current = positionals.length > 0 ? (positionals[positionals.length - 1]?.options ?? {}) : {};
|
||||
const inherited: Record<string, unknown> = {};
|
||||
for (let i = 0; i < positionals.length - 1; i++) {
|
||||
for (const [k, v] of Object.entries(positionals[i].options ?? {})) {
|
||||
inherited[k] = v;
|
||||
}
|
||||
}
|
||||
optionValues = { ...current };
|
||||
inheritedValues = inherited;
|
||||
});
|
||||
});
|
||||
|
||||
async function saveOptions() {
|
||||
saving = true;
|
||||
try {
|
||||
await updateCheckOptions(checkerId, optionValues);
|
||||
checkOptionsPromise = getCheckOptions(checkerId);
|
||||
toasts.addToast({
|
||||
message: $t("checkers.messages.options-updated"),
|
||||
type: "success",
|
||||
timeout: 5000,
|
||||
});
|
||||
} catch (error) {
|
||||
toasts.addErrorToast({
|
||||
message: $t("checkers.messages.update-failed", { error: String(error) }),
|
||||
timeout: 10000,
|
||||
});
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanOrphanedOptions(allEditableOpts: HappydnsCheckerOptionDocumentation[]) {
|
||||
const validOptIds = new Set(allEditableOpts.map((opt) => opt.id));
|
||||
const cleanedOptions: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(optionValues)) {
|
||||
if (validOptIds.has(key)) {
|
||||
cleanedOptions[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
saving = true;
|
||||
try {
|
||||
await updateCheckOptions(checkerId, cleanedOptions);
|
||||
checkOptionsPromise = getCheckOptions(checkerId);
|
||||
toasts.addToast({
|
||||
message: $t("checkers.messages.options-cleaned"),
|
||||
type: "success",
|
||||
timeout: 5000,
|
||||
});
|
||||
} catch (error) {
|
||||
toasts.addErrorToast({
|
||||
message: $t("checkers.messages.clean-failed", { error: String(error) }),
|
||||
timeout: 10000,
|
||||
});
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getOrphanedOptions(
|
||||
allEditableOpts: HappydnsCheckerOptionDocumentation[],
|
||||
readOnlyGroups: { opts: HappydnsCheckerOptionDocumentation[] }[],
|
||||
): string[] {
|
||||
const validOptIds = new Set(allEditableOpts.map((opt) => opt.id));
|
||||
for (const group of readOnlyGroups) {
|
||||
for (const opt of group.opts) {
|
||||
validOptIds.add(opt.id);
|
||||
}
|
||||
}
|
||||
return Object.keys(optionValues).filter((key) => !validOptIds.has(key));
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{resolvedStatus?.name ?? checkerId} - {$t("checkers.title")} - happyDomain</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex-fill mt-1 mb-5">
|
||||
<PageTitle title={resolvedStatus?.name ?? checkerId}></PageTitle>
|
||||
|
||||
{#await checkStatusPromise}
|
||||
<Card body>
|
||||
<p class="text-center mb-0">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
{$t("checkers.loading-info")}
|
||||
</p>
|
||||
</Card>
|
||||
{:then status}
|
||||
{#if status}
|
||||
{@const adminOpts = status.options?.adminOpts || []}
|
||||
{@const userOpts = status.options?.userOpts || []}
|
||||
{@const rulesAdminOpts = (status.rules || []).flatMap((r) => r.options?.adminOpts || [])}
|
||||
{@const rulesUserOpts = (status.rules || []).flatMap((r) => r.options?.userOpts || [])}
|
||||
{@const allEditableOpts = [...adminOpts, ...userOpts, ...rulesAdminOpts, ...rulesUserOpts]}
|
||||
{@const editableGroups = [
|
||||
{ label: $t("checkers.detail.admin-options"), opts: adminOpts },
|
||||
{ label: $t("checkers.detail.configuration"), opts: userOpts },
|
||||
]}
|
||||
{@const readOnlyGroups = [
|
||||
{ key: "domainOpts", label: $t("checkers.option-groups.domain-settings"), opts: status.options?.domainOpts || [] },
|
||||
{ key: "serviceOpts", label: $t("checkers.option-groups.service-settings"), opts: status.options?.serviceOpts || [] },
|
||||
{ key: "runOpts", label: $t("checkers.option-groups.checker-parameters"), opts: status.options?.runOpts || [] },
|
||||
]}
|
||||
{@const orphanedOpts = getOrphanedOptions(allEditableOpts, readOnlyGroups)}
|
||||
<Row class="mb-4">
|
||||
<Col md={6}>
|
||||
<Card class="mb-3">
|
||||
<CardHeader>
|
||||
<strong>{$t("checkers.detail.checker-information")}</strong>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">{$t("checkers.detail.name")}</dt>
|
||||
<dd class="col-sm-8">{status.name}</dd>
|
||||
|
||||
<dt class="col-sm-4">{$t("checkers.detail.availability")}</dt>
|
||||
<dd class="col-sm-8">
|
||||
{#each getAvailBadges(status.availability) as badge}
|
||||
<Badge color={badge.color}>{badge.label}</Badge>
|
||||
{:else}
|
||||
<Badge color="secondary">
|
||||
{$t("checkers.availability.general")}
|
||||
</Badge>
|
||||
{/each}
|
||||
</dd>
|
||||
</dl>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{#if status.rules && status.rules.length > 0}
|
||||
<CheckerRulesCard
|
||||
rules={status.rules}
|
||||
bind:optionValues
|
||||
{inheritedValues}
|
||||
{saving}
|
||||
onsave={saveOptions}
|
||||
/>
|
||||
{/if}
|
||||
</Col>
|
||||
|
||||
<Col md={6}>
|
||||
<CheckerOptionsPanel
|
||||
{checkOptionsPromise}
|
||||
{editableGroups}
|
||||
{readOnlyGroups}
|
||||
bind:optionValues
|
||||
{inheritedValues}
|
||||
{saving}
|
||||
onsave={saveOptions}
|
||||
{orphanedOpts}
|
||||
onclean={() => cleanOrphanedOptions(allEditableOpts)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{:else}
|
||||
<Alert color="danger">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
{$t("checkers.checker-info-not-found")}
|
||||
</Alert>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<Alert color="danger">
|
||||
<Icon name="exclamation-triangle-fill"></Icon>
|
||||
{$t("checkers.error-loading-checker", { error: error.message })}
|
||||
</Alert>
|
||||
{/await}
|
||||
</div>
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
import { Button, Col, Container, Icon, Row, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { deleteDomain as APIDeleteDomain } from "$lib/api/domains";
|
||||
import ChecksSidebarContent from "$lib/components/checkers/ChecksSidebarContent.svelte";
|
||||
import SelectDomain from "$lib/components/domains/SelectDomain.svelte";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import type { ZoneMeta } from "$lib/model/zone";
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
import ServiceDetailsOffcanvas from "./ServiceDetailsOffcanvas.svelte";
|
||||
import ServiceSidebar from "./ServiceSidebar.svelte";
|
||||
import ZoneSidebar from "./ZoneSidebar.svelte";
|
||||
import { thisZone } from "$lib/stores/thiszone";
|
||||
|
||||
interface Props {
|
||||
data: { domain: Domain };
|
||||
|
|
@ -57,13 +59,15 @@
|
|||
"/domains/" +
|
||||
encodeURIComponent(domainLink(dn)) +
|
||||
(page.route.id
|
||||
? page.route.id.startsWith("/domains/[dn]/logs")
|
||||
? "/logs"
|
||||
: page.route.id.startsWith("/domains/[dn]/history")
|
||||
? "/history"
|
||||
: page.route.id.startsWith("/domains/[dn]/[[historyid]]/export")
|
||||
? "/export"
|
||||
: ""
|
||||
? page.route.id.startsWith("/domains/[dn]/checks")
|
||||
? "/checks"
|
||||
: page.route.id.startsWith("/domains/[dn]/logs")
|
||||
? "/logs"
|
||||
: page.route.id.startsWith("/domains/[dn]/history")
|
||||
? "/history"
|
||||
: page.route.id.startsWith("/domains/[dn]/[[historyid]]/export")
|
||||
? "/export"
|
||||
: ""
|
||||
: ""),
|
||||
);
|
||||
}
|
||||
|
|
@ -145,7 +149,15 @@
|
|||
<SelectDomain bind:selectedDomain />
|
||||
</div>
|
||||
|
||||
{#if page.route.id && (page.route.id.startsWith("/domains/[dn]/history") || page.route.id.startsWith("/domains/[dn]/logs") || page.route.id.startsWith("/domains/[dn]/[[historyid]]/export"))}
|
||||
{#if page.route.id && page.route.id.startsWith("/domains/[dn]/checks")}
|
||||
<ChecksSidebarContent
|
||||
domain={data.domain}
|
||||
checksBase={"/domains/" +
|
||||
encodeURIComponent(domainLink(selectedDomain)) +
|
||||
"/checks"}
|
||||
backHref={"/domains/" + encodeURIComponent(domainLink(selectedDomain))}
|
||||
/>
|
||||
{:else if page.route.id && (page.route.id.startsWith("/domains/[dn]/history") || page.route.id.startsWith("/domains/[dn]/logs") || page.route.id.startsWith("/domains/[dn]/[[historyid]]/export"))}
|
||||
<a
|
||||
href="/domains/{encodeURIComponent(domainLink(selectedDomain))}"
|
||||
class="sidebar-back d-flex align-items-center gap-1 mt-3 text-muted text-decoration-none fw-semibold"
|
||||
|
|
@ -153,6 +165,32 @@
|
|||
<Icon name="chevron-left" />
|
||||
{$t("zones.return-to")}
|
||||
</a>
|
||||
{:else if page.route.id && page.route.id.startsWith("/domains/[dn]/[[historyid]]/[subdomain]/[serviceid]/checks")}
|
||||
<ChecksSidebarContent
|
||||
domain={data.domain}
|
||||
checksBase={"/domains/" +
|
||||
encodeURIComponent(domainLink(selectedDomain)) +
|
||||
"/" +
|
||||
encodeURIComponent(page.data.history ?? "") +
|
||||
"/" +
|
||||
encodeURIComponent(page.params.subdomain ?? "") +
|
||||
"/" +
|
||||
encodeURIComponent(page.data.serviceid ?? "") +
|
||||
"/checks"}
|
||||
backHref={"/domains/" +
|
||||
encodeURIComponent(domainLink(selectedDomain)) +
|
||||
"/" +
|
||||
encodeURIComponent(page.data.history ?? "") +
|
||||
"/" +
|
||||
encodeURIComponent(page.params.subdomain ?? "") +
|
||||
"/" +
|
||||
encodeURIComponent(page.data.serviceid ?? "")}
|
||||
serviceContext={{
|
||||
zoneId: page.data.zoneId ?? "",
|
||||
subdomain: page.data.subdomain ?? "",
|
||||
serviceid: page.data.serviceid ?? "",
|
||||
}}
|
||||
/>
|
||||
{:else if page.route.id === "/domains/[dn]/[[historyid]]/[subdomain]/[serviceid]"}
|
||||
<ServiceSidebar
|
||||
origin={data.domain}
|
||||
|
|
@ -169,9 +207,8 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
<div class="flex-fill"></div>
|
||||
|
||||
{#if !(data.domain.zone_history && $domains_idx[selectedDomain] && data.domain.id === $domains_idx[selectedDomain].id && selectedHistory)}
|
||||
<div class="flex-fill"></div>
|
||||
<Button
|
||||
color="danger"
|
||||
class="mt-3"
|
||||
|
|
@ -186,7 +223,8 @@
|
|||
{/if}
|
||||
{$t("domains.stop")}
|
||||
</Button>
|
||||
{:else}
|
||||
{:else if $domains_idx[data.domain.id] && $thisZone}
|
||||
<div class="flex-fill"></div>
|
||||
<ButtonZonePublish domain={data.domain} history={selectedHistory} />
|
||||
{/if}
|
||||
{:else}
|
||||
|
|
@ -195,9 +233,14 @@
|
|||
</div>
|
||||
{/if}
|
||||
</Col>
|
||||
<Col sm={8} md={9} class="d-flex">
|
||||
<div
|
||||
class="col-sm-8 col-md-9 d-flex"
|
||||
class:p-0={page.route &&
|
||||
(page.route.id == "/domains/[dn]/checks/[checkerId]/executions/[execId]" ||
|
||||
page.route.id == "/domains/[dn]/[[historyid]]/[subdomain]/[serviceid]/checks/[checkerId]/executions/[execId]")}
|
||||
>
|
||||
{@render children?.()}
|
||||
</Col>
|
||||
</div>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Icon,
|
||||
Input,
|
||||
|
|
@ -40,6 +41,7 @@
|
|||
Spinner,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { listServiceCheckers } from "$lib/api/checkers";
|
||||
import { getServiceSpec } from "$lib/api/service_specs";
|
||||
import { deleteZoneService, updateZoneService } from "$lib/api/zone";
|
||||
import ServiceBadges from "./[[historyid]]/ServiceBadges.svelte";
|
||||
|
|
@ -48,10 +50,12 @@
|
|||
import { collectRRs } from "$lib/dns";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
import { checkers } from "$lib/stores/checkers";
|
||||
import { domainLink } from "$lib/stores/domains";
|
||||
import { servicesSpecs, servicesSpecsLoaded } from "$lib/stores/services";
|
||||
import { thisZone } from "$lib/stores/thiszone";
|
||||
import { t } from "$lib/translations";
|
||||
import { getStatusColor, getStatusI18nKey } from "$lib/utils";
|
||||
|
||||
interface Props {
|
||||
domain: Domain;
|
||||
|
|
@ -97,6 +101,19 @@
|
|||
service._svctype !== "abstract.NSOnlyOrigin",
|
||||
);
|
||||
|
||||
const zoneId = $derived(selectedHistory || domain.zone_history?.[0] || "");
|
||||
const subdomain = $derived(service._domain || "@");
|
||||
const checksPromise = $derived(
|
||||
service._id && zoneId
|
||||
? listServiceCheckers(domain.id, zoneId, subdomain, service._id)
|
||||
: null,
|
||||
);
|
||||
const serviceChecksPath = $derived(
|
||||
service._id && zoneId
|
||||
? `/domains/${encodeURIComponent(domain.domain)}/${encodeURIComponent(zoneId)}/${encodeURIComponent(service._domain || "@")}/${encodeURIComponent(service._id)}/checks`
|
||||
: null,
|
||||
);
|
||||
|
||||
let ttlSaveInProgress = $state(false);
|
||||
|
||||
function saveTtl() {
|
||||
|
|
@ -156,6 +173,59 @@
|
|||
{/await}
|
||||
{/if}
|
||||
<PropagationStatus propagatedAt={service._propagated_at} />
|
||||
{#if checksPromise}
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<small class="text-muted fw-semibold text-uppercase">
|
||||
{$t("checkers.service-checks")}
|
||||
</small>
|
||||
{#if serviceChecksPath}
|
||||
<a href={serviceChecksPath} class="small" onclick={() => (isOpen = false)}>
|
||||
{$t("checkers.view-all")} →
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{#await checksPromise}
|
||||
<div class="text-center py-2">
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
{:then checkerStatuses}
|
||||
{#if checkerStatuses && checkerStatuses.length > 0}
|
||||
<div class="d-flex flex-column gap-1">
|
||||
{#each checkerStatuses as check}
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a
|
||||
href={serviceChecksPath +
|
||||
"/" +
|
||||
check.id +
|
||||
"/executions"}
|
||||
class="text-truncate me-2"
|
||||
onclick={() => (isOpen = false)}
|
||||
>
|
||||
{$checkers?.[check.id ?? ""]?.name ??
|
||||
check.name ??
|
||||
check.id}
|
||||
</a>
|
||||
{#if check.latestExecution?.result}
|
||||
<Badge color={getStatusColor(check.latestExecution.result.status)}>
|
||||
{$t(getStatusI18nKey(check.latestExecution.result.status))}
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge color="secondary">
|
||||
{$t("checkers.status.not-run")}
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<small class="text-muted fst-italic">{$t("checkers.no-checks")}</small>
|
||||
{/if}
|
||||
{:catch}
|
||||
<small class="text-danger">{$t("checkers.load-error")}</small>
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-fill"></div>
|
||||
{#if service._id}
|
||||
<div class="d-flex align-items-center gap-2 mt-2">
|
||||
|
|
|
|||
|
|
@ -113,6 +113,9 @@
|
|||
<DropdownItem href={`/domains/${domainLink(selectedDomain)}/logs`}>
|
||||
{$t("domains.actions.audit")}
|
||||
</DropdownItem>
|
||||
<DropdownItem href={`/domains/${domainLink(selectedDomain)}/checks`}>
|
||||
{$t("domains.actions.checks")}
|
||||
</DropdownItem>
|
||||
<DropdownItem divider />
|
||||
<DropdownItem on:click={viewZone} disabled={!$sortedDomains}>
|
||||
{$t("domains.actions.view")}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@
|
|||
import type { Domain } from "$lib/model/domain";
|
||||
import type { ServiceWithValue } from "$lib/model/service.svelte";
|
||||
import { servicesSpecs, servicesSpecsLoaded } from "$lib/stores/services";
|
||||
import { thisZone } from "$lib/stores/thiszone";
|
||||
import { t } from "$lib/translations";
|
||||
import { getStatusColor, getStatusIcon } from "$lib/utils";
|
||||
import PropagationCountdown from "$lib/components/services/PropagationCountdown.svelte";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -83,7 +85,11 @@
|
|||
<Icon name="plus-circle" /> {$t("service.new")}
|
||||
{/if}
|
||||
</CardTitle>
|
||||
{#if service?._propagated_at && isPropagating}
|
||||
{#if service?._id && $thisZone?.services_check_status?.[service._id] !== undefined}
|
||||
<span class={"text-" + getStatusColor($thisZone.services_check_status![service._id])}>
|
||||
<Icon name={getStatusIcon($thisZone.services_check_status![service._id])} />
|
||||
</span>
|
||||
{:else if service?._propagated_at && isPropagating}
|
||||
<Progress
|
||||
class="rounded"
|
||||
barClassName="px-2 text-end"
|
||||
|
|
|
|||
|
|
@ -178,6 +178,14 @@
|
|||
|
||||
<div class="d-flex justify-content-end align-items-center gap-2 mt-3">
|
||||
{#if service._id}
|
||||
<Button
|
||||
color="info"
|
||||
outline
|
||||
href="checks"
|
||||
>
|
||||
<Icon name="shield-check" />
|
||||
{$t("checkers.title")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={addServiceInProgress}
|
||||
form="addSvcForm"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import type { Load } from "@sveltejs/kit";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { checkers, refreshCheckers } from "$lib/stores/checkers";
|
||||
|
||||
export const load: Load = async ({ parent, params }) => {
|
||||
const data = await parent();
|
||||
|
||||
if (!get(checkers)) await refreshCheckers();
|
||||
|
||||
const subdomain = params.subdomain === "@" ? "" : params.subdomain;
|
||||
const serviceid = params.serviceid;
|
||||
|
||||
return {
|
||||
...data,
|
||||
subdomain,
|
||||
serviceid,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<!--
|
||||
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 { t } from "$lib/translations";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import { fqdn } from "$lib/dns";
|
||||
import { domainLink } from "$lib/stores/domains";
|
||||
import CheckerListPage from "$lib/components/checkers/CheckerListPage.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 checksBase = $derived(
|
||||
`/domains/${domainLink(domain.id)}/${encodeURIComponent(zoneId)}/${encodeURIComponent(page.params.subdomain!)}/${encodeURIComponent(serviceid)}/checks`,
|
||||
);
|
||||
</script>
|
||||
|
||||
<CheckerListPage
|
||||
scope={{ domainId: domain.id, zoneId, subdomain, serviceId: serviceid }}
|
||||
{checksBase}
|
||||
title={$t("checkers.list.title") + fqdn(subdomain, domain.domain)}
|
||||
domainName={fqdn(subdomain, domain.domain)}
|
||||
filterAvailability="applyToService"
|
||||
/>
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<!--
|
||||
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 { t } from "$lib/translations";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import { fqdn } from "$lib/dns";
|
||||
import { domainLink } from "$lib/stores/domains";
|
||||
import CheckerConfigPage from "$lib/components/checkers/CheckerConfigPage.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 checksBase = $derived(
|
||||
`/domains/${domainLink(domain.id)}/${encodeURIComponent(zoneId)}/${encodeURIComponent(page.params.subdomain!)}/${encodeURIComponent(serviceid)}/checks`,
|
||||
);
|
||||
</script>
|
||||
|
||||
<CheckerConfigPage
|
||||
scope={{ domainId: domain.id, zoneId, subdomain, serviceId: serviceid }}
|
||||
{checksBase}
|
||||
{checkerId}
|
||||
domainName={fqdn(subdomain, domain.domain)}
|
||||
editableGroups={(status) => [
|
||||
{
|
||||
label: $t("checkers.option-groups.service-settings"),
|
||||
opts: status.options?.serviceOpts || [],
|
||||
},
|
||||
{
|
||||
label: $t("checkers.detail.admin-options"),
|
||||
opts: status.options?.adminOpts || [],
|
||||
},
|
||||
{
|
||||
label: $t("checkers.detail.configuration"),
|
||||
opts: status.options?.userOpts || [],
|
||||
},
|
||||
]}
|
||||
readOnlyGroups={(status) => [
|
||||
{
|
||||
key: "domainOpts",
|
||||
label: $t("checkers.option-groups.domain-settings"),
|
||||
opts: status.options?.domainOpts || [],
|
||||
},
|
||||
{
|
||||
key: "runOpts",
|
||||
label: $t("checkers.option-groups.checker-parameters"),
|
||||
opts: status.options?.runOpts || [],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<!--
|
||||
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 { domainLink } from "$lib/stores/domains";
|
||||
import ExecutionListPage from "$lib/components/checkers/ExecutionListPage.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 checksBase = $derived(
|
||||
`/domains/${domainLink(domain.id)}/${encodeURIComponent(zoneId)}/${encodeURIComponent(page.params.subdomain!)}/${encodeURIComponent(serviceid)}/checks`,
|
||||
);
|
||||
</script>
|
||||
|
||||
<ExecutionListPage
|
||||
scope={{ domainId: domain.id, zoneId, subdomain, serviceId: serviceid }}
|
||||
{checksBase}
|
||||
{checkerId}
|
||||
domainName={fqdn(subdomain, domain.domain)}
|
||||
/>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<!--
|
||||
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 ExecutionDetailPage from "$lib/components/checkers/ExecutionDetailPage.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>
|
||||
|
||||
<ExecutionDetailPage
|
||||
scope={{ domainId: domain.id, zoneId, subdomain, serviceId: serviceid }}
|
||||
{checkerId}
|
||||
{execId}
|
||||
/>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<!--
|
||||
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)}
|
||||
/>
|
||||
14
web/src/routes/domains/[dn]/checks/+layout.ts
Normal file
14
web/src/routes/domains/[dn]/checks/+layout.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { Load } from "@sveltejs/kit";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { checkers, refreshCheckers } from "$lib/stores/checkers";
|
||||
|
||||
export const load: Load = async ({ parent }) => {
|
||||
const data = await parent();
|
||||
|
||||
if (!get(checkers)) await refreshCheckers();
|
||||
|
||||
return {
|
||||
...data,
|
||||
};
|
||||
};
|
||||
42
web/src/routes/domains/[dn]/checks/+page.svelte
Normal file
42
web/src/routes/domains/[dn]/checks/+page.svelte
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!--
|
||||
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 { t } from "$lib/translations";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import { domainLink } from "$lib/stores/domains";
|
||||
import CheckerListPage from "$lib/components/checkers/CheckerListPage.svelte";
|
||||
|
||||
let domain: Domain = $derived(page.data.domain);
|
||||
let checksBase = $derived(`/domains/${domainLink(domain.id)}/checks`);
|
||||
</script>
|
||||
|
||||
<CheckerListPage
|
||||
scope={{ domainId: domain.id }}
|
||||
{checksBase}
|
||||
title={$t("checkers.list.title") + domain.domain}
|
||||
domainName={domain.domain}
|
||||
filterAvailability="applyToDomain"
|
||||
/>
|
||||
68
web/src/routes/domains/[dn]/checks/[checkerId]/+page.svelte
Normal file
68
web/src/routes/domains/[dn]/checks/[checkerId]/+page.svelte
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<!--
|
||||
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 { t } from "$lib/translations";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import { domainLink } from "$lib/stores/domains";
|
||||
import CheckerConfigPage from "$lib/components/checkers/CheckerConfigPage.svelte";
|
||||
|
||||
let domain: Domain = $derived(page.data.domain);
|
||||
let checkerId = $derived(page.params.checkerId!);
|
||||
let checksBase = $derived(`/domains/${domainLink(domain.id)}/checks`);
|
||||
</script>
|
||||
|
||||
<CheckerConfigPage
|
||||
scope={{ domainId: domain.id }}
|
||||
{checksBase}
|
||||
{checkerId}
|
||||
domainName={domain.domain}
|
||||
editableGroups={(status) => [
|
||||
{
|
||||
label: $t("checkers.option-groups.domain-settings"),
|
||||
opts: status.options?.domainOpts || [],
|
||||
},
|
||||
{
|
||||
label: $t("checkers.detail.admin-options"),
|
||||
opts: status.options?.adminOpts || [],
|
||||
},
|
||||
{
|
||||
label: $t("checkers.detail.configuration"),
|
||||
opts: status.options?.userOpts || [],
|
||||
},
|
||||
]}
|
||||
readOnlyGroups={(status) => [
|
||||
{
|
||||
key: "serviceOpts",
|
||||
label: $t("checkers.option-groups.service-settings"),
|
||||
opts: status.options?.serviceOpts || [],
|
||||
},
|
||||
{
|
||||
key: "runOpts",
|
||||
label: $t("checkers.option-groups.checker-parameters"),
|
||||
opts: status.options?.runOpts || [],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<!--
|
||||
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 { domainLink } from "$lib/stores/domains";
|
||||
import ExecutionListPage from "$lib/components/checkers/ExecutionListPage.svelte";
|
||||
|
||||
let domain: Domain = $derived(page.data.domain);
|
||||
let checkerId = $derived(page.params.checkerId!);
|
||||
let checksBase = $derived(`/domains/${domainLink(domain.id)}/checks`);
|
||||
</script>
|
||||
|
||||
<ExecutionListPage
|
||||
scope={{ domainId: domain.id }}
|
||||
{checksBase}
|
||||
{checkerId}
|
||||
domainName={domain.domain}
|
||||
/>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<!--
|
||||
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 ExecutionDetailPage from "$lib/components/checkers/ExecutionDetailPage.svelte";
|
||||
|
||||
let domain: Domain = $derived(page.data.domain);
|
||||
let checkerId = $derived(page.params.checkerId!);
|
||||
let execId = $derived(page.params.execId!);
|
||||
</script>
|
||||
|
||||
<ExecutionDetailPage
|
||||
scope={{ domainId: domain.id }}
|
||||
{checkerId}
|
||||
{execId}
|
||||
/>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<!--
|
||||
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 type { HappydnsObservationSnapshot } from "$lib/api-base/types.gen";
|
||||
import ObservationReportCard from "$lib/components/checkers/ObservationReportCard.svelte";
|
||||
|
||||
interface Props {
|
||||
observations: HappydnsObservationSnapshot;
|
||||
}
|
||||
|
||||
let { observations }: Props = $props();
|
||||
</script>
|
||||
|
||||
<ObservationReportCard {observations} />
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<!--
|
||||
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}
|
||||
/>
|
||||
Loading…
Add table
Add a link
Reference in a new issue