mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-10-29 08:04:58 +00:00
feat: domain monitoring (merge plan M2, phase 1)
Port of the domain-intelligence pipeline from the old Beszel fork, adapted to Dash's SQLite + no-PocketBase architecture: - internal/domain: RDAP via IANA bootstrap (cached), TCP WHOIS :43 fallback with multi-format parser, DNS (NS/MX/TXT/CNAME/A/AAAA), TLS leaf cert details, ip-api.com geolocation, provider detection (DNS/email/hosting/CA). HTML scraping, paid APIs, and exec-based whois deliberately dropped. - API: domains CRUD + synchronous refresh + domain_checks snapshot history; refresh runs inside the monitor sweep on interval_h. - UI: /domains + /domains/:id pages, add/edit dialog, domain board widget (favicon + expiry countdown), palette + nav entries, domainId picker in the widget dialog. - Test seam: Server.lookup injects the refresh; domain tests run hermetic against a stubbed result. Verified live: github.com resolves MarkMonitor expiry 2028-10, Route 53 DNS, GitHub Pages hosting, Sectigo cert with 69d left. Authored By: TDvorak <[email protected]> Generated with Devin (https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
1ff1b29ae7
commit
f9fd1d9cf2
18 files changed
+2376
-14
No files matched your search
@@ -0,0 +1,37 @@
|
||||
import type { WidgetData } from "@/widgets";
|
||||
import type { Item } from "@/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Board tile for a tracked domain: favicon, name, expiry countdown.
|
||||
export function DomainWidget({ item, data, error }: {
|
||||
item: Item;
|
||||
data: WidgetData | undefined;
|
||||
error: string | undefined;
|
||||
}) {
|
||||
const days = typeof data?.daysUntilExpiry === "number" ? data.daysUntilExpiry : undefined;
|
||||
const sslDays = typeof data?.sslDaysUntilExpiry === "number" ? data.sslDaysUntilExpiry : undefined;
|
||||
const expiring = data?.expiring === true;
|
||||
const domainId = typeof item.config?.domainId === "string" ? item.config.domainId : "";
|
||||
|
||||
return (
|
||||
<a
|
||||
href={domainId ? `/domains/${domainId}` : "/domains"}
|
||||
className="flex w-full items-center gap-2.5 text-left"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{typeof data?.faviconUrl === "string" && data.faviconUrl ? (
|
||||
<img src={data.faviconUrl} alt="" className="size-4 shrink-0 rounded-sm" loading="lazy" />
|
||||
) : null}
|
||||
<span className="min-w-0 flex-1 truncate text-[12.5px] font-medium">
|
||||
{typeof data?.name === "string" ? data.name : item.name}
|
||||
</span>
|
||||
<span className={cn(
|
||||
"shrink-0 font-mono text-[10.5px]",
|
||||
error ? "text-down" : expiring || (days !== undefined && days < 0) ? "text-down" : "text-text-faint",
|
||||
)}>
|
||||
{error ? "err" : days === undefined ? "…" : days < 0 ? "expired" : `${days}d`}
|
||||
{sslDays !== undefined && sslDays <= 30 ? ` · ssl ${sslDays}d` : ""}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user