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.
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.
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.
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.
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.
- 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.
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.
- 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).
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.
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.
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.
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.
PR 4 of 4 for configurable dashboard widgets.
- Replace hard-coded Jellyfin/Backups dashboard sections with a loop that
renders enabled widget instances by sort_order.
- Add WidgetInstance renderer and WidgetConfigDialog for adding, editing,
enabling/disabling, deleting, and reordering widgets.
- Add addon pages for grafana, prometheus, and ssh-tasks at /addons/:addonId.
- Register /addons/:addonId route in App.tsx.
- Update docs/REQUIREMENTS.md with the widget system design and API.
Verification:
- backend ruff clean; pytest 200 passed
- frontend npm run lint: 0 errors
- frontend npm run build: success
- frontend npm run test -- src/widgets/registry.test.ts: 3 passed
Web UI rework. Completes the DataGrid migration (7a + 7b):
- pages/Media.tsx off @mui/x-data-grid + @mui/material onto DataTable:
15 locked columns (title/series/season/episode/type/year/runtime_min/
size/bitrate/hdr/video/resolution/date_added/library/path);
enablePagination + manualPagination + rowCount from queryResult.total;
page state (pageIndex/pageSize) -> offset/limit into useMediaQuery;
onRowClick -> navigate('/files?path=...') preserved; stable path-derived
getRowId so selection survives server paging; column-visibility toggle.
Hard rule honored: NO sorting, NO resizing (visibility-only).
- Migrate Media shell (Select/Input/Progress/Card/grid/Typography/Tabs).
- Media component tests (column set + row-click nav).
- Harness fix: polyfill ResizeObserver in test/setup.ts — jsdom lacks it
and Radix primitives (Select/ScrollArea/etc.) reference it; was causing
cross-test failures once Media pulled shadcn Select into the pool.
Gate: build + lint + test green (23 files / 64 tests).
- Switch docker-compose.observability.yml from named volumes to host
bind mounts under OBSERVABILITY_DATA_ROOT, defaulting to
./observability-data.
- Make all service ports configurable via environment variables
(PROMETHEUS_PORT, LOKI_PORT, ALLOY_PORT, GRAFANA_PORT,
ALERTMANAGER_PORT, NODE_EXPORTER_PORT).
- Add VITE_GRAFANA_URL handling to ObservabilityPage so Grafana links
point to the configured standalone instance.
- Update docs/observability-runbooks.md with the env variable table,
reachable-web-UI table, and backup/restore instructions for the new
host-directory layout.
- Replace embedded Grafana iframes in ObservabilityPage with external
Grafana link cards. Grafana URL is configurable via VITE_GRAFANA_URL
(frontend) or GRAFANA_URL (backend env exposed to frontend build).
- Remove the grafana scrape job from monitoring/prometheus/prometheus.yml
so the main Prometheus stack no longer depends on Grafana.
- Pass GRAFANA_URL through docker-compose.yml and docker-compose.dev.yml.
The dark mode button was toggling React state but never applying it to the
DOM. Add a useDarkMode hook that:
- Persists the preference in localStorage
- Defaults to the OS prefers-color-scheme on first visit
- Adds/removes the 'dark' class on <html> to trigger CSS variable overrides
Also add dark-mode color overrides for all Tailwind v4 theme variables in
index.css.
Co-authored-by: openhands <openhands@all-hands.dev>
Refactor SSHKeyManager to match app layout pattern:
- List of SSH keys on the left (SelectionRailCard)
- Key details and form on the right (SectionCard)
- Add selectedSSHKeyId state to Settings component
- Click a key to select and populate the form
- New key button to create a new key
- Shows fingerprint and public key for selected key
- Delete button moved to the right panel