Commit Graph

175 Commits

Author SHA1 Message Date
Developer 84c1262bd6 Frontend: Observability split -- Alerts + Links + Metrics tabs (Slice 9)
Split the aggregate ObservabilityPage into three instance-scoped tabs on
their respective service pages, replacing the AlertsTab/LinksTab/
MetricsTab stubs.

AlertsTab (alertmanager): Alertmanager status line + active-alerts
summary (count + by severity) + expandable alert list (AlertItem with
severity badge, summary, description, labels, active-since). Empty
state when no alerts.

LinksTab (grafana): Grafana status line + machine-selector dropdown +
GrafanaLinkCards (Node Exporter metrics dashboard, Loki log explorer)
generated from instance.config.base_url. Empty states when no base_url
or no machine selected.

MetricsTab (prometheus): Prometheus status line + Node Exporter targets
table with labels badges. Empty state when no targets.

All three tabs use the existing observability hooks which are global /
first-configured (no service_id param yet). Per-instance scoping by
instance.id is a documented follow-up once the hooks gain the parameter
(same pattern as JobsTab slice 7). LinksTab does read
instance.config.base_url for the specific Grafana deep-link URL.

stubs.tsx loses AlertsTab/LinksTab/MetricsTab stubs (only OverviewTab
stub remains); index.ts wires the real components.

Old ObservabilityPage.tsx stays in the repo (route removed slice 4;
file deleted slice 11).

Tests: 2 per tab (renders content + empty/error states with mocked
hooks). 106 tests pass (+6); lint/build green.

Refs openspec/changes/services-as-hub-ia/ (spec R2.4/R8, tasks slice 9).
2026-06-26 19:52:06 +00:00
Developer 8f4e8428f0 Authentik Users + Messaging tabs + message endpoint (Slice 8)
Replace the UsersTab and MessagingTab stubs on the Authentik service
page, built new against the Authentik directory endpoint (the old
Jellyfin-backed Users page was deleted in slice 3).

Backend message endpoint (Option A -- implemented):
- POST /api/services/authentik/{service_id}/message accepts
  {recipient_emails, subject, html_body}, validates SMTP, enqueues via
  the existing mail_queue. Returns {status, request_id, recipient_count}
  on success or {status: 'error', error} on failure (200, matching the
  directory endpoint's graceful-error pattern).
- GET /api/services/authentik/{service_id}/message/status proxies
  mail_queue.status().

UsersTab: paginated (25/page), searchable directory table sourced from
GET /api/services/authentik/{id}/users. Columns: name, username, email,
status (is_active badge). Graceful error Alert on endpoint error.

MessagingTab: minimal but functional compose -- recipient search +
toggle buttons (Authentik users with emails), subject, HTML body
textarea (default template), send wired to the new endpoint, result
Alert. Rich-text toolbar, attachment upload, and queue-status banner
are follow-ups (the old compose UI had them; this slice ships the core
send flow).

New: api/authentik.ts, hooks/useAuthentik.ts (useAuthentikUsers +
useAuthentikMessageStatus), UsersTab + MessagingTab + tests. stubs.tsx
loses both stubs; index.ts wires the real components.

Tests: UsersTab (renders users + error state), MessagingTab (renders
compose form). 100 frontend tests pass (+4); 271 backend tests pass
(no regression); lint/build green both sides.

Refs openspec/changes/services-as-hub-ia/ (spec R6.2/R7.2/R7.3, tasks
slice 8).
2026-06-26 19:44:35 +00:00
Developer 6a1f8bbd59 Frontend: backups Jobs tab (Slice 7)
Replace the JobsTab stub with a real implementation on the backups
service page, lifted from components/BackupsPage.tsx. Renders the
Jobs/Runs/Alerts sub-tabs with their existing tables
(BackupJobsTable, BackupRunsTable, BackupAlertsTable) and the
acknowledge-alert mutation.

The backup hooks (useBackupJobs/Runs/Alerts) currently query globally --
the backend gained service_id attribution in slice 3, but the hooks
don't yet accept a serviceId param. This tab shows all backups data for
now; per-instance scoping by instance.id is a documented follow-up once
the hooks gain the parameter.

The page heading from BackupsPage is dropped (the service page header
already shows the instance name + 'Backups' binding).

stubs.tsx loses the JobsTab stub; index.ts wires the real component.

Tests: JobsTab renders sub-tabs + job-name rows with mocked hooks. 96
tests pass (+2); lint/build green.

Refs openspec/changes/services-as-hub-ia/ (spec R2.4, tasks slice 7).
2026-06-26 19:34:05 +00:00
Developer b2e1acd257 Frontend: ssh_tasks Files + Actions tabs (Slice 6)
Replace the FilesTab and ActionsTab stubs with real implementations on
the ssh_tasks service page.

FilesTab (pages/service-tabs/FilesTab.tsx): lifts the operational content
from the top-level FileBrowser page into an instance-scoped tab.
Directory listing, path bar, ffprobe preview, and job execution all use
instance.id as the machine id (replacing the old machine-tab selector +
machine_id search param). The initial path is read from ?path= search
param so deep links work (resolves the MediaTab row-click navigation).

ActionsTab (pages/service-tabs/ActionsTab.tsx): lifts the saved-tasks
CRUD + run + history content from the top-level Actions page. instance.id
is the fixed default run service -- the old service-selector dropdown is
removed (the instance is implicit; switch instances via the service page
switcher to run on a different one).

MediaTab row-click cross-slice fix: navigates to
/services/ssh_tasks/<first-enabled-id>?path=<encoded> when an enabled
ssh_tasks instance exists, else falls back to /services/ssh_tasks (which
shows the ServiceTypePage resolver/empty state). Resolves the 404 flag
from slice 5.

service-tabs/index.ts wires the new components; FilesTab/ActionsTab
stubs removed.

Note: this branch is based on main, not on mobile-responsive-parity, so
the tabs lift main's DataTable + column-visibility pattern (no
MobileCardRow -- reconciles when the branches merge).

