Initial commit: DNS Test - DNS health checking tool
Go backend (miekg/dns) + Nuxt 3 frontend (Tailwind CSS v4). 8 check categories, 52 checks total: - Overview: @ record, WWW, MX with ASN/provider lookup - Domain Registration: expiry, registrar (RDAP + whois fallback) - Parent Delegation: NS records, glue, consistency - Nameservers: 17 checks (reachability, auth, recursion, TCP/UDP, AXFR, etc.) - SOA: serial consistency, timing values - Mail (MX): 11 checks (CNAME, PTR, public IPs, consistency) - Mail Auth: SPF, DKIM, DMARC - WWW: A record, CNAME Features: - SSE streaming (results appear as each category completes) - SQLite history (modernc.org/sqlite) - Rate limiting, CORS, request logging - Dark mode, responsive design
This commit is contained in:
173
frontend/app/components/ReportSection.vue
Normal file
173
frontend/app/components/ReportSection.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
|
||||
<button
|
||||
@click="expanded = !expanded"
|
||||
class="flex w-full items-center justify-between px-5 py-4 text-left hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">{{ category.title }}</h3>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span v-if="passCount > 0" class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
||||
{{ passCount }} passed
|
||||
</span>
|
||||
<span v-if="warnCount > 0" class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400">
|
||||
{{ warnCount }} {{ warnCount === 1 ? 'warning' : 'warnings' }}
|
||||
</span>
|
||||
<span v-if="failCount > 0" class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
||||
{{ failCount }} {{ failCount === 1 ? 'problem' : 'problems' }}
|
||||
</span>
|
||||
<span v-if="infoCount > 0" class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
|
||||
{{ infoCount }} info
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-gray-400 transition-transform dark:text-gray-500"
|
||||
:class="{ 'rotate-180': expanded }"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div v-if="expanded" class="border-t border-gray-200 dark:border-gray-800">
|
||||
<!-- When filtering by status, show only those checks directly -->
|
||||
<template v-if="props.statusFilter">
|
||||
<CheckResult
|
||||
v-for="check in filteredByStatus"
|
||||
:key="check.id"
|
||||
:check="check"
|
||||
:default-expanded="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<!-- Problems (fail) — always shown expanded with details -->
|
||||
<CheckResult
|
||||
v-for="check in failChecks"
|
||||
:key="check.id"
|
||||
:check="check"
|
||||
:default-expanded="true"
|
||||
/>
|
||||
|
||||
<!-- Warnings — shown expanded with details -->
|
||||
<CheckResult
|
||||
v-for="check in warnChecks"
|
||||
:key="check.id"
|
||||
:check="check"
|
||||
:default-expanded="true"
|
||||
/>
|
||||
|
||||
<!-- Passed checks — condensed into a compact list -->
|
||||
<div v-if="passChecks.length > 0" class="border-b border-gray-100 last:border-b-0 dark:border-gray-800">
|
||||
<div
|
||||
class="flex cursor-pointer items-start gap-3 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
@click="passExpanded = !passExpanded"
|
||||
>
|
||||
<div class="mt-0.5 shrink-0">
|
||||
<StatusBadge status="pass" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ passChecks.length }} {{ passChecks.length === 1 ? 'check' : 'checks' }} passed
|
||||
</h4>
|
||||
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ passChecks.map(c => c.title).join(' · ') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-1 shrink-0 text-gray-400 dark:text-gray-500">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 transition-transform"
|
||||
:class="{ 'rotate-180': passExpanded }"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="passExpanded" class="border-t border-gray-100 bg-gray-50/50 dark:border-gray-800 dark:bg-gray-800/20">
|
||||
<CheckResult
|
||||
v-for="check in passChecks"
|
||||
:key="check.id"
|
||||
:check="check"
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info checks — condensed similarly -->
|
||||
<div v-if="infoChecks.length > 0" class="border-b border-gray-100 last:border-b-0 dark:border-gray-800">
|
||||
<div
|
||||
class="flex cursor-pointer items-start gap-3 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
@click="infoExpanded = !infoExpanded"
|
||||
>
|
||||
<div class="mt-0.5 shrink-0">
|
||||
<StatusBadge status="info" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ infoChecks.length }} informational {{ infoChecks.length === 1 ? 'note' : 'notes' }}
|
||||
</h4>
|
||||
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ infoChecks.map(c => c.title).join(' · ') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-1 shrink-0 text-gray-400 dark:text-gray-500">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 transition-transform"
|
||||
:class="{ 'rotate-180': infoExpanded }"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="infoExpanded" class="border-t border-gray-100 bg-gray-50/50 dark:border-gray-800 dark:bg-gray-800/20">
|
||||
<CheckResult
|
||||
v-for="check in infoChecks"
|
||||
:key="check.id"
|
||||
:check="check"
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Category, CheckStatus } from '~/types/dns'
|
||||
|
||||
const props = defineProps<{
|
||||
category: Category
|
||||
statusFilter?: CheckStatus | null
|
||||
}>()
|
||||
|
||||
const expanded = ref(true)
|
||||
const passExpanded = ref(false)
|
||||
const infoExpanded = ref(false)
|
||||
|
||||
const passChecks = computed(() => props.category.checks.filter(c => c.status === 'pass'))
|
||||
const warnChecks = computed(() => props.category.checks.filter(c => c.status === 'warn'))
|
||||
const failChecks = computed(() => props.category.checks.filter(c => c.status === 'fail'))
|
||||
const infoChecks = computed(() => props.category.checks.filter(c => c.status === 'info'))
|
||||
const filteredByStatus = computed(() => props.statusFilter ? props.category.checks.filter(c => c.status === props.statusFilter) : [])
|
||||
|
||||
const passCount = computed(() => passChecks.value.length)
|
||||
const warnCount = computed(() => warnChecks.value.length)
|
||||
const failCount = computed(() => failChecks.value.length)
|
||||
const infoCount = computed(() => infoChecks.value.length)
|
||||
</script>
|
||||
Reference in New Issue
Block a user