Commit Graph
25 Commits
Author SHA1 Message Date
bobbanandClaude Sonnet 5 1ff1c6550c Share the excluded-ranges filter with IP Addresses; add Admin Links page
IP Addresses (IPAM) now reads the same excluded-ranges setting the
Consistency page manages, so "not interesting" addresses - a Docker
bridge network repeating on every host, say - can be hidden there too.
Adds a "Hide excluded addresses" toggle (on by default, with a live
count), an inline ranges editor matching Consistency's, an "excluded"
badge on rows shown anyway, and a per-row "Exclude..." shortcut that
suggests a /24 (or /64 for IPv6) around that address. Editing ranges
from either page updates both, since it's one shared setting.

Also adds Operations > Admin Links: a single page summarizing every
admin bookmark added across all servers (Dockge, Webmin, Cockpit, etc,
previously only visible per-server on each server's own detail page),
sortable and searchable, with the same add/edit/delete capability -
adding one here just asks which server it belongs to.

Verified both with real HTTP-level tests: a genuine Express app, a
scratch SQLite DB, and forged sessions, covering the exclusion
matching, the shared-setting round trip, the links aggregation and
join, and role enforcement - the real dev DB was confirmed untouched
throughout. Both packages build clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-30 00:07:27 +02:00
bobbanandClaude Sonnet 5 236b1da0dc Add a Network > Ports page: agent-reported ports plus manual openings
Summarizes every server's agent-reported listening ports in one
cross-server table (grouped by protocol+port, addresses merged,
loopback-only flagged) - previously this only existed per-server on
each server's own detail page.

Adds a second table for ports this app has no way to see on its own:
manually-recorded openings on a router, edge firewall, or cloud
security group, each with a label, external port/protocol, an optional
link to a tracked server (with its own internal port when NAT changes
it) or a freeform destination, a free-text source, and a comment.
Viewer-readable; adding/editing/deleting needs operator or admin.

The agent-port grouping logic (dedupe by protocol+port, detect
loopback-only sockets) was shared with the existing per-server Ports
card via a new agentPorts.ts service instead of duplicating it.

Verified with a real HTTP-level test: a genuine Express app with the
actual routers, a scratch SQLite DB, and forged admin/viewer sessions,
covering grouping correctness, the server-name join, input validation,
and role enforcement - the real dev DB was confirmed untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 23:48:35 +02:00
bobbanandClaude Sonnet 5 de5c39dddf Add an osTicket integration: list open tickets from its database
osTicket's own REST API only supports creating tickets, not listing or
reading them, so this reads osTicket's MySQL/MariaDB database directly
with a read-only user instead - the only integration in this app that
isn't a REST API. Joins the ticket, status, priority, department,
staff, team, and user tables, filtered to tickets in the "open" state
(status names are customizable per install, but that state flag isn't).

Surfaces per-ticket subject, priority, department, assignee, requester,
and osTicket's own overdue/awaiting-reply flags, plus a page at
/osticket and a Dashboard widget with open/overdue/awaiting-reply
counts.

Not verified against a live instance: unlike the HTTP-based
integrations, there was no way to fake a MySQL server to test against
in this environment, so the query is built from osTicket's published
schema but has never actually run against a real database. See
INTEGRATIONS.md for the read-only grant needed and further caveats.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 23:09:56 +02:00
bobbanandClaude Sonnet 5 df2a5ce42b Add a Proxmox Backup Server integration: datastore/snapshot verification status
Proxmox VE already shows whether the last vzdump push to PBS succeeded, but
has no visibility into PBS's own backup verification, GC/prune health, or
host status. This adds PBS as its own integration (own adapter, page, nav
entry, and Dashboard widget) that reads datastore usage and, for every
stored snapshot, its verification state directly from PBS.

A new daily check (mirroring the existing Proxmox backup-failure check)
notifies when a snapshot has failed verification or a datastore couldn't be
read, with its own toggle in Settings -> Notifications and its own
maintenance-window silencing.

Not verified against a live PBS instance — built from PBS's published API
docs and a scratch test against a mocked PBS server exercising the adapter's
parsing and auth-header format (PBSAPIToken uses a colon separator, unlike
PVE's PVEAPIToken which uses =). See INTEGRATIONS.md for details and the
"not verified" caveat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 20:32:52 +02:00
bobbanandClaude Sonnet 5 26de6cb243 Move Uptime Kuma from Infrastructure to Operations in the sidebar
It watches over things rather than being a machine/platform itself,
closer in spirit to Maintenance than to Proxmox or Tailscale. Also
balances the two groups (6/2 -> 5/3).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:52:41 +02:00
bobbanandClaude Sonnet 5 1a2dd19736 Add an Uptime Kuma integration: monitor status and which server each one watches
New integration, following the existing pattern: config in-app (URL +
API key, credentials encrypted at rest), its own Uptime Kuma page, an
Integrations list entry, a Dashboard widget, and diagnostic-log/
integration-down-alert coverage for free via the shared withDiagLogging
wrapper. Read-only -- no start/stop equivalent exists for a monitor.

Uptime Kuma has no conventional REST API (the dashboard talks to it over
Socket.IO); researched before writing any code, since guessing wrong here
would have cost real time. The one machine-readable, authenticated
endpoint that lists every monitor is its Prometheus exporter at
GET /metrics, gated by HTTP Basic auth -- an API key as the password with
the username left blank on current installs, or the real dashboard
login on installs from before the API-key feature existed. This adapter
authenticates the same way and parses that endpoint's text-exposition
format itself (metrics: monitor_status, monitor_response_time,
monitor_cert_days_remaining, monitor_uptime_ratio; labels: monitor_id,
monitor_name, monitor_type, monitor_url, monitor_hostname, monitor_port),
verified against the documented metric/label set and the actual upstream
source (server/prometheus.js). A malformed line is skipped rather than
failing the whole scrape.

"What server is being monitored for what": each monitor's target (an IP
for TCP checks, or the hostname out of the URL for HTTP/keyword checks)
is matched against your servers' own IPs and hostnames -- reusing the
same kind of match already used in the consistency report -- and linked
to that server's page. Monitors with no single network target (groups,
push monitors, DNS/keyword checks with a complex URL) are left unmatched
rather than guessed at. Uptime Kuma's tags aren't read, since the
Prometheus endpoint doesn't reliably distinguish a tag label from any
other label it might add later.

The username field is the first genuinely optional integration config
field this app has had; IntegrationField gained an `optional` flag
(server validation and both the add/edit web forms honor it) rather than
special-casing Uptime Kuma.

Verified with 48 backend checks (Prometheus text parsing including
escaped quotes, decimals, negative numbers, and malformed lines; TCP vs.
HTTP target/port extraction; every documented status code; server
matching by IP, hostname, and short name, including no-match cases; the
route's real HTTP round trip against a fake Uptime Kuma server, wrong
credentials, upstream failures, roles, wrong/disabled/missing
integration, diagnostic-log entries; the optional-field validation rule)
and by driving the real page and the real Dashboard widget in a browser
against the real routers, including CSV export and column sorting. Real
dev database mtime untouched.

Not verified: a real Uptime Kuma instance. Everything here was checked
against Uptime Kuma's documented metric format, its actual upstream
source, and a fake server built to match both -- not against a live
installation. If your instance's /metrics output differs from what's
documented (older version, unusual monitor types), the parser should
degrade to an empty or partial monitor list rather than error, but that
degradation itself hasn't been observed against the real thing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:50:27 +02:00
bobbanandClaude Sonnet 5 ada2e648e9 Group the sidebar into collapsible submenus
The flat 22-item menu becomes 7 entries: Dashboard and Secrets as plain
links, and five groups.
- Infrastructure: Servers, Proxmox, Synology, Docker, Tailscale
- Network: DNS, Domains, IP Addresses, Consistency
- Automation: Semaphore, Gitea
- Operations: Maintenance, Generator
- Administration: Integrations, Users, Sessions, Audit Log, Diagnostic
  Log, Settings
Privacy moves to the sidebar footer beside the theme toggle and sign-out,
since it's about the person rather than the homelab.

Behaviour:
- Groups open on demand. The group holding the current page is always
  open (deep routes count: /servers/7 keeps Servers active), and its
  header stays emphasised even if you close it. Which groups you leave
  open is remembered in the browser, and the menu still works if storage
  is blocked.
- Role handling: pages a role can't use are dropped, an emptied group
  disappears, and a group reduced to a single page is drawn as that
  page's own link. A viewer therefore sees a plain Integrations link
  where an admin sees Administration, and an operator sees Administration
  with Integrations and Audit Log.
- Toggling a group on mobile keeps the menu open; choosing a page closes
  it, as before.
- Group headers are real buttons with aria-expanded.

Front end only: one component, plus a small CSS rule tightening submenu
rows so several open groups still fit on one screen. Routes, permissions
and backend are unchanged.

Verified in a browser with the real shell as admin, operator and viewer:
structure per role, open/close and aria state, active highlighting on
direct and deep routes, auto-open of the current page's group, state
surviving a reload, and the mobile toggler behaviour. Not done: the
attention dots on closed groups (deliberately held for a later step).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 03:00:22 +02:00
bobbanandClaude Sonnet 5 72f8c85406 Add a Privacy page: what is stored, where it goes, and your own data
A page every signed-in user can open. It states what the installation
stores (accounts, sign-in sessions, audit and diagnostic logs, server
reports, the secrets tracker, credentials, inventory) and for how long,
where data goes (Authentik, integrations, DNS providers, notification
channels, domain registries, certificate checks, port scans), what lives
in the browser, who can see what, and how to limit or remove data.

Written from what the code actually does, including the uncomfortable
parts: the session file keeps the user's Authentik ID token plus the IP
and browser from sign-in; audit entries keep a name snapshot after an
account is gone; the app has no delete-account function; cron commands
in agent reports can contain sensitive text. It also says what isn't
there -- no telemetry, update checks, third-party scripts, fonts or
tracking cookies -- which was checked against the web build and the
server's outbound calls before being asserted.

Live values rather than boilerplate: log retention (and whether it's on),
which integration and DNS provider types are enabled, how many domains,
certificate checks and reporting servers, and which notification channels
are on. Channel addresses are shown to admins only, and only the host --
never a path, query string or token -- since a webhook URL can embed a key.

Each user also sees their own account and active sign-ins, and can
"Download my data": their account, their sign-ins and the audit-log entries
made under their account, as JSON. Only their own -- never another user's --
and without session ids or ID tokens. The export is itself audit-logged, so
a later export shows it.

Verified with 23 backend checks (own-vs-others isolation for audit counts,
sessions and export; no session ids, ID tokens or channel secrets in any
response; admin-vs-viewer channel visibility; live counts; audit of the
export; auth) using an isolated session directory so real sessions are
never read, and in a browser against the real router, including the
download. Real dev database and session files untouched.

Not legal text: this is a transparency page for the people using the app,
not a privacy policy or a GDPR compliance document.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 20:28:03 +02:00
bobbanandClaude Sonnet 5 2352689fd3 Add a consistency report across IPAM, DNS and servers
New Consistency page listing where the three places this app records
what lives at an address disagree:
- Address conflicts: the same address reported by more than one server.
- DNS out of date: a record named after a server (its hostname, or its
  short name) that points at an address the server doesn't report.
- IPAM out of date: an entry labelled with a server's name at an
  address the server doesn't report.
- Not in IPAM: addresses a server reports or DNS points at that IPAM
  doesn't list, merged into one finding per address, with a one-click
  "Add to IPAM" that pre-fills a label.
- No DNS record: server LAN addresses no cached A/AAAA record resolves to.

It compares data the app already holds and fetches nothing when opened,
so the page states how many servers had reported addresses and how many
DNS zones are synced (and how old the oldest sync is) -- DNS records are
only cached for zones that have been synced, and a report that silently
treated missing data as "no records" would mislead.

Rules chosen to keep it from crying wolf:
- Only private addresses are compared; public DNS records aren't expected
  to be in IPAM.
- Servers with no reported addresses are never judged.
- Agents report IPv4 only, so records are only compared within an address
  family (an AAAA record isn't "stale" for lacking an IPv6 address).
- Docker bridge networks (172.16/12) are ignored: shared ones aren't
  conflicts, and they aren't listed unless someone put them in DNS.
- Tailscale addresses don't need DNS records (MagicDNS), and IPAM entries
  kept current by the Tailscale/Proxmox syncs aren't second-guessed.

Findings anyone has decided are fine can be ignored (operators) with a
reason. An ignore is keyed on the finding's stable identity so it stays
ignored across runs, its stored text comes from the finding rather than
the request, and it is marked "no longer occurring" once the condition
goes away. Ignore/restore are audit-logged.

New table consistency_ignores (migration 0012). portScan's private-address
helper is now exported and shared.

Verified with 36 checks (each rule and its exclusions, address-family and
case/trailing-dot handling, IPv6 case, ordering, stable keys, the report
route including a garbled agent report, source counts, ignore/unignore
rules and audit entries) and by driving the page against the real routers
in a browser: Add to IPAM actually created the entry, ignore and restore,
severity filter, "show all", the viewer view, and narrow-width layout
(which found and fixed a squeezed badge and clipped buttons). Real dev
database mtime untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 04:08:51 +02:00
bobbanandClaude Sonnet 5 ca0fa817f8 Track domain registration expiry, with daily reminders
New Domains page listing when each domain registration expires, read from
the registry. Domains behind the DNS zones already synced are picked up
automatically; others can be added by hand. You're reminded daily from N
days before expiry (Settings > Notifications, default 30) until it's
renewed, and told when an expiry date hasn't been refreshable for several
days so a stale date isn't trusted silently.

RDAP alone would not have covered this homelab: .se, .nu, .io, .eu and .de
are not in IANA's RDAP bootstrap. Lookups therefore try RDAP where the TLD
publishes a server and fall back to WHOIS on port 43, found via IANA's
own referral, parsing the expiry line out of the free-text answer. Only
the expiry date and registrar are read or stored. Verified live against
the real registries: .se and .nu via WHOIS, .com/.org/.dev via RDAP.

Behaviour worth knowing:
- A DNS zone that is a subdomain (lab.example.se) resolves to the
  registration that actually expires by trying the name and then its
  parents, so no public-suffix list is needed. Zones already covered by a
  tracked domain are not looked up again.
- "Couldn't ask" is never confused with "not registered": network errors,
  rate limits and garbled answers are errors, and a transient error at any
  level stops the walk from concluding the domain doesn't exist.
- A failed refresh keeps the last known expiry and records why, rather
  than blanking a date that's still relied on.
- Zones that don't resolve to a real registration (.lan, .local, unregistered
  names) simply get no row. Zone-derived rows disappear when their zone
  does; manual rows stay. Zone-derived rows can't be deleted by hand.
- Registries that don't publish an expiry (.de, .eu) are tracked with a
  note instead of a date.
- Input like "example.com/path" is refused rather than silently reduced
  to its host.
- Runs on the daily secret-expiry schedule and reminder time, on demand
  (Check all now / per domain), and once at startup if nothing has been
  read in a day. Never blocks startup, one lookup at a time with a pause.
  The warning window lives with the other thresholds in settings.

New table domains (migration 0011); two settings fields (toggle and
warning days).

Verified with 90 checks against fake RDAP/WHOIS backends (name
normalization, date formats, WHOIS parsing including rate-limit and
no-expiry answers, bootstrap and referral caching, stale-cache fallback,
parent walking, add/sync/check/refresh, concurrency guard, alert
selection and stale detection, the daily notification and its toggle,
role rules) plus a live smoke test against real registries and a browser
check of the page against the real router. Real dev database mtime
untouched. Not checked: a screenshot of the finished page (the capture
timed out); structure, sorting, errors and the viewer view were verified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 03:31:02 +02:00
bobbanandClaude Sonnet 5 aae4f0d74f Add maintenance mode to silence alerts while working on a server or integration
Rebooting Proxmox or patching a server triggered failure/offline alerts
you then had to dismiss. A maintenance window silences alerts about one
server, integration, or DNS provider for a chosen time. New Maintenance
page (start with a duration and optional reason, end early, see what's
silenced and what isn't) and a banner in the app shell so every signed-in
user can see what is currently silenced. Starting/ending is operator-only
and audit-logged; starting one on a target that already has a window
restarts its clock instead of stacking.

Silenced for the target: server offline/disk alerts, Proxmox/Synology
storage and health alerts, Proxmox backup alerts, and "integration down"
alerts. Not silenced: expiry and update reminders, DNS change notices.

The design goal is that this cannot hide a real outage:
- Every window has a required end (5 min to 7 days); there is no
  open-ended option, so a forgotten window expires by itself.
- A silenced problem is deliberately NOT recorded as "known". If it is
  still present when the window ends it alerts then, as new. A problem
  that was already alerted before the window stays known, so it isn't
  repeated, and is reported cleared only after the window ends.
- Failure alerts keep counting failures during a window without marking
  themselves alerted, so an outage that outlasts the window alerts on the
  very next failed call.

Known limitation, stated on the page: integration-failure alerts are
tracked per service TYPE (all "proxmox"), not per configured instance, so
a window on one Proxmox integration also silences a failure on a second
Proxmox integration while it's open. Fixing that means threading the
integration id through every adapter and the diagnostic log, which is a
much larger change than this feature.

Also moved the API-error-message helper out of Secrets.tsx into a shared
util now that two pages use it. New table maintenance_windows (migration
0008).

Verified with 44 checks: the condition-key-to-subject mapping (including
server:3 vs server:33), the diff rules with silenced subjects (new problem
not recorded, alerts when the window ends; already-known one carried and
not repeated; clears only after the window), window expiry and
integration/DNS-provider source matching, the failure tracker end to end
against a webhook (silent during a window while an unrelated service still
alerts; outage that outlasts the window alerts on the next failure and
only once; fail-and-recover fully inside a window sends nothing), a full
health pass against a real window, and the real router with a stubbed
session (role rules, duration bounds including the missing-duration case,
extend-not-stack, 404s, deleted targets hidden, audit entries). Real dev
database mtime untouched.

Not done: I haven't clicked through the new page or banner in a browser
(they sit behind the Authentik login); it builds and the API behind it is
tested.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 02:23:06 +02:00
bobbanandClaude Sonnet 5 0da73711d9 Add a Generator page for server names, usernames, and passwords
New "Generator" nav item, open to every role since it's a pure
client-side utility with no data mutation. Three independent cards:
- Server name: picks from Swedish girl names or Disney characters (or
  both), skipping any name already used by an existing server —
  checked against the real server list, not just avoiding duplicates
  within one session.
- Username: adjective+animal with a configurable separator and an
  optional 2-digit suffix.
- Password: length slider, per-charset toggles, an "exclude ambiguous
  characters" option (0/O, 1/l/I), and a rough entropy/strength
  readout. Uses crypto.getRandomValues with rejection sampling (not
  Math.random or a plain modulo), since a biased password generator
  is a real security footgun.

Nothing generated here is sent to or stored on the server — it's
computed entirely in the browser and only reaches the backend if the
user pastes it into some other form themselves (e.g. Secrets, adding
a server).

Verified generators.ts directly: collision avoidance always returns
the one remaining unused name when every other option in a themed
list is taken, the full-list-exhausted fallback produces a numbered
name that still doesn't collide, matching is case-insensitive,
per-charset password generation only ever produces characters from
the selected sets, excludeAmbiguous holds across 200 40-character
samples, entropy math matches the expected log2 formula, and a
5000-sample single-character distribution came out uniform (469-521
per digit against an expected 500, no modulo bias) confirming the
rejection-sampling RNG is unbiased.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 20:56:15 +02:00
bobbanandClaude Sonnet 5 6d673db9ec Add session management: see who's signed in, revoke a session
No visibility existed into who was currently signed in or a way to
force a device out. Sessions already live as files via
session-file-store, so this reads that store directly rather than
adding a new DB table: new Settings-adjacent "Sessions" page
(admin-only, alongside Users) lists every live session with the
user's name/email/resolved role, IP, a friendly "Browser on OS"
summary parsed from the user-agent, last-active time, and expiry, with
a Revoke button per row (extra confirmation if you revoke your own
current session, since that signs you out immediately).

IP and user-agent are now captured into the session at login
(auth/router.ts) since express-session doesn't track them itself.
session-file-store's own Store type doesn't declare its list()
method, so sessionStore.ts adds a narrow local interface for it rather
than losing type safety on the rest of the store.

Verified against a real session directory seeded through the actual
session-file-store APIs (not hand-written JSON): confirmed correct
field resolution including a session whose user row was later deleted
(role resolves to null instead of crashing), correctly excluded a
mid-OIDC-login session with no completed user yet, correctly excluded
an already-expired session, and confirmed revoke actually deletes the
right session file and only that one. Confirmed the real dev
database's mtime was untouched throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 20:29:29 +02:00
bobbanandClaude Sonnet 5 009bb3e027 Add a global search / command palette (Ctrl/Cmd+K)
With 6 integrations, DNS, secrets, IPAM, and servers all in one app,
there was no single place to type a hostname/IP/name and jump
straight to it. New GET /api/search aggregates a LIKE-based search
across servers, secrets, IPAM, integrations, DNS providers, DNS
zones, and DNS records (joining zone/provider names onto each record
result) in one round trip — homelab-scale row counts make a naive
LIKE scan plenty fast, no FTS needed. Every underlying resource's own
list endpoint already only requires requireAuth (viewer role
included), so the aggregate endpoint uses the same single check.

Frontend: a self-contained CommandPalette component (Tabler's modal
CSS classes driven by React state, since the app doesn't load
Bootstrap's JS) opens via a sidebar search button or Ctrl/Cmd+K from
anywhere, debounces input, and supports arrow-key navigation. Results
link to the right page: servers to their existing /servers/:id
detail route; DNS zone/record results deep-link via new ?providerId=
&zoneId= query-param handling added to Dns.tsx (auto-selects that
provider/zone on load, since Dns.tsx previously held selection only
in local state with no URL sync); secrets/IPAM results link with
?q= to prefill each page's existing client-side search box;
integrations link to their type's dashboard page (no per-instance
route exists yet, so same-type integrations share one link).

Verified the query logic (joins, case-insensitive LIKE, correct
zone/provider name resolution) against an isolated scratch database
seeded with realistic cross-referencing rows — a server name match, a
case-mismatched match against both a secret and a DNS provider
sharing "cloudflare", an IP address matching both an IPAM entry and
the DNS A record pointing at it (confirming the record's joined zone
and provider names came through correctly), an integration name
match, and a no-match query returning every category empty. Did not
re-verify the requireAuth/asyncHandler wiring itself, since it's the
same one-line pattern already proven across every other router in
this app. Confirmed the real dev database's mtime was untouched
throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 03:20:05 +02:00
bobban 19f84838cc Revert "Move dark mode toggle from sidebar to Settings > Display"
This reverts commit 4ba86429d5.
2026-09-19 01:58:18 +02:00
bobban 4ba86429d5 Move dark mode toggle from sidebar to Settings > Display 2026-09-19 01:56:08 +02:00
bobbanandClaude Sonnet 5 5c35080e22 Add a dark mode toggle
Personal, per-browser preference (localStorage), not an admin-wide
Display setting like date/time format — every role can pick their own.
Sets data-bs-theme on <html> to hook into Tabler's built-in dark
variant, applied via an inline script in index.html before React
mounts so there's no light-mode flash on load. Toggle button lives in
the sidebar footer next to Sign out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 01:51:59 +02:00
bobbanandClaude Sonnet 5 e35da87886 Add a Diagnostic Log, ported and generalized from Sloth Manager
Sloth Manager tracked every API call made to DNS providers for
connectivity troubleshooting. Port that here, generalized to cover
every outbound integration this app makes, not just DNS -- Tailscale,
Proxmox, Synology, Semaphore, Gitea, and Dockhand calls now show up
too, since a broken API token or unreachable host on any of them is
just as worth diagnosing.

New services/diagLog.ts: a generic withDiagLogging(source, adapter)
wraps every async method of any adapter object with timing +
success/failure recording, without touching a single adapter's
request/error-handling internals -- every DNS and integration adapter
interface here is already just a flat set of async methods, so this
one wrapper works for all twelve of them. Applied it at each adapter
factory's own return statement (one line each) rather than at the
route layer, so background jobs that construct adapters directly
(the Tailscale key-expiry scheduler, IPAM sync, agent-driven Proxmox
lookups) get logged too, not just requests through routes/integrations.ts.

New diag_log table (ring-buffered to the last 500 rows, mirroring
Sloth Manager's approach -- this is for live troubleshooting, not a
durable record) and admin-only GET/DELETE /api/diag-log routes, source/
result filters, pagination.

New admin-only Diagnostic Log page: filterable, paginated table with a
Clear button. Also introduces the shared useSortable hook + SortableTh
component used here for the first time -- a follow-up commit applies
the same sorting (and CSV export) to the rest of the app's tables, per
the same request.

Verified end-to-end against a temp SQLite DB with real migrations: a
fake wrapped adapter's successful and failing calls both land correctly
in the log with the right source/operation/latency/error, and the
source/ok filters and clear-log operation all behave correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:41:06 +02:00
bobbanandClaude Sonnet 5 42f073df81 Reorder sidebar nav to group related pages together
Dashboard, Servers, Proxmox, Docker, Synology, Secrets, DNS, IP
Addresses, Tailscale, Semaphore, Gitea, Integrations, Users, Audit
Log, Settings -- as requested, no routes or labels changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:16:39 +02:00
bobbanandClaude Sonnet 5 a781df9c51 Trim the Servers page down to a server list + add-server flow
Scheduled-task browsing/editing already lives on each server's detail
page (ServerDetail.tsx), so the overview page's filters, task-group
listing, and add/edit-task form were pure duplication -- the only
things it needs to do are list servers (as clickable cards) and let
an admin register new ones / issue or rotate agent tokens.

Renamed ServersTasks.tsx -> Servers.tsx to match its new, narrower
scope; updated the nav label, route, and ServerDetail's back-link
text from "Servers & Tasks" to "Servers" accordingly. No server-side
or API changes -- this only removes now-redundant frontend code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 18:38:46 +02:00
bobbanandClaude Sonnet 5 95bd831d81 Move Proxmox and Synology to their own top-level pages
Same move as Docker/Tailscale/Semaphore/Gitea: each was buried inside
the generic Integrations browsing view. Give them dedicated pages too
— this was the last pair, so every integration type with a browsing UI
now has its own page.

New web/src/pages/{Proxmox,Synology}.tsx reuse the existing, unchanged
API routes and adapters (no server changes) with their own integration
picker scoped to just that type. Added matching nav items and routes.
Synology stays read-only, matching its original design.

Removed the corresponding state/handlers/tables from Integrations.tsx
(296 lines). It's now down to just the "Manage integrations" CRUD
table plus a browsing dropdown whose six branches are all redirects to
the type's own page — genuinely pointless now that every type has
moved out, worth simplifying separately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 00:21:07 +02:00
bobbanandClaude Sonnet 5 8fef68c27a Move Tailscale, Semaphore, and Gitea to their own top-level pages
Same move as Docker: each was buried inside the generic Integrations
browsing view, sharing one "pick any integration" dropdown across six
unrelated types. Give them dedicated pages instead.

New web/src/pages/{Tailscale,Semaphore,Gitea}.tsx each reuse the
existing, unchanged API routes and adapters (no server changes) with
their own integration picker scoped to just that type. Added matching
nav items (Tailscale, Semaphore, Gitea, right after Docker) and routes.

Removed the corresponding state/handlers/tables from Integrations.tsx
entirely (374 lines). Adding, editing, enabling/disabling, and
deleting each credential itself still happens under Integrations ->
Manage integrations, same as Proxmox and Synology, which stay on the
Integrations page. Selecting one of these three types in Integrations'
generic browsing dropdown now points to its new page instead of
rendering a table there too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 00:09:02 +02:00
bobbanandClaude Sonnet 5 52dc7ed1f5 Move Dockhand container management to its own top-level Docker page
Container management was buried inside the generic Integrations
browsing view, mixed in with five unrelated integration types behind
a single "pick any integration" dropdown. Give it a dedicated page,
matching how Servers & Tasks and DNS already get their own top-level
spot instead of living inside Integrations.

New web/src/pages/Docker.tsx reuses the existing, unchanged Dockhand
API routes and adapter (no server changes) — it just has its own
integration picker scoped to Dockhand only, rather than sharing
Integrations' any-type dropdown. Added a Docker nav item (right after
Integrations) and route.

Removed the Dockhand-specific state/handlers/table from Integrations
entirely; adding, editing, enabling/disabling, and deleting the
Dockhand credential itself still happens under Integrations -> Manage
integrations like every other integration. Selecting a Dockhand row in
Integrations' generic browsing dropdown now points to the Docker page
instead of rendering a container table there too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 23:57:11 +02:00
bobbanandClaude Sonnet 5 7fed1dbfa4 Move the signed-in-as/sign-out block to the bottom of the sidebar
Was a separate top page-header bar above every page's content. Moved
into the vertical navbar itself using Tabler's own .navbar-footer
class (order:1 + margin-top:auto), which pins it to the bottom of the
sidebar regardless of how many nav items are above it. Removed the
now-empty page-header entirely.

Verified visually in the browser at both desktop width (sidebar is
position:fixed there — footer sits flush at the bottom) and mobile
width (collapsed dropdown menu — footer just follows the nav list).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 21:10:07 +02:00
bobbanandClaude Sonnet 5 6bd2ed52c1 Scaffold Homelab Manager foundation
Monorepo (Express+TS+Drizzle/libSQL server, React+Vite+Tabler web) matching
the stack used by ScheduleTaskManager and Sloth Manager. Includes Authentik
OIDC login with local admin/operator/viewer roles (first user becomes admin),
a generalized audit log, encrypted-at-rest storage for future integration API
tokens, the DB schema for all planned modules, and the Tabler-styled app
shell/nav. Also ports the Secrets (expiry tracker) and IP Addresses (IPAM)
modules from Sloth Manager onto the new stack.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 21:57:13 +02:00