Tests: FilesTab (instance-scoped hooks + ?path= deep-link), ActionsTab
(instance-scoped task list), MediaTab test mock updated. 94 tests pass
(+4); lint/build green.

Refs openspec/changes/services-as-hub-ia/ (spec R2.4, tasks slice 6).
2026-06-26 19:27:16 +00:00
Developer dd48214987 Frontend: Jellyfin Media + Requests tabs (Slice 5)
Replace the MediaTab and RequestsTab stubs with real implementations on
the Jellyfin service page.

MediaTab (pages/service-tabs/MediaTab.tsx): lifts the operational content
from the top-level Media page into an instance-scoped tab. Build controls,
status display, library counts, media DataTable, and pagination all read
the Jellyfin service id directly from the instance prop (replacing the old
URL-search-param service selector + dropdown). Row-click navigation to
the file browser is preserved (note: target /files is a cross-slice
dependency on slice 6's FilesTab).

RequestsTab (pages/service-tabs/RequestsTab.tsx): reads the absorbed
jellyseerr_url + jellyseerr_api_key from the Jellyfin instance config.
When unconfigured, renders a CTA to add the fields via the Config tab.
When configured, shows the Jellyseerr URL + an honest placeholder (no
requests backend endpoint exists yet -- out of scope for this slice).

service-tabs/index.ts updated to wire the new components; the
MediaTabStub/RequestsTabStub removed from stubs.tsx.

Note: this branch is based on main, not on mobile-responsive-parity, so
MediaTab lifts main's DataTable + TanStack column-visibility mobile
hiding (no MobileCardRow -- that lands when the branches reconcile).

Tests: MediaTab (instance-scoped hooks + build controls + table render)
+ RequestsTab (configured URL vs empty-state CTA). 90 tests pass (+6);
lint/build green.

Cross-slice flag: MediaTab row-click -> /files will 404 until slice 6
re-routes it to the ssh_tasks FilesTab.

Refs openspec/changes/services-as-hub-ia/ (spec R2.4, tasks slice 5).
2026-06-26 19:12:25 +00:00
Developer caf6c226ff Frontend: data-driven nav + service-page tab skeleton + stubs (Slice 4)
The IA shell lands. The static navItems array is replaced by useNavItems(),
which combines useServiceInstances (enabled instances) + useDashboards to
build the nav in spec order: Main Dashboard, named dashboards, conditional
service-type entries (one per configured type; ssh_tasks contributes Files
+ Actions, nextcloud contributes none), Services, Settings.

Legacy top-level routes (/media, /files, /actions, /users, /observability,
/backups, /monitoring, /applications) are removed; a NotFoundPage catch-all
returns 404 (R4.7).

ServicePage is refactored to a tab skeleton: Overview | type-specific
content tabs | Widgets | Config. serviceContentTabs(type) returns the
per-type set (jellyfin=Media+Requests, ssh_tasks=Files+Actions, backups=Jobs,
authentik=Users+Messaging, alertmanager=Alerts, grafana=Links,
prometheus=Metrics, nextcloud=none). Content tabs are stubs ('coming soon');
real content migrates in slices 5-9. Widgets + Config tabs preserve the
existing widget-list and config/secrets editing verbatim.

ServiceTypePage resolves /services/:type (no id) by redirecting to the
first enabled instance; empty state when none.

Instance switcher (Select) appears when >1 ENABLED sibling of the same
type exists (R3.1).

Empty states: Dashboard shows an 'Add a service' CTA when no instances
exist; ServicesPage already had a strong empty state.

Fixes from Slice 4 review:
- B1 (blocker): secret editing regressed because buildInput() hardcoded
  secrets:{} after the ConfigBody lift orphaned draftSecrets. Lifted
  draftSecrets to the parent ServicePage; buildInput now sends only the
  non-blank typed drafts ('leave blank to keep' semantics restored).
- S1: switcher trigger keys off enabled siblings, not total.

New: navEntries.ts + test, dashboards api/hook, service-tabs/ stubs +
index, ServiceTypePage, ServicePage tab skeleton + ConfigBody lift,
Dashboard empty-state CTA, ServicePage tab/switcher/secret-save tests.

Note: this branch is based on main (mobile-responsive-parity is unmerged);
the mobile SheetForm on ServicePage will be re-added when content tabs
get real content (slices 5-9). 84 tests pass (+1 secret-save guard);
lint/build green.

Refs openspec/changes/services-as-hub-ia/ (spec R1-R4/R9, tasks slice 4).
2026-06-26 19:03:18 +00:00
Developer a43d6a6206 Backend: drop users router, backups service attribution, named dashboards (Slice 3)
Users router removed:
- Delete routers/users.py + users_impl.py (Jellyfin-backed user directory,
  Jellyfin-email message compose, Jellyseerr enrichment).
- Drop orphaned get_jellyseerr_client dep from dependencies.py
  (get_user_id stays; used by dashboard/media/media_index_worker).
- clients/jellyseerr.py stays (still imported by widgets/sources.py).
- test_api.py TestUsers block + mock_jellyseerr fixture removed.

Backups service attribution:
- backup_jobs gains a nullable service_id column (PRAGMA migration).
- _resolve_backup_service_id helper: explicit service_id wins, else
  first-wins an enabled backups instance, else empty (backward-compat).
- Both report endpoints accept ?service_id= and persist it on the job.
- Dashboard summary + poller aggregate across all jobs unchanged.

Named dashboards backend:
- named_dashboards table (id, label, slug UNIQUE, sort_order, payload_json,
  timestamps) with full CRUD methods + _slugify/_unique_slug helpers.
- models/dashboards.py (NamedDashboardInput/NamedDashboard).
- routers/dashboards.py: GET/POST/PUT/DELETE /api/dashboards.
- Router registered in main.py.

Tests: test_dashboards.py (CRUD, slug collision, explicit slug, 404);
test_api.py trimmed. 271 backend tests pass (was 268; +6 dashboards -3
users); ruff clean.

Refs openspec/changes/services-as-hub-ia/ (spec R5/R6.1, tasks slice 3).
2026-06-26 18:33:24 +00:00
Developer 9370e52cfc Backend: Authentik directory client + endpoint (Slice 2)
AuthentikClient (clients/authentik.py) wraps Authentik's directory API:
- Bearer-token requests.Session, base_url normalization (rstrip / and
  trailing /api/v3), get() helper mirroring JellyseerrClient.
- users(search, page, page_size) calls GET /api/v3/core/users/ and
  normalizes Authentik's {pagination, results} shape into
  {items, total, page, page_size} for frontend consumption.

Directory endpoint (routers/authentik_users.py):
- GET /api/services/authentik/{service_id}/users resolves the service
  record, builds the client from decrypted api_token, returns the
  normalized user list.
- Graceful error handling matching monitoring.py: not-configured and
  unreachable return {items:[], total:0, error} with 200 (no 500s).
- _resolve_service_record copied in (self-contained; shared-utility
  extraction is a follow-up).

Router registered in main.py.

Tests: 12 new (8 client unit + 4 endpoint integration covering success,
not-configured, unreachable, URL/params). 268 backend tests pass; ruff
clean.

Refs openspec/changes/services-as-hub-ia/ (spec R6.2/R7.2, tasks slice 2).
2026-06-26 18:11:44 +00:00
Developer b3b167c075 Backend: add backups + authentik service types, absorb jellyseerr (Slice 1)
New service types:
- backups: BackupsConfig(ingestion_label), no secrets, summary widget kind.
  Modeled as a service so it can be named/multi-instanced like others.
- authentik: AuthentikConfig(base_url, timeout_seconds), api_token secret
  (required). Directory source for the upcoming Users tab.

Jellyseerr absorption:
- JellyfinConfig gains optional jellyseerr_url + jellyseerr_api_key fields.
- integrations/jellyseerr.py deleted; registry entry removed.
- clients/jellyseerr.py stays (JellyseerrClient still used by enrichment).
- One-time idempotent migration in settings_store.ensure_defaults():
  jellyseerr service rows merge into a paired Jellyfin (exactly-one merges;
  multiple picks first unpaired; none/all-paired drops with a logged
  warning). The api_key is decrypted from secrets before moving to config.

Registry is now 8 types: alertmanager, authentik, backups, grafana,
jellyfin, nextcloud, prometheus, ssh_tasks.

Tests: registry count updated to 8, jellyseerr-absent assertion, new-type
definition assertions, and migration tests (single-jellyfin merge, no-
jellyfin drop, idempotency). 256 backend tests pass; ruff clean.

Refs openspec/changes/services-as-hub-ia/ (spec R6, tasks slice 1).
2026-06-26 18:02:59 +00:00
Developer fe028b0e6f Plan services-as-hub IA rework (OpenSpec change)
Reorganize the app around services as the hub. Operational content
(Media, Files, Actions, Users, Backups) moves into type-specific tabs on
the service page. Top nav shrinks to Main Dashboard + named dashboards +
conditional per-type entries (appear when configured) + Services + Settings.

Decisions (D1-D17): conditional type entries; instance switcher for
multi-instance; Files/Actions into ssh_tasks tabs; Backups = new service
type; Users -> Authentik (in scope); Observability split per type (no
aggregate); main dashboard special at /; named dashboards = widgets +
pinned service links; each named dashboard = top entry; Authentik =
directory source (OIDC unchanged); Messaging -> Authentik users via SMTP;
Jellyseerr absorbed into Jellyfin config; standard tab skeleton
(Overview | content | Widgets | Config); Overview = health + metrics;
routing /services/:type/:id + /d/:slug; legacy routes 404; empty-state
CTAs.

Authentik directory client + endpoint included. 12 chained PRs forecast
(backend types -> frontend shell -> content tabs -> dashboards ->
cleanup -> verify).
2026-06-26 17:16:52 +00:00
Developer 3d331e4c72 Make service connection config editable on service page
The service detail page showed non-secret connection config (base_url,
user_id, username, timeout_seconds) as read-only. Render schema-driven
editable inputs (reusing the create-dialog pattern) with a draftConfig
state hydrated from the instance, and unify the save button to persist
both config and secrets. Number fields render as type=number; the base_url
schema description surfaces as helper text.
2026-06-26 09:52:43 +00:00
Developer eebc86a52b Enforce http(s) schema on service base_url fields
Add a shared ServiceBaseUrl type (BeforeValidator + Field description) in
integrations/base.py and apply it to base_url across all six service configs
(grafana, prometheus, alertmanager, jellyfin, jellyseerr, nextcloud). Missing
http:// or https:// schema now fails fast with a clear 422 instead of breaking
HTTP clients silently. Tests cover reject/accept cases; REQUIREMENTS updated.
2026-06-26 09:52:20 +00:00
Developer 56b919ea1f style(frontend/api): apply formatter to backups.ts and client.ts
Convert indentation to tabs and reflow long import lines. No behavior
change.

Co-authored-by: el Gentleman <gentleman@pi.local>
2026-06-26 09:10:32 +00:00
Developer 648320abfd chore(project-map): refresh .pi-map role/arch summaries
Regenerate project map artifacts across backend, docs, openspec, and
root to refresh role descriptions and architectural notes after recent
service-registry and observability changes.

Co-authored-by: el Gentleman <gentleman@pi.local>
2026-06-26 09:10:19 +00:00
Developer 7d252489de fix(api): return 503 instead of 500 when Jellyfin/SSH not configured
On a fresh deploy with no Jellyfin service configured yet,
get_jellyfin_client (and get_user_id / get_ssh_client) raised a plain
RuntimeError, which bubbled up as a 500 traceback on every
Jellyfin-dependent route (dashboard counts/libraries/activity, media,
users). Convert those RuntimeErrors to HTTPException(503) with a clear
detail message so FastAPI returns a clean 503 JSON response instead of
a 500, and the frontend can render a not-configured state.

- dependencies.py: get_jellyfin_client (no service / missing creds),
  get_user_id (no users discovered), and get_ssh_client (no SSH machine
  + no legacy key path) now raise HTTPException(503, detail=...).
- tests/test_api.py: added
  TestDashboard.test_jellyfin_endpoints_return_503_when_not_configured
  covering /api/dashboard/counts and /activity.

ruff clean; 240 backend tests pass.
2026-06-26 08:39:39 +00:00
Developer 04319025de fix(api): attach Bearer token to services/widgets/backups requests
Under AUTH_ENABLED=true, api/services.ts, api/widgets.ts, and
api/backups.ts called fetch() directly without attaching the OIDC
access token, so every services/widgets/backups request 401'd while
api/client.ts requests succeeded. The token was only attached in
client.ts.

Extract the auth-attaching fetch helpers (buildUrl/buildHeaders/
readErrorDetail + get/post/put/del/postForm) into a new api/shared.ts
that consults getAccessToken(), rewrite services.ts/widgets.ts/
backups.ts to use them, and consolidate client.ts to import from
shared.ts (removing its duplicated copies). Now every backend request
goes through one auth-attaching path.

As a side benefit, error messages surface the HTTP status + backend
detail instead of a generic "Failed to ..." string.

Bug masked in dev because dev runs AUTH_ENABLED=false. npm run build
clean; 0 lint errors; 72 frontend tests pass.
2026-06-26 08:18:39 +00:00
Developer 8bc209b27e docs: fix stale-live docs and drop obsolete Obsidian spec
Refreshes the docs that were actively misleading about the current
FastAPI + React + service-registry app, and deletes one obsolete design.

- CONTRIBUTING.md: full rewrite — Streamlit-era guidance replaced with
  the current backend (ruff/pytest, src/ layout) + frontend (npm
  lint/build/test) workflow, service-registry model, and shadcn/Tailwind
  stack. Mirrors AGENTS.md.
- README.md: removed the non-existent /addons/:addonId route (Services
  page is current); fixed the per-machine Jellyfin wording; replaced the
  py_compile dev snippet with ruff + pytest / npm lint+build+test.
- backend/README.md: updated the structure tree (removed deleted
  clients/resources.py; added routers backups/services/tasks/widgets,
  integrations/, models/, widgets/, workers/); dropped the "starts the
  collector" sentence (MonitoringPoller is decommissioned).
- frontend/README.md: corrected the uvicorn module path
  (main:app -> media_library_viewer_api.main:app).
- Deleted docs/superpowers/specs/2026-05-08-obsidian-documentation-design.md
  (Obsidian vault never built; stack refs MUI/D3/AG Grid all removed).

Historical docs (MIGRATION_PLAN, superpowers backup-monitoring, the
bannered design/runbook/context files) deferred to a later banner pass.
2026-06-25 09:07:19 +00:00
alex cbc2740e37 Update docker-compose.yml 2026-06-25 10:03:54 +02:00
Developer 6919158012 docs: complete Jellyfin migration, archive jellyfin-service-registry
Slice 3 (final) of jellyfin-service-registry. Documents the completed
migration and archives the SDD change.

- docs/REQUIREMENTS.md: marked the machine-level Jellyfin follow-up
  resolved; added a decision-log entry (Jellyfin no longer a machine
  service, dead media_root/path_prefix removed; global config +
  path_utils retained for Jellyfin->SSH path resolution).
- CHANGELOG.md: struck through the old follow-up note; added a
  Follow-up #2 section describing the machine field + service removal.
- Archived openspec/changes/jellyfin-service-registry (no active SDD
  changes remain).

Backend ruff clean / 239 tests pass; frontend 0 lint errors / build
clean / 72 tests pass.
2026-06-24 14:56:25 +00:00
Developer fd12e921fd refactor(settings): remove dead machine path fields from frontend
Slice 2 of jellyfin-service-registry. Removes the machine-level
media_root/path_prefix fields from the frontend now that the backend no
longer stores them.

- types/index.ts: dropped media_root/path_prefix from MonitoringMachine
  and MonitoringMachineInput.
- pages/Settings.tsx: removed the media_root form input, the read-only
  "Media root" detail (replaced with a local-hint field mirroring the
  editor), and media_root/path_prefix from emptyMachine() and both
  edit-handler reset mappings; updated the section description.
- tests: removed media_root/path_prefix from Settings/Media/FileBrowser
  test fixtures.

npm run build (tsc -b + vite) clean; 0 lint errors; 72 tests pass.
2026-06-24 14:51:28 +00:00
Developer 7107815a5c refactor(settings): drop jellyfin machine service + dead machine path fields
Slice 1 of jellyfin-service-registry. Jellyfin is configured exclusively
via the service registry now; the machine-level media_root/path_prefix
fields were dead duplicates of the global config.

- services/settings_store.py: DEFAULT_SERVICES no longer includes
  "jellyfin" (now ["monitoring", "files"]). Removed machine-level
  media_root/path_prefix from _default_local_machine, _row_to_machine,
  _normalize_machine_payload, _seed_local_machine, get_machine_config,
  and upsert_machine. _default_local_machine no longer reads global
  config, so the get_settings import is dropped.
- routers/settings.py: removed media_root/path_prefix from
  MonitoringMachineInput (dead API input; store already ignored them).

The global config remote_media_root/path_prefix properties + path_utils.py
are unchanged (files.py and media_index still use them for Jellyfin->SSH
path resolution). ruff clean; 239 backend tests pass.
2026-06-24 14:38:47 +00:00
Developer 38b2de54ff chore: archive observability-service-registry, track pi-map artifacts
- Archive the completed observability-service-registry SDD change into
  openspec/changes/archive/ (delivered across 5 slices; only
  jellyfin-service-registry remains active).
- Stop ignoring .pi-map.md / .pi-map.index.md so the navigation maps are
  versioned alongside the code, and add the regenerated map pairs repo-wide.
2026-06-24 13:28:23 +00:00
Developer c1610c93a1 docs(observability): refresh docs for service-registry end state
After the observability-service-registry slices removed the observability
env vars and the file-SD writer, several docs still instructed readers to
set vars that no longer exist. Updated the live config instructions;
historical decision-log entries are left intact.

- README.md: removed VITE_GRAFANA_URL/VITE_PROMETHEUS_URL/ALERTMANAGER_URL
  from compose examples and the env-var block; added a note that
  observability is configured on the Services page; updated Notes.
- frontend/README.md: dropped the stale VITE_* deep-link sentence.
- docs/REQUIREMENTS.md: fixed one stale trailing phrase in the
  externalization decision-log entry (VITE_* no longer "remain").
- docs/monitoring-logging-design.md: added a "Superseded mechanisms" note
  under Implementation Plan so the Phase 2/3 file-SD + alertmanager_url
  details read as historical, not current wiring.
- context.md: strengthened the status banner to cover the env->service-
  registry and file-SD->http_sd_configs shift; body marked historical.

.env.example is assistant-edit-blocked; updated replacement text provided
to the user separately.
2026-06-24 09:15:50 +00:00
Developer b1a66a1ab7 chore(observability): remove remaining observability env vars, docs
Slice 5 (final) of observability-service-registry. Completes the move to
service-registry-only observability config: no observability service env
vars remain.

- config.py: removed alertmanager_url + alertmanager_webhook_url fields.
- docker-compose.yml / docker-compose.dev.yml: removed ALERTMANAGER_URL,
  ALERTMANAGER_WEBHOOK_URL (backend env), and VITE_GRAFANA_URL,
  VITE_PROMETHEUS_URL (frontend build args / dev env).
- frontend/Dockerfile: removed the VITE_GRAFANA_URL / VITE_PROMETHEUS_URL
  ARG, build-stage ENV, and dev-stage ENV lines.
- docs: REQUIREMENTS decision-log entry; CHANGELOG Added/Changed/BREAKING
  for the observability service registry; backend/README monitoring
  section (Observability page, services page config, http_sd_configs,
  new health endpoints, log-only webhook).

The only observability env var remaining is PROMETHEUS_ENABLED (Manage's
own /metrics toggle). Grep-gated: no live references to the removed
vars/fields in backend src, frontend src, compose, or Dockerfile.

ruff clean; 239 backend tests pass; frontend 0 lint errors, build clean,
72 tests pass.

.env.example is assistant-edit-blocked; user follow-up noted in the SDD
tasks: drop the removed vars there too.
2026-06-24 08:49:53 +00:00
Developer b200025daa refactor(observability): drop file-SD writer for http_sd_configs
Slice 4 of observability-service-registry. Removes the shared-file
Prometheus bridge; external Prometheus now consumes node-exporter targets
via http_sd_configs against GET /api/monitoring/prometheus-targets.

- services/targets.py: removed write_prometheus_targets() (the file
  writer) and its json/Path/get_settings imports; updated module docstring.
  build_node_exporter_targets() is unchanged and still powers the HTTP
  endpoint.
- main.py: removed the startup write_prometheus_targets call.
- routers/settings.py: removed the _write_prometheus_targets helper and
  its three post machine create/update/delete call sites + the now-unused
  targets import.
- config.py: removed the prometheus_file_sd_dir field.
- docker-compose.yml / docker-compose.dev.yml: removed the
  PROMETHEUS_FILE_SD_DIR backend env var.
- tests: removed TestWritePrometheusTargets + the write_prometheus_targets
  import in test_targets.py; rewrote the two TestSettingsMachines tests to
  assert machines appear/disappear from /api/monitoring/prometheus-targets
  (the surviving HTTP path) instead of the removed file-writer side effect.

ruff clean; 239 backend tests pass.
2026-06-24 08:37:20 +00:00
Developer 0c5698c903 feat(observability): service discovery, health cards, alertmanager widget
Slice 3 of observability-service-registry (frontend). The Observability
page discovers Grafana from the service registry instead of env vars,
adds Grafana + Prometheus health cards, and ships an alertmanager
active_alerts dashboard widget.

- types: added GrafanaStatus + PrometheusStatus; added optional
  service_id/error to AlertmanagerStatus.
- api/client.ts + hooks/useObservability.ts: fetchGrafanaStatus,
  fetchPrometheusStatus, useGrafanaStatus, usePrometheusStatus.
- widgets/AlertmanagerAlertsWidget.tsx (new): presentational widget
  consuming the active_alerts summary shape (total/by_severity/alerts);
  exported from widgets/index.ts.
- integrations/registry.ts: alertmanager binding (active_alerts kind,
  30s refresh, optional severity_filter); registry.test.ts updated to
  6 service types incl alertmanager + a resolve test.
- components/ObservabilityPage.tsx: removed
  import.meta.env.VITE_GRAFANA_URL; derive GRAFANA_BASE_URL from the
  first enabled grafana service via useServiceInstances("grafana");
  added Grafana + Prometheus HealthCards (up/not-configured/unreachable)
  with QueryError retry blocks; machine dashboard shows a "No Grafana
  service configured" empty-state linking to /services when none is set.

npm run build (tsc -b + vite) clean; 0 lint errors; 72 frontend tests
pass. Reviewed fresh-context (read-only): no blockers.
2026-06-24 08:23:23 +00:00
Developer 14771ae990 feat(observability): resolve services from registry, add health endpoints
Slice 2 of observability-service-registry. The monitoring router resolves
observability components from the service registry instead of env vars.

- routers/monitoring.py: removed _alertmanager_client/_webhook_client env
  readers + the get_settings import. Added _resolve_service_record(store,
  service_type, service_id?) -> ServiceRecord|None (requested instance with
  type+enabled checks, else first enabled instance), plus _base_url/_timeout/
  _auth_headers (Bearer from api_key)/_status_response helpers.
- /alerts + /alertmanager-status now take service_id? + Depends(store),
  resolve an alertmanager service, return graceful not-configured/
  unreachable payloads including service_id/name; status down-branches now
  include peers:[] + error (fixes prior type drift).
- NEW /grafana-status (probes /api/health) and /prometheus-status (probes
  /-/healthy then /api/v1/status/buildinfo) returning
  {up,version,service_id,name,error}.
- Webhook receiver is now log-only (dropped the outbound
  ALERTMANAGER_WEBHOOK_URL forward).
- tests: rewrote TestAlertmanager + TestAlertmanagerWebhook to mock
  _resolve_service_record/requests.get (not-configured via empty registry);
  added TestGrafanaStatus/TestPrometheusStatus and a TestResolveServiceRecord
  unit class covering service_id match/type-mismatch/disabled and first-
  enabled/none-enabled paths.

Orphaned config fields alertmanager_url/alertmanager_webhook_url and the
env-var removal land in Slice 5. ruff clean; 240 backend tests pass.

Reviewed fresh-context (read-only): no blockers.
2026-06-24 07:53:25 +00:00
Developer 7d49df3e7d feat(observability): add alertmanager service type and widget
Slice 1 of observability-service-registry. Alertmanager becomes a
first-class service-registry type, mirroring grafana/prometheus.

- integrations/alertmanager.py (new): AlertmanagerConfig
  (base_url, timeout_seconds), AlertmanagerAlertsWidgetConfig (optional
  severity_filter), shared summarize_alerts() helper, and DEFINITION
  (service_type "alertmanager", secret api_key, widget "active_alerts").
- integrations/registry.py: register ALERTMANAGER (7 types now).
- widgets/sources.py: AlertmanagerWidgetSource fetches
  {base_url}/api/v1/alerts, sends optional Bearer token from the api_key
  secret, applies optional severity_filter, and summarizes via the shared
  helper; registered in SERVICE_ADAPTERS.
- routers/monitoring.py: _summary_from_alerts delegates to the shared
  summarize_alerts (behavior unchanged).
- tests: registry now 7 types; /api/services/types lists alertmanager;
  4 new adapter tests (summarize, severity filter, bearer token, missing
  service).

Backend-only slice; the frontend active_alerts widget binding lands in a
later slice. ruff clean; 228 backend tests pass.

Reviewed fresh-context (read-only): no blockers.
2026-06-23 22:25:50 +00:00
Developer c13e274ca4 docs(openspec): re-scope observability-service-registry change
Rename grafana-prometheus-polish -> observability-service-registry and
rewrite proposal/design/tasks for the approved vision: all observability
integration (alertmanager, grafana, prometheus) configured as service-
registry instances in the UI, surfaced on a dedicated page, with widgets
per service definition -- nothing in the env.

Key scope decisions captured:
- Add alertmanager as a 6th service type + active_alerts widget.
- Rewire /alerts + /alertmanager-status to resolve from service records
  (first-enabled-instance default; no primary flag in v1).
- Add /grafana-status + /prometheus-status health endpoints.
- Observability page discovers services; kill VITE_GRAFANA_URL /
  VITE_PROMETHEUS_URL deep-links.
- Webhook receiver stays log-only (drop the outbound forward).
- Remove PROMETHEUS_FILE_SD_DIR + the file-writer; external Prometheus
  uses http_sd_configs against GET /api/monitoring/prometheus-targets.
  build_node_exporter_targets + that endpoint stay.
- PROMETHEUS_ENABLED stays (Manage's own /metrics toggle).
- End state: zero observability *service* env vars.

Plan = 5 slices, each <=400 changed lines, green tests/lint/build,
commit per slice.
2026-06-23 21:48:58 +00:00
Developer d4f95b64d4 chore(observability): externalize stack from root compose files
Manage now connects to existing Grafana/Prometheus/Alertmanager instances
and never deploys its own stack.

- docker-compose.yml / docker-compose.dev.yml: removed prometheus, loki,
  alloy, grafana, alertmanager, node-exporter services, the monitoring
  network, and observability named volumes; they now ship only backend +
  frontend. Dev frontend now joins the web network so the Vite dev proxy
  can reach the backend.
- backend: alertmanager_url default is now empty; /api/monitoring/alerts
  and /alertmanager-status return graceful "not configured" responses
  when ALERTMANAGER_URL is unset. Added not-configured tests.
- docker-compose.observability.yml: kept as the optional standalone
  example; header clarifies Manage does not deploy it.
- Removed orphaned combined monitoring/prometheus/prometheus.yml
  (standalone stack uses prometheus.standalone.yml).
- Docs (README, REQUIREMENTS decision log, monitoring-logging-design,
  observability-runbooks, context.md, MIGRATION_PLAN, frontend/README,
  CHANGELOG) updated to the connect-to-existing model.

VITE_GRAFANA_URL / VITE_PROMETHEUS_URL remain as optional frontend
deep-link overrides. .env.example still needs a manual update (safety
policy blocks assistant edits): set ALERTMANAGER_URL empty/optional and
move standalone-only vars out of the root file.
2026-06-23 21:20:07 +00:00
Developer 4d520ab0e3 docs(openspec): add SDD artifacts for next changes
- jellyfin-service-registry: proposal, design, and tasks for completing
  the Jellyfin migration off machine-level config.
- grafana-prometheus-polish: proposal, design, and tasks for improving
  the Grafana/Prometheus observability integration.

Both are planning-only artifacts; implementation not started.
2026-06-23 20:40:35 +00:00
Developer ca8927834e chore(openspec): archive completed changes
Move finished change directories to openspec/changes/archive/:
- configurable-dashboard-widgets
- decommission-monitoring-poller
- service-registry
- unify-tasks-on-services

All associated implementation has been merged to main.
2026-06-23 19:38:34 +00:00
Developer a39dbf272c docs(backend): remove legacy monitoring poller endpoints from README
The legacy SSH-scraping MonitoringPoller and its endpoints were
decommissioned earlier; update the backend README endpoint list and
Monitoring description to match the current Alertmanager + Prometheus
targets + Grafana observability model.
2026-06-23 17:52:59 +00:00
Developer 50eb76a10d feat(tasks): unify saved tasks on ssh_tasks services
- Add shared task_runner.run_saved_task helper used by routers/tasks.py and
  widgets/sources.py SshTaskWidgetSource.
- Saved tasks now target ssh_tasks service instances via default_service_id;
  the legacy default_machine_id and saved_task_runs are removed.
- Actions page lists ssh_tasks services for default and run-time selection.
- Update types, API client, hooks, tests, docs, and changelog.

Backend tests: 222 passed. Frontend lint/build/test: clean (71 passed).
2026-06-23 16:46:46 +00:00
Developer d7ad933b2a Merge pull request 'docs(unify-tasks): SDD artifacts' from docs/unify-tasks-sdd into main 2026-06-23 13:05:53 +00:00
Developer 8c69911252 docs(unify-tasks): SDD proposal, design, and tasks
Design-only artifacts for unifying saved tasks on ssh_tasks services.
No implementation yet.

- proposal: two-path problem (Actions→machine vs widget→service), goals,
  non-goals, grilling decisions (SSH-only, service_task_runs only, keep override)
- design: shared run_saved_task helper, column rename, saved_task_runs dropped,
  API + frontend changes, 2-slice plan
- tasks: backend (shared runner + router) + frontend (Actions page)
2026-06-23 13:05:52 +00:00
Developer 7b3e2ebace Merge pull request 'chore: remove dead machine-level Jellyfin/Jellyseerr fields' (#13) from chore/remove-dead-machine-jellyfin-fields into main 2026-06-23 12:55:32 +00:00
Developer cfb9977532 chore: remove dead machine-level Jellyfin/Jellyseerr fields
Follow-up #1 to the service-registry change. Jellyfin/Jellyseerr now resolve
from the service registry, so the machine-level app fields are dead config.

- dependencies.py: drop dead _jellyseerr_client_for; simplify _resolve_machine
  to SSH-only.
- settings_store.py + routers/settings.py: remove jellyfin_*/jellyseerr_* from
  machine default config, get_machine_config, normalization, row mappers, and
  MachineInput.
- frontend types + Settings.tsx: drop the fields and the Jellyfin/Jellyseerr
  form sections + service options.
- Update frontend test fixtures.

Existing DB rows may still carry these keys in config_json; they are inert and
drop on the next machine save. Verification: backend ruff clean, pytest 222;
frontend lint 0 errors, build success, 70 tests.
2026-06-23 12:54:27 +00:00
Developer 802a9202e9 Merge pull request 'feat(services): select Jellyfin via jellyfin_service_id on the frontend' (#12) from feat/service-registry-jellyfin-services-frontend into main 2026-06-23 12:28:53 +00:00
Developer 7ab9b1ac59 style(tests): apply formatter to Applications and Media tests 2026-06-23 12:28:53 +00:00
Developer cbb703341e feat(services): select Jellyfin via jellyfin_service_id on the frontend
Slice 4b frontend half. Jellyfin-touching pages now select a Jellyfin service
instance instead of a machine.

- api/client.ts: Jellyfin-backed calls (counts/libraries/activity/users, media
  status/build/stop/force-stop, queryMedia) send jellyfin_service_id.
- hooks/useDashboard, useUsers, useMedia: selector param renamed to
  jellyfinServiceId.
- pages/Media + Applications: list jellyfin service instances and persist
  jellyfin_service_id in the URL.
- Dashboard (widgets) and Users (default instance) need no selector change.
- Update Applications + Media tests for the new hook/param.

Files/SSH transport keeps machine_id. Verification: frontend lint 0 errors,
build success, 70 tests; backend ruff clean, 222 tests.
2026-06-23 12:10:27 +00:00
Developer a13f560df2 Merge pull request 'feat(services): resolve Jellyfin/Jellyseerr from the service registry (backend)' (#11) from feat/service-registry-jellyfin-services-backend into main 2026-06-23 11:48:25 +00:00
Developer 5eb49be697 style(dependencies): apply formatter to dependencies rewrite 2026-06-23 11:48:25 +00:00
Developer 8ff735d644 feat(services): resolve Jellyfin/Jellyseerr from the service registry (backend)
Slice 4b backend half. Jellyfin and Jellyseerr clients are now resolved from
service instances instead of machine-level app config.

- Add jellyseerr service definition (6 service types total); add user_id to
  the Jellyfin service config.
- dependencies.py: jellyfin_service_id query param + _service_record
  (decrypt-on-read); get_jellyfin_client / get_jellyseerr_client / get_user_id
  resolve against the service registry (first enabled instance as fallback).
- SSH/Files transport (get_ssh_client) unchanged; still uses machine_id.
- Update service-registry tests for 6 types.

Selection model: split params — ?jellyfin_service_id= for Jellyfin/Jellyseerr,
?machine_id= for SSH/Files. Frontend threading follows in the next PR.

Verification: backend ruff clean, pytest 222 passed; frontend green (unchanged).
2026-06-23 11:43:33 +00:00
Developer d998e6ab0c Merge pull request 'feat(services): cleanup, services admin UI, docs' (#10) from feat/service-registry-cleanup-services-ui into main 2026-06-23 11:07:20 +00:00
Developer 9a6cbfae68 style(services): apply formatter to App and ServicesPage 2026-06-23 11:07:19 +00:00
Developer c9c72be0b6 feat(services): cleanup, services admin UI, docs
PR 4a of the runtime service registry change.

- Remove addon pages (/addons/:addonId, AddonPage, addons/*) superseded by
  service pages.
- Remove grafana_url/prometheus_url from backend config, compose, .env.example,
  and README (URLs now live on service records; VITE_ frontend deep-link vars
  retained).
- Add Services page (/services) with create/list/delete + sidebar nav, so
  services are configurable in the tool itself and service pages are reachable.
- Update docs/REQUIREMENTS.md service-registry section; add CHANGELOG.md with
  the breaking-upgrade note (MANAGE_ENCRYPTION_KEY required; grafana/prometheus
  env vars removed; default widget seeding removed).

Verification: backend ruff clean, pytest 222 passed; frontend lint 0 errors,
build success, 70 tests passed.
2026-06-23 10:57:30 +00:00
Developer 5ec35b4849 Merge pull request 'feat(services): frontend services runtime and widget rebind' (#9) from feat/service-registry-frontend-runtime into main 2026-06-22 19:13:59 +00:00
Developer 739ad38e29 style(services): apply formatter to frontend services runtime 2026-06-22 19:13:59 +00:00
Developer 1da67f38c7 feat(services): frontend services runtime and widget rebind
PR 3 of 4 for the runtime service registry change.

- Add service + new-shape widget TypeScript types; widgets carry service_id
  + widget_kind (service-bound) or null (built-in).
- Add services API client + TanStack Query hooks; reconcile the widget API
  client/hooks to the new endpoints (remove sources/types; add builtin kinds).
- Add closed frontend service registry (integrations/registry.ts) mirroring the
  backend, with resolveWidget(widget, services) mapping a widget to its
  component + refresh interval.
- Add ServicePage at /services/:serviceType/:serviceId with config view,
  empty-on-edit secret inputs + 'set' badges, enable toggle, delete, and the
  service's widget-kind list.
- Register /services/:serviceType/:serviceId in App.tsx.
- Reconcile the six widget components to refreshIntervalMs + description props;
  rewrite WidgetConfigDialog around a service -> widget-kind picker.
- Update Dashboard test; add integrations/registry.test.ts.

Verification: frontend lint 0 errors, build success, 70 tests passed; backend
ruff clean, 222 tests passed.
2026-06-22 18:59:41 +00:00