Files
dnstest/frontend/app/components/ReportSection.vue
robertas_stauskas a70f3262e0 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
2026-03-20 13:39:57 +02:00

174 lines
7.0 KiB
Vue

<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>