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
141 lines
5.0 KiB
Vue
141 lines
5.0 KiB
Vue
<template>
|
|
<div class="mx-auto max-w-5xl px-4 py-8 sm:px-6">
|
|
<div class="mb-8">
|
|
<DomainInput :initial-value="domain" @submit="onNewDomain" />
|
|
</div>
|
|
|
|
<!-- Loading state with progress -->
|
|
<div v-if="loading && !report" class="flex flex-col items-center justify-center py-20">
|
|
<div class="relative h-16 w-16">
|
|
<div class="absolute inset-0 rounded-full border-4 border-gray-200 dark:border-gray-700"></div>
|
|
<div class="absolute inset-0 animate-spin rounded-full border-4 border-transparent border-t-indigo-600 dark:border-t-indigo-400"></div>
|
|
</div>
|
|
<p class="mt-6 text-lg font-medium text-gray-600 dark:text-gray-400">
|
|
Checking DNS configuration...
|
|
</p>
|
|
<div class="mt-4 w-64">
|
|
<div class="flex h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
|
<div
|
|
class="bg-indigo-600 transition-all duration-500 dark:bg-indigo-400"
|
|
:style="{ width: `${(progress / totalCategories) * 100}%` }"
|
|
></div>
|
|
</div>
|
|
<p class="mt-2 text-center text-xs text-gray-400 dark:text-gray-500">
|
|
{{ progress }} / {{ totalCategories }} categories
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error state -->
|
|
<div v-else-if="error && !report" class="rounded-xl border border-red-200 bg-red-50 p-6 text-center dark:border-red-800 dark:bg-red-950/30">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
<h2 class="mt-4 text-lg font-semibold text-red-800 dark:text-red-300">DNS Check Failed</h2>
|
|
<p class="mt-2 text-red-600 dark:text-red-400">{{ error }}</p>
|
|
<button
|
|
@click="checkDomain(domain)"
|
|
class="mt-4 inline-flex items-center gap-2 rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
|
|
>
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Results (shown progressively as categories arrive) -->
|
|
<template v-if="report">
|
|
<div class="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
|
Report for <span class="text-indigo-600 dark:text-indigo-400">{{ report.domain }}</span>
|
|
</h1>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
<template v-if="loading">Checking...</template>
|
|
<template v-else>Completed in {{ report.duration_ms }}ms</template>
|
|
· {{ formatTimestamp(report.timestamp) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress bar while still loading -->
|
|
<div v-if="loading" class="mb-4">
|
|
<div class="flex h-1.5 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
|
<div
|
|
class="bg-indigo-600 transition-all duration-500 dark:bg-indigo-400"
|
|
:style="{ width: `${(progress / totalCategories) * 100}%` }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<SummaryBar :summary="report.summary" :active-filter="statusFilter" @filter="onFilter" />
|
|
|
|
<div class="mt-8 space-y-6">
|
|
<ReportSection
|
|
v-for="category in filteredCategories"
|
|
:key="category.name"
|
|
:category="category"
|
|
:status-filter="statusFilter"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
import type { CheckStatus } from '~/types/dns'
|
|
const { report, loading, error, progress, totalCategories, checkDomain } = useDnsCheck()
|
|
const statusFilter = ref<CheckStatus | null>(null)
|
|
|
|
const domain = computed(() => {
|
|
const params = route.params.domain
|
|
if (Array.isArray(params)) {
|
|
return params.join('/')
|
|
}
|
|
return params || ''
|
|
})
|
|
|
|
// Sort categories in consistent order as they arrive
|
|
const categoryOrder = ['overview', 'registration', 'parent', 'nameservers', 'soa', 'mx', 'mail-auth', 'www']
|
|
const sortedCategories = computed(() => {
|
|
if (!report.value) return []
|
|
return [...report.value.categories].sort((a, b) => {
|
|
return categoryOrder.indexOf(a.name) - categoryOrder.indexOf(b.name)
|
|
})
|
|
})
|
|
|
|
// Filter categories to only those with matching checks
|
|
const filteredCategories = computed(() => {
|
|
if (!statusFilter.value) return sortedCategories.value
|
|
return sortedCategories.value.filter(cat =>
|
|
cat.checks.some(c => c.status === statusFilter.value)
|
|
)
|
|
})
|
|
|
|
function onFilter(status: CheckStatus | null) {
|
|
statusFilter.value = status
|
|
}
|
|
|
|
function onNewDomain(newDomain: string) {
|
|
router.push(`/${newDomain}`)
|
|
}
|
|
|
|
function formatTimestamp(ts: string) {
|
|
try {
|
|
return new Date(ts).toLocaleString()
|
|
} catch {
|
|
return ts
|
|
}
|
|
}
|
|
|
|
watch(domain, (val) => {
|
|
if (val) {
|
|
checkDomain(val)
|
|
}
|
|
}, { immediate: true })
|
|
|
|
useHead({
|
|
title: () => domain.value ? `DNS Test - ${domain.value}` : 'DNS Test',
|
|
})
|
|
</script>
|