Commit Graph

239 Commits

Author SHA1 Message Date
Developer c886fcdf09 spec(grafana-metric-gateway): verify + close GM-115 + reconcile tracking
Add 3 test cases (startup old-config validation warning; status auth_failed
for 401/403) closing the GM-115 PARTIAL. Write apply-progress.md, tick all 33
tasks, add verify-report.md (15/16 PASS, 1 PARTIAL->PASS). Gates green: 331+
pytest, ruff clean, npm build+lint 0 errors, 151 vitest.
2026-07-09 21:53:18 +00:00
Developer 7e91e7f931 feat(grafana-metric-gateway): slice 2 — rename widgets to Metric*
git mv PrometheusChartWidget→MetricChartWidget, PrometheusGaugeWidget→
MetricGaugeWidget, PrometheusMeanWidget→MetricMeanWidget (+ 3 test files,
R100 history preserved). Update registry imports/refs + barrel exports.
Adapt PrometheusMetricWidget for §3.4 Option A: read normalized {result:
[{label,points}]} series shape (last-point extraction) instead of old Prom
{resultType,result} vector. PrometheusMetricWidget NOT renamed (design §3.1).
GM-111/112/116 satisfied. All gates: 151 vitest, build exit 0, lint 0 errors.
2026-07-09 21:34:26 +00:00
Developer df80c68f89 feat(grafana-metric-gateway): slice 1 — backend gateway transport
Route all prometheus widget queries through Grafana /api/ds/query instead of
direct Prom HTTP. PrometheusConfig: drop base_url, add grafana_url +
datasource_uid; secret grafana_api_key (required). PrometheusWidgetSource →
MetricSource with _gateway_query POST method. normalize_grafana_frames
recovered from 65bae95 + shared _dedup_label helper. Gateway-path status
check. Startup old-config validation. CHANGELOG migration note. All adapter
tests rewritten for POST /api/ds/query + Grafana frames mock. Backend: 331
pytest pass, ruff clean. Frontend: build green (unchanged in S1).
2026-07-09 21:26:05 +00:00
Developer 798196ffc7 spec(grafana-metric-gateway): add tasks (2 slices, each <=400 lines)
S1 backend transport: gateway config + normalize_grafana_frames (from 65bae95)
+ MetricSource adapter + status + validation + CHANGELOG + tests. S2 frontend:
git mv widget renames to Metric* + registry/barrel updates. Each slice leaves
pytest/npm build/npm lint green. 5 risk flags incl first non-additive sync.
2026-07-09 21:01:38 +00:00
Developer 872e95f8f7 spec(grafana-metric-gateway): add design
8 design decisions: PrometheusConfig (grafana_url/datasource_uid + grafana_api_key
secret), /api/ds/query body per widget kind (window presets -> intervalMs/
maxDataPoints), normalize_grafana_frames refactored from 65bae95 into
prometheus_range.py (shares label-dedup with matrix normalizer), MetricSource
adapter, gateway-path status check, git mv widget renames, startup old-config
validation. 2-slice plan. 3 source findings flagged.
2026-07-09 20:57:07 +00:00
Developer bf8de32815 spec(grafana-metric-gateway): add spec (16 reqs GM-101..116)
Gateway transport: prometheus service config gains grafana_url/api_key/
datasource_uid; all queries via POST /api/ds/query; frames->series restored.
First non-additive canonical sync: MODIFIES 17 SC- requirements (transport
changes, intent preserved), PRESERVES 11, ADDS 3 (gateway status, startup
validation, sanctioned-transport statement). 3 spec assumptions settled where
proposal was silent.
2026-07-09 20:49:45 +00:00
Developer 8afdd9c2bc spec(grafana-metric-gateway): add proposal
Route metric queries through Grafana /api/ds/query instead of direct Prom
(Prom is firewalled / unreachable from Manage; Grafana is the only path).
Partial revert of prometheus-direct-charting: keep the gauge/mean/
LineSeriesChart rendering, restore the frames->series normalizer (recovered
from git 65bae95), change prometheus service config to hold Grafana gateway
fields (url + api_key + datasource_uid). Rename widgets to neutral Metric*.
First non-additive canonical sync (prometheus-charting MODIFIED). Updates the
pending service-credential-tester proposal dependency.
2026-07-09 20:41:59 +00:00
Developer 3e3e69b0be spec(service-credential-tester): add proposal
On-demand per-service-type credential tester for the add/edit dialog. Model on
test_machine_ssh. POST /api/services/test dispatching to per-type routines
(qBit login+maindata, Prom instant query, Jellyfin /Users, etc.) returning
{ok, detail, evidence}. Frontend Test button + gate Create/Save on pass with
'Save anyway' override. Resolves #3 (qBit login failures visible in UI not
logs) and complements #1 (validation surfacing, already shipped).
2026-07-09 20:13:57 +00:00
Developer cadb6d0991 fix(nav): add qBittorrent to per-service-type navbar entries
qBittorrent was missing from SERVICE_TYPE_NAV_ENTRIES, so configured qBit
instances never appeared in the left nav (unlike jellyfin/prometheus/etc.).
Add entry with Magnet icon. navEntries.test.ts filters by configured types
(no fixed-count assertion) so it stays green.
2026-07-09 20:11:12 +00:00
Developer 493c0e1aeb fix(services): surface validation errors in add-service dialog
CreateServiceDialog.save() awaited mutateAsync without a try/catch, so a
backend 422 (e.g. base_url missing http:// schema) threw uncaught and the
dialog sat silent with no feedback. Wrap in try/catch, hold the error in
local state, render a destructive Alert above the footer. Reset/onClose
only on success; on error the user can fix and retry.
2026-07-09 20:10:46 +00:00
Developer a9381e2471 spec(per-instance-hook-scoping): add proposal
Correctness fix: observability + backup hooks query globally, so multi-instance
service pages show data for the wrong instance. Tabs already accept instance
prop with TODO comments; backend mostly supports service_id already. Scope:
add serviceId to 6 hooks + fetch fns + 3 tabs; add service_id to backup
endpoints. Backward-compatible (optional params). ~250-350 lines, single slice.
2026-07-09 14:35:14 +00:00
Developer e461279566 chore(services-as-hub-ia): archive verified change (paperwork-only)
Code merged to main on 2026-06-26 (01527ae) + fix passes. All 10 ACs PASS.
~8600 ins / ~3900 del across 103 files. Only SDD artifacts were untracked.
No code changes, no canonical sync. NOTE: verify-report describes a Grafana
LinksTab later removed by prometheus-direct-charting (2026-07-08); historically
accurate point-in-time record — see archive-report.md footnote.
2026-07-09 14:11:15 +00:00
Developer 1fe7c06083 chore(mobile-responsive-parity): archive verified change (paperwork-only)
Code merged to main on 2026-06-26 via rebase (01527ae) + bug-fix passes
(5a43894/04871bd/f7f590f). All 8 ACs PASS. Only SDD artifacts were untracked;
this archive closes the paperwork gap. No code changes, no canonical sync.
See archive-report.md for carry-over notes (iOS real-device test residual).
2026-07-09 14:11:05 +00:00
Developer ec7ebd7013 chore: regenerate stale .pi-map.md / .pi-map.index.md project maps
Project map had drifted: claimed recharts unused (was used), ObservabilityPage.tsx
exists (refactored to service-tabs/), missed JellyfinNowPlayingWidget +
authentik/backups service types. Regenerate to reflect post-change reality:
new Qbittorrent/LineSeriesChart/Prometheus{Gauge,Mean}Widget files,
service_data.py/qbittorrent_store.py, canonical prometheus-charting +
service-storage specs, archived changes.
2026-07-09 14:05:56 +00:00
Developer 98f17f6e5d chore: apply pre-existing formatter reformat to MediaTab.tsx
Whitespace-only line reflow (formatter-on-save artifact from a prior session,
not logic). Committing to pristine the working tree.
2026-07-09 13:50:36 +00:00
Developer 5cb5e79032 chore(service-storage-harness): archive verified+synced change
Move to openspec/changes/archive/2026-07-09-service-storage-harness/
(history preserved via rename detection). 9 artifacts: proposal/spec/design/
tasks/apply-progress/verify-report/sync-report/archive-report + delta spec.
Canonical openspec/specs/service-storage/ remains. Native status engine
discrepancy (ambiguous change selection) disregarded per parent verification.
2026-07-09 09:47:05 +00:00
Developer c9201a004c spec(service-storage-harness): sync into canonical service-storage domain
New canonical openspec/specs/service-storage/spec.md (28 reqs SS-101..128).
Change-side delta + sync-report. web-ui + prometheus-charting canonicals
untouched.
2026-07-09 09:37:22 +00:00
Developer 40a7ac80d3 spec(service-storage-harness): verify + reconcile tracking
Write apply-progress.md, tick all 35 tasks, add verify-report.md (28/28
SS-101..128 PASS). Gates green: 322 pytest, ruff clean, FE build+lint 0
errors, PrometheusChartWidget extraction 4/4 non-regressive. No blocking
code findings. Process note: slice 2 was +644 lines over 400-line budget
(additive, no scope creep; retrospectively extract+widgets could split).
2026-07-09 09:30:52 +00:00
Developer c9404f0794 spec(service-storage-harness): add spec (28 requirements SS-101..128)
Covers harness lifecycle, QbittorrentSampleStore, QbittorrentClient,
3 widget kinds (totals=item count, active=DL/UL, speed=LineSeriesChart
reuse), MediaIndex migration (+service_id, scoped replace_items bug fix,
backward-compat query), cascade-delete wiring, test/build greenness.
2026-07-09 09:15:32 +00:00
Developer 75c949ad25 feat(service-storage-harness): slice 4 — cascade-delete wiring + integration test
Wire ServiceDataHarness.cascade_delete into SettingsStore.delete_service
(best-effort try/except, logs on failure). Fix migration runner to also
catch 'no such table' on fresh DBs (ALTER TABLE before init_schema).
Integration test proves end-to-end cascade across both concerns (qBit
samples + media items) with multi-instance preservation.

Backend: 322 pytest pass, ruff clean.
2026-07-09 09:11:42 +00:00
Developer c87f398e37 feat(service-storage-harness): slice 3 — migrate MediaIndex onto harness (scoped replace_items, +service_id)
Register MediaIndex as a harness concern with ALTER TABLE migration to add
service_id column (idempotent). Scope replace_items by service_id (FIXES latent
global-clear bug where building for one Jellyfin wiped another's rows). Scope
query by service_id (empty-string = all rows, backward-compat). Thread
service_id through build_media_index + worker + query_media router. New
regression test proves scoped replace preserves other services' rows.

Backend: 321 pytest pass, ruff clean. Frontend: build green.
2026-07-09 09:00:31 +00:00
Developer 1fb12b8a0a feat(service-storage-harness): slice 2 — qbit widgets + LineSeriesChart extract 2026-07-09 08:49:20 +00:00
Developer e7bd0afdd1 feat(service-storage-harness): slice 1 — harness + qbit store + client + integration
ServiceDataHarness (services/service_data.py): lifecycle-only registry of
per-concern storage — DB provisioning, idempotent migrations (ALTER TABLE
duplicate-column-name caught per-statement), cascade_delete(service_id).
QbittorrentSampleStore: append/window/prune (MAX_SAMPLES=120) in
qbittorrent.db. QbittorrentClient: cookie-login Web API client (403 re-login,
/sync/maindata). Integration registered with 3 widget kinds (totals/active/
speed). Harness initialized in main.py lifespan.

Backend: 314 pytest pass (21 new), ruff clean.
2026-07-09 08:24:08 +00:00
Developer 9a251db23c spec(service-storage-harness): add tasks (4 slices, each <=400 lines)
S1 harness+store+client+integration; S2 widget adapter+3 FE widgets+
LineSeriesChart extract; S3 MediaIndex +service_id migration (fixes latent
global-clear bug); S4 cascade-delete wiring. Each slice leaves pytest/npm
build/npm lint green. Risk flags on LineSeriesChart extract + MediaIndex migration.
2026-07-09 08:02:31 +00:00
Developer cff082c7a1 spec(service-storage-harness): add design
10 design decisions incl: lifecycle-only harness, per-concern DB files,
QbittorrentSampleStore (120-sample cap), qBit cookie client, 3 widget kinds
(totals=item count, active=DL/UL state, speed=reuses Change A renderer via
shared LineSeriesChart extract), MediaIndex +service_id backfill, scoped
replace_items (fixes latent global-clear bug), best-effort cascade-delete.
4-slice plan. 3 source findings (worker already threads service_id).
2026-07-09 07:55:59 +00:00
Developer bc389ae3f7 spec(service-storage-harness): reconcile proposal post Change A + question round
Resolve Q1 (reuse Change A's PrometheusChartWidget renderer via InService
data path), Q2 (totals = item count, not transfer bytes), Q3 (active =
downloading/uploading), Q4 (N instances), Q5 (username/password cookie
auth). Remove TBD/stale thin-dashboard caveats.
2026-07-09 07:45:55 +00:00
Developer 7efc06a629 chore(prometheus-direct-charting): archive verified+synced change
Move to openspec/changes/archive/2026-07-08-prometheus-direct-charting/
(git mv, history preserved). 9 artifacts: proposal/spec/design/tasks/
apply-progress/verify-report/sync-report/archive-report + delta spec.
Canonical openspec/specs/prometheus-charting/ remains.
2026-07-08 23:01:35 +00:00
Developer 3e77075171 spec(prometheus-direct-charting): sync into canonical prometheus-charting domain
New canonical domain openspec/specs/prometheus-charting/spec.md with all
27 requirements (SC-101..127) as the durable post-change contract. Change-side
delta specs/prometheus-charting/spec.md + sync-report.md. web-ui canonical
untouched (different concern).
2026-07-08 22:57:29 +00:00
Developer 7440603cdb spec(prometheus-direct-charting): verify + close SC-125 + reconcile tracking
Add loading-state tests to the three Prometheus widget test files
(closes SC-125 PARTIAL). Write apply-progress.md, tick all 39 tasks,
add verify-report.md (26/27 PASS, 1 PARTIAL->PASS). All gates green:
293 pytest, ruff clean, npm build+lint 0 errors. No blocking findings.
2026-07-08 22:50:30 +00:00
Developer 67ca0fc3bc feat(prometheus-direct-charting): slice 3 — remove grafana + config rewrite + changelog
Remove the entire Grafana surface: integrations/grafana.py, GrafanaWidgetSource
(+ _fetch_chart, now redundant since prometheus chart exists), GrafanaLinkWidget,
LinksTab, get_grafana_status endpoint, useGrafanaStatus hook, GrafanaStatus type,
fetchGrafanaStatus client fn, registry/nav/tab entries (FE+BE). Rewrite
config.yaml thin-dashboard rule to match reality (recharts is sanctioned for
Prometheus-backed series). CHANGELOG migration note added.

Backend: 293 pytest pass, ruff clean. Frontend: build+lint green (0 errors).
SC-115/116 grep-clean (only prometheus_range.py migration comments + Dashboard.test
shortcut fixture remain — both spec-allowed).
2026-07-08 22:35:06 +00:00
Developer 65bae95e3c feat(prometheus-direct-charting): slice 2 — gauge + mean widgets
Add gauge widget (recharts RadialBarChart with configurable threshold
bands, scalar-only per SC-111) and mean widget (client-side average over
range-query window, scalar-only per SC-114). Extract shared _instant_query
helper from the metric path; _fetch_gauge and _fetch_mean dispatch in
PrometheusWidgetSource.fetch(). Both new widget kinds declared in
integrations/prometheus.py and frontend registry.

Backend: 305 pytest pass, ruff clean. Frontend: 136 vitest pass, build+lint green.
2026-07-08 22:10:11 +00:00
Developer 5dad98231f feat(prometheus-direct-charting): slice 1 — prom range query + chart rebrand
Add PrometheusWidgetSource._fetch_chart hitting /api/v1/query_range directly
(SC-101..104). New shared helpers in widgets/prometheus_range.py:
step_for_window (window preset -> step, ~200pts) and normalize_prometheus_matrix
(extracted label/dedup rule, retargeted at Prom matrix, robust to malformed
data). Chart widget kind moved grafana->prometheus in both registries;
GrafanaChartWidget renamed -> PrometheusChartWidget (git mv, recharts body
preserved). Grafana binding/service untouched (removed in slice 3).

Backend: 298 pytest pass, ruff clean. Frontend: 128 vitest pass, build+lint green.
2026-07-08 21:55:02 +00:00
Developer d906b0392b spec(prometheus-direct-charting): add tasks (3 slices, each <=400 lines)
S1 adds Prom chart (range query + rebrand, no grafana removal); S2 adds
gauge+mean; S3 removes grafana + config rewrite + changelog. Each slice
leaves pytest/npm build/npm lint green. Review-workload forecast per slice.
2026-07-08 21:33:16 +00:00
Developer 9b7415080b spec(prometheus-direct-charting): patch SC-116/118 for service-tabs refactor
ObservabilityPage.tsx was refactored into service-tabs/; update removal
criteria to name real targets and whitelist the Dashboard.test fixture
shortcut-label collision.
2026-07-08 21:29:09 +00:00
Developer 78e273efe8 spec(prometheus-direct-charting): add design
5 locked design decisions: step derivation formula (max(15, round(s/200))),
shared normalize_prometheus_matrix helper in widgets/prometheus_range.py,
recharts RadialBarChart gauge w/ threshold bands, mean via client-side
avg over query_range, 3-slice plan each <=400 lines.
Source-verified: ObservabilityPage refactored into service-tabs/ (map stale).
2026-07-08 21:28:02 +00:00
Developer b7e5ca3cbc spec(prometheus-direct-charting): add proposal + spec
Drop Grafana as chart middleman; query Prometheus directly via
/api/v1/query_range. Rebrand GrafanaChartWidget -> PrometheusChartWidget,
add gauge + mean widget kinds, remove Grafana surface, rewrite stale
thin-dashboard rule in config.yaml. 27 acceptance requirements (SC-101..127).
2026-07-08 21:21:14 +00:00
Developer 67c51f9fc0 Fix: user_id validation causes read timeout on slow Jellyfin
The previous fix called client.users() unconditionally to validate the
user_id, adding an extra HTTP round-trip before the build. On a slow
Jellyfin connection this burned through the 10s timeout before the
actual libraries() call.

Restructured to try libraries(user_id) directly first. Only when that
fails does the worker resolve the username via the users API. So:
- Correctly configured user_id (internal hash): zero extra round-trips.
- Username like 'admin': libraries() fails → users() resolves it → retry.

283 backend tests pass; ruff clean.
2026-07-06 17:35:19 +00:00
Developer 7497469d5e Fix: user_id 'admin' rejected by Jellyfin API (400)
The Jellyfin service config's user_id field accepts either the internal
Jellyfin user ID (a long hash) or a username (e.g. 'admin'). The worker
passed the raw value directly to client.libraries(user_id), but Jellyfin's
API rejects usernames with a 400.

Now validates the configured user_id against the Jellyfin users API:
1. If it matches a user's Id (internal hash), use it directly.
2. If it matches a user's Name (username like 'admin'), resolve the Id.
3. If no match, fall back to the first user and log a warning.

283 backend tests pass; ruff clean.
2026-07-06 15:45:51 +00:00
Developer a3888026ab Fix: media worker crashes silently before writing error state
The worker called _resolve_jellyfin() and client.libraries() OUTSIDE the
try/except block. If Jellyfin was unreachable, the worker crashed with
an unhandled exception and NO error state was written to the DB — the
status stayed 'queued' forever with zero feedback.

Moved _resolve_jellyfin + client.libraries INSIDE the try block, and
created final_index + staging_index BEFORE the try so the except handler
can write the error state. Now any failure (connection refused, timeout,
missing config) is written to the DB as build_error and the frontend
shows it inline.

283 backend tests pass; ruff clean.
2026-07-06 15:36:07 +00:00
Developer 787f46700f Fix: media index build never starts (blocking Jellyfin dependency)
The build endpoint had Depends(get_jellyfin_client) and Depends(get_user_id)
which executed BEFORE the function body. If Jellyfin was unreachable, these
raised HTTPException(503), the function never ran, and the worker was never
started. The frontend mutation had no onError handler, so the failure was
completely silent — the button briefly showed 'Building...' then reverted
to 'Build index' with zero feedback.

Backend fix: removed the Jellyfin dependencies from post_build_index.
The worker subprocess resolves its own Jellyfin connection via
_resolve_jellyfin(service_id) — the endpoint just needs to start the
worker process. The libraries count starts at 0 and gets updated by
the worker once it connects.

Frontend fix: added onError to useBuildIndex that invalidates the status
query (so the UI reflects the non-building state). MediaTab now displays
the build error inline: 'Build failed: <message>' next to the button.

283 backend tests pass (updated build test for new no-dependency flow);
128 frontend tests pass; ruff/eslint clean.
2026-07-06 15:25:48 +00:00
Developer 57fe04ae7b Fix: edit button on referenced widgets opened list view instead of edit
The WidgetConfigDialog useEffect that auto-enters edit mode when
editWidgetId is set only searched owned widget instances (from
useWidgetInstances). Referenced widgets (from useWidgetReferences)
were never found, so startEdit never fired and the dialog fell through
to the list view.

Now the effect searches both owned instances and referenced widgets,
so clicking edit on any widget — owned or referenced — opens the edit
form directly.

128 tests pass; 0 lint errors; build clean.
2026-07-06 14:59:24 +00:00
Developer 5a43894875 Fix: sheet scroll, direct-edit close, mobile copy btn, service badge
Four fixes:

1. Mobile edit fullscreen scroll: the Sheet primitive's
   data-[side=bottom]:h-auto was overriding our h-[100dvh] on
   SheetForm, preventing scroll. Added data-[side=bottom]:h-[100dvh]
   to the SheetForm className to win the specificity battle.

2. Direct-edit close showed list view: when opened via editWidgetId
   (the hover edit button), saving or canceling called reset() which
   showed the widget list instead of closing the dialog. Now derives
   directEdit from editWidgetId — when true, reset() calls onClose()
   to close entirely.

3. Copy button missing on mobile: MobileWidgetSections didn't pass
   onCopy to its WidgetInstanceCard instances. Now accepts and wires
   onCopyWidget, so referenced widgets show the copy/detach button on
   mobile too.

4. Service enabled badge stale: ServiceConfigEditor showed
   instance.enabled (the initial prop) instead of the local enabled
   state. Now reads the local enabled variable so the badge updates
   when the user toggles the switch.

128 tests pass; 0 lint errors; build clean.
2026-07-06 14:51:14 +00:00
Developer 04871bd7d4 Fix: dialog scroll, isDirty false positive, mobile edit btn, widget copy
Five fixes:

1. Dialog mobile scroll: DialogContent now has max-h-[calc(100dvh-2rem)]
   overflow-y-auto so dialogs that don't fit on screen can scroll
   instead of clipping their footer (and Cancel button) off-screen.

2. isDirty false positive: WidgetConfigDialog's SheetForm used
   isDirty={draft !== null} which was true the moment you opened edit
   mode, even with no changes. Now stores a draftBaseline at startEdit
   time and compares JSON.stringify(draft) !== JSON.stringify(baseline).
   The discard-confirmation only appears when something actually changed.

3. Mobile edit button always visible: the widget card's edit button was
   opacity-0 group-hover:opacity-100 (hover-only). Changed to
   md:opacity-0 md:group-hover:opacity-100 — always visible below md,
   hover-reveal at md+.

4. Copy button for referenced widgets: WidgetInstanceCard gains an onCopy
   prop. On the Dashboard, referenced widgets get a Copy icon button that
   triggers detachRef (creates an independent clone). The edit button on
   referenced widgets edits the original (shared config).

5. ConfirmDialog Cancel: fixed by #1 (the Cancel button was off-screen
   on mobile dialogs that couldn't scroll).

128 tests pass; lint/build green.
2026-07-06 14:26:58 +00:00
Developer a63467e163 Fix: main dashboard edit showed all widgets (missing scope filter)
WidgetConfigDialog fetched useWidgetInstances(serviceId) with no scope
filter. On the main dashboard (serviceId undefined, dashboardScope='main'),
this returned ALL widget instances including service-scoped ones from
service overviews — so editing the main dashboard showed widgets that
were never added there.

Fix: pass scope='dashboard' when serviceId is empty and dashboardScope
is set. This fetches only dashboard-scoped widgets (service_id IS NULL).
The 'Add existing' picker (allWidgets) stays unscoped so users can still
reference service widgets onto the dashboard.

128 tests pass; lint/build green.
2026-07-06 14:09:22 +00:00
Developer d8c0a37210 Fix: settings master/detail, widget kind filter, reorder, media worker
Four fixes:

1. Settings > Services tab: master/detail layout. Replaced the vertical
   stack of ServiceConfigEditor cards with a SelectionRailCard (list on
   left) + SectionCard (details on right) — same pattern as Machines
   and SSH Keys tabs. Click a service in the rail to edit it.

2. Service Overview widget restriction. When adding widgets on a
   service's Overview, the dialog now only shows built-in widgets +
   widgets for THAT service type (not all services). Dashboard/named
   dashboards (no serviceId) still see all.

3. Reorder buttons fixed. The swap-sort_order approach was a no-op when
   both items had sort_order=0 (the default). Now moveInstance
   renumbers ALL items by their new index position (i * 10) after the
   swap, guaranteeing values change. References use updateRef, owned
   widgets use saveWidget, both sequential.

4. Media index worker resolution. The subprocess worker called
   get_jellyfin_client/get_user_id (FastAPI request dependencies) which
   don't work outside request context. Now accepts a service_id
   parameter (passed from post_build_index) and resolves the Jellyfin
   instance directly from the settings store via _resolve_jellyfin.
   Raises RuntimeError (not HTTPException) on failure.

283 backend tests pass; 128 frontend tests pass; ruff/eslint clean.
2026-07-06 14:03:05 +00:00
Developer eeb0cccbce Add hover-reveal edit button to widget cards + auto-open edit mode
Each widget card now shows a settings icon in the top-right corner on
hover (desktop) or always-visible (mobile via mobile-touch-target).
Clicking it opens the WidgetConfigDialog directly in edit mode for that
widget (via a new editWidgetId prop on WidgetConfigDialog that
auto-enters the draft-edit path via useEffect).

Wired across all three widget surfaces:
- Dashboard (both mobile section + desktop grid)
- Service OverviewTab
- NamedDashboardPage

128 tests pass; build/lint green.
2026-07-06 12:53:14 +00:00
Developer 691d78ff06 Dedup _resolve_service_record into shared service_resolution module
Extract the duplicated _resolve_service_record helper (identical in
routers/monitoring.py and routers/authentik_users.py) into a shared
services/service_resolution.py module. Both routers now import
resolve_service_record from the shared module.

The authentik router previously hardcoded service_type='authentik' in
its local copy; the shared helper takes service_type as a param (same
as monitoring's did).

Tests updated: test_api.py patches now target the correct module paths
(resolve_service_record on the monitoring module where it's imported,
build_service_record on the service_resolution module).

283 backend tests pass; ruff clean.
2026-07-06 12:25:17 +00:00
Developer 1e636fdbe2 Follow-ups: reference reorder, detach service_id, named-dashboard widgets
Three reusable-widget follow-up fixes:

1. Reference sort_order independently reorderable. Reordering a
   referenced widget now updates the widget_references.sort_order (per-
   dashboard), not the shared widget instance sort_order. New backend
   update_widget_reference method + PUT /api/widgets/references/{id}
   endpoint. Frontend moveInstance checks _ref_id to choose the right
   mutation (updateRef for references, saveWidget for owned).

2. Detach preserves service_id. detach_widget_reference now copies the
   original widget's service_id into the clone, so service-bound widgets
   (Grafana chart, Jellyfin activity) continue to render after detach.

3. Named dashboards support widget references. NamedDashboardPage
   fetches useWidgetReferences('named:<slug>') and renders them via
   WidgetInstanceCard alongside pinned links. 'Edit widgets' button
   opens WidgetConfigDialog with dashboardScope='named:<slug>'.

Also: removed useMemo on combinedWidgets in WidgetConfigDialog to fix
a react-hooks/preserve-manual-memoization lint error (the React Compiler
ESLint plugin couldn't verify the spread+sort memoization).

283 backend tests pass (+1 update_reference test); 128 frontend tests
pass; ruff clean; 0 lint errors.
2026-07-06 12:11:47 +00:00
Developer c36262d7b6 Reusable widgets: reference widgets across dashboards + detach to clone
Widgets configured on one dashboard (e.g., a Grafana service's Overview)
can now be live-referenced on other dashboards. Editing the widget config
updates it everywhere it's referenced. References can be detached into
independent clones.

Backend: new widget_references table (dashboard_scope, widget_id,
sort_order) with ON DELETE CASCADE. CRUD methods + 4 endpoints:
GET/POST /api/widgets/references, DELETE /api/widgets/references/{id},
POST /api/widgets/references/{id}/detach (clones the widget into a
standalone instance, then removes the reference).

Frontend: WidgetConfigDialog gains a dashboardScope prop. When set
(the main Dashboard passes 'main'), the dialog shows:
- Owned + referenced widgets in a combined list, with a link badge on
  references.
- 'Add existing widget' picker: searchable list of ALL widget instances
  not already on this dashboard. Click to create a reference.
- Detach button on references: clones the widget (service_id=NULL) and
  removes the reference.
- Delete on a reference removes the REFERENCE (not the original widget).

Dashboard renders referenced widgets alongside owned widgets.

Detaching a service-bound widget clones it with service_id=NULL — the
clone may need re-binding to a service to render correctly. Named
dashboards don't pass dashboardScope yet (pinned-links-only); when they
gain widget support, the backend already handles any scope string.

282 backend tests pass (+2 reference lifecycle); 127 frontend tests
pass; ruff/eslint/tsc/vite all green.
2026-07-06 11:34:48 +00:00
Developer 94bf830955 Fix invisible chart lines + extract Prometheus labels for multi-series
Two fixes for the Grafana chart widget:

1. Invisible lines: the CHART_COLORS used 'hsl(var(--chart-1))' but the
   CSS variable is named '--color-chart-1' and already contains a hex
   color (#4f8cff). The hsl() wrapper produced invalid CSS, making
   every stroke invisible. Fixed to var(--color-chart-1).

2. Multiple series collision: the backend labeled all Prometheus series
   with the value field name (often just 'Value'), so multiple time
   series collided on the same recharts dataKey and overwrote each
   other. Now extracts meaningful labels from the Grafana frame metadata:
   prefers displayName, then Prometheus metric labels (e.g.
   'instance=server1:9100 mode=iowait'), then falls back to the field
   name. Duplicate labels get a numeric suffix for uniqueness.

Multi-series queries now render correctly: each Prometheus time series
gets its own colored line with a unique label in the legend/tooltip.

280 backend tests pass (+1 labels test); 127 frontend tests pass; ruff/
eslint clean.
2026-07-06 10:59:16 +00:00