mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-10-28 07:41:27 +00:00
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>
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import type { Item } from "@/types";
|
|
import { ClockWidget } from "@/widgets/clock";
|
|
import { PiholeWidget, AdguardWidget } from "@/widgets/blocker";
|
|
import { ImmichWidget } from "@/widgets/immich";
|
|
import { MonitorWidget } from "@/widgets/monitor";
|
|
import { DomainWidget } from "@/widgets/domain";
|
|
|
|
// WidgetHost seam: renderers own the tile chrome; this maps config.type to
|
|
// the component that fills it. Adding a widget = one fetcher in Go + one
|
|
// entry here.
|
|
|
|
export type WidgetComponent = (p: { item: Item; data: WidgetData | undefined; error: string | undefined }) => React.ReactNode;
|
|
|
|
export interface WidgetData {
|
|
[key: string]: string | number | boolean | undefined;
|
|
}
|
|
|
|
// Widget payloads are free-form per type; components read fields defensively.
|
|
function isData(v: unknown): v is WidgetData {
|
|
return typeof v === "object" && v !== null;
|
|
}
|
|
|
|
interface WidgetRegistry {
|
|
[type: string]: { local?: boolean; view: WidgetComponent };
|
|
}
|
|
|
|
const registry: WidgetRegistry = {
|
|
clock: { local: true, view: ClockWidget },
|
|
pihole: { view: PiholeWidget },
|
|
adguard: { view: AdguardWidget },
|
|
immich: { view: ImmichWidget },
|
|
monitor: { view: MonitorWidget },
|
|
domain: { view: DomainWidget },
|
|
};
|
|
|
|
export function cfgStr(item: Item, key: string): string {
|
|
const cfg = item.config;
|
|
if (!cfg) return "";
|
|
const v = cfg[key];
|
|
return typeof v === "string" ? v : "";
|
|
}
|
|
|
|
export function cfgType(item: Item): string {
|
|
return cfgStr(item, "type");
|
|
}
|
|
|
|
/** Polls /api/widgets/:id/data every 30s for server-backed widgets. */
|
|
export function useWidgetData(item: Item): { data: WidgetData | undefined; error: string | undefined } {
|
|
const type = cfgType(item);
|
|
const local = registry[type]?.local ?? false;
|
|
const [state, setState] = useState<{ data: WidgetData | undefined; error: string | undefined }>({
|
|
data: undefined, error: undefined,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (local || !type) return;
|
|
let dead = false;
|
|
let timer: ReturnType<typeof setTimeout>;
|
|
|
|
const tick = async () => {
|
|
try {
|
|
const res = await fetch(`/api/widgets/${item.id}/data`);
|
|
const body: unknown = await res.json();
|
|
if (dead) return;
|
|
if (!res.ok) {
|
|
let msg = "fetch failed";
|
|
if (typeof body === "object" && body !== null && "error" in body) {
|
|
msg = String(body.error);
|
|
}
|
|
setState((s) => ({ data: s.data, error: msg }));
|
|
} else {
|
|
let data: WidgetData = {};
|
|
if (typeof body === "object" && body !== null && "data" in body && isData(body.data)) {
|
|
data = body.data;
|
|
}
|
|
setState({ data, error: undefined });
|
|
}
|
|
} catch {
|
|
if (!dead) setState((s) => ({ data: s.data, error: "offline" }));
|
|
}
|
|
if (!dead) timer = setTimeout(tick, 30_000);
|
|
};
|
|
void tick();
|
|
return () => {
|
|
dead = true;
|
|
clearTimeout(timer);
|
|
};
|
|
}, [item.id, type, local]);
|
|
|
|
return state;
|
|
}
|
|
|
|
/** WidgetContent renders inside a renderer's tile chrome. */
|
|
export function WidgetContent({ item }: { item: Item }) {
|
|
const { data, error } = useWidgetData(item);
|
|
const type = cfgType(item);
|
|
const entry = registry[type];
|
|
if (!entry) {
|
|
return <span className="text-[11px] uppercase tracking-[0.1em] text-text-faint">{item.name || "widget"}</span>;
|
|
}
|
|
return <>{entry.view({ item, data, error })}</>;
|
|
}
|