feat: external importers + uptime monitors (merge plan M1)

Two tracks from the Beszel/Domain-Locker merge plan in ROADMAP.md:

- Importers: POST /api/import/external sniffs Homepage services.yml,
  Homarr JSON, and Dashy conf.yml into sections/items; SettingsDialog
  import now accepts all four formats.
- Monitors (M1): monitors + heartbeats schema, pure checkers for
  http/tcp/ping/dns/keyword/json-query/push, scheduler on app ctx,
  CRUD + check-now + push ingest API, /monitors + /monitors/:id pages
  (wouter), monitor board widget, ServiceDialog monitor opt-in.

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:
Tomas DvorakandDevin committed 2026-09-21 18:14:36 +02:00
1 parent f428e721fe
commit 1ff1b29ae7
29 files changed
+3050 -98

No files matched your search

+2
View File
@@ -3,6 +3,7 @@ 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";
// 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
@@ -28,6 +29,7 @@ const registry: WidgetRegistry = {
pihole: { view: PiholeWidget },
adguard: { view: AdguardWidget },
immich: { view: ImmichWidget },
monitor: { view: MonitorWidget },
};
export function cfgStr(item: Item, key: string): string {
+38
View File
@@ -0,0 +1,38 @@
import type { WidgetData } from "@/widgets";
import type { Item } from "@/types";
import { cn } from "@/lib/utils";
// Board tile for an uptime monitor: status dot, uptime %, avg ping.
// config.monitorId binds it; data comes from the monitor fetcher.
export function MonitorWidget({ item, data, error }: {
item: Item;
data: WidgetData | undefined;
error: string | undefined;
}) {
const status = typeof data?.status === "string" ? data.status : undefined;
const uptime = typeof data?.uptime24 === "number" ? data.uptime24 : undefined;
const ping = typeof data?.ping === "number" ? data.ping : undefined;
const monId = typeof item.config?.monitorId === "string" ? item.config.monitorId : "";
return (
<a
href={monId ? `/monitors/${monId}` : "/monitors"}
className="flex w-full items-center gap-2.5 text-left"
onClick={(e) => e.stopPropagation()}
>
<span
className={cn(
"size-1.5 shrink-0 rounded-full",
error ? "bg-down" : status === "up" ? "bg-up" : status === "down" ? "bg-down" : "bg-text-faint/40",
)}
/>
<span className="min-w-0 flex-1 truncate text-[12.5px] font-medium">
{typeof data?.name === "string" ? data.name : item.name}
</span>
<span className="shrink-0 font-mono text-[10.5px] text-text-faint">
{error ? "err" : uptime !== undefined ? `${uptime.toFixed(1)}%` : "…"}
{ping ? ` · ${Math.round(ping)}ms` : ""}
</span>
</a>
);
}