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

+27 -8
View File
@@ -31,6 +31,7 @@ export function WidgetDialog({
const [name, setName] = useState("");
const [sectionId, setSectionId] = useState("");
const [values, setValues] = useState<Record<string, string>>({});
const [monitors, setMonitors] = useState<{ id?: string; name?: string }[]>([]);
const [busy, setBusy] = useState(false);
const [err, setErr] = useState("");
@@ -42,6 +43,10 @@ export function WidgetDialog({
.then((r) => r.json() as Promise<WidgetType[]>)
.then(setTypes)
.catch(() => setTypes([]));
fetch("/api/monitors")
.then((r) => r.json() as Promise<{ id?: string; name?: string }[]>)
.then(setMonitors)
.catch(() => setMonitors([]));
const cfg = item?.config ?? null;
setType(cfg && typeof cfg.type === "string" ? cfg.type : "");
@@ -141,14 +146,28 @@ export function WidgetDialog({
<Label htmlFor={`wdg-${f.key}`}>
{f.label}{f.required ? "" : " (optional)"}
</Label>
<Input
id={`wdg-${f.key}`}
type={f.secret ? "password" : "text"}
value={values[f.key] ?? ""}
placeholder={f.placeholder}
onChange={(e) => setValues((v) => ({ ...v, [f.key]: e.target.value }))}
className="font-mono text-[12px]"
/>
{f.key === "monitorId" ? (
<Select
value={values[f.key] ?? ""}
onValueChange={(v) => setValues((s) => ({ ...s, [f.key]: v }))}
>
<SelectTrigger><SelectValue placeholder="Pick a monitor…" /></SelectTrigger>
<SelectContent>
{monitors.map((m) => (
<SelectItem key={m.id} value={m.id ?? ""}>{m.name}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
id={`wdg-${f.key}`}
type={f.secret ? "password" : "text"}
value={values[f.key] ?? ""}
placeholder={f.placeholder}
onChange={(e) => setValues((v) => ({ ...v, [f.key]: e.target.value }))}
className="font-mono text-[12px]"
/>
)}
</div>
))}