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.
This commit is contained in:
Developer
2026-07-06 14:03:05 +00:00
parent eeb0cccbce
commit d8c0a37210
4 changed files with 231 additions and 155 deletions
+23 -22
View File
@@ -232,7 +232,6 @@ export function WidgetConfigDialog({
startEdit(target);
}
}
}, [open, editWidgetId, instances]);
function startAddBuiltIn(kind: string) {
@@ -307,27 +306,26 @@ export function WidgetConfigDialog({
async function moveInstance(index: number, direction: -1 | 1) {
const targetIndex = index + direction;
if (targetIndex < 0 || targetIndex >= combinedWidgets.length) return;
const a = combinedWidgets[index];
const b = combinedWidgets[targetIndex];
const aRefId = (a as { _ref_id?: string })._ref_id;
const bRefId = (b as { _ref_id?: string })._ref_id;
// References use their own sort_order on the widget_references row;
// owned widgets use the widget instance's sort_order.
if (aRefId) {
await updateRef.mutateAsync({
referenceId: aRefId,
sortOrder: b.sort_order,
});
} else {
await saveWidget.mutateAsync({ ...a, sort_order: b.sort_order });
}
if (bRefId) {
await updateRef.mutateAsync({
referenceId: bRefId,
sortOrder: a.sort_order,
});
} else {
await saveWidget.mutateAsync({ ...b, sort_order: a.sort_order });
// Swap the two items in a copy, then renumber ALL items by their new
// index position (index * 10). This guarantees the sort_order values
// change even when both items previously shared the same value (e.g. 0).
const reordered = [...combinedWidgets];
const tmp = reordered[index];
reordered[index] = reordered[targetIndex];
reordered[targetIndex] = tmp;
// Sequential (not Promise.all) to avoid cache-invalidation race.
for (let i = 0; i < reordered.length; i++) {
const item = reordered[i];
const newSortOrder = i * 10;
const refId = (item as { _ref_id?: string })._ref_id;
if (refId) {
await updateRef.mutateAsync({
referenceId: refId,
sortOrder: newSortOrder,
});
} else {
await saveWidget.mutateAsync({ ...item, sort_order: newSortOrder });
}
}
}
@@ -655,6 +653,9 @@ export function WidgetConfigDialog({
))}
{services
.filter((s) => s.enabled)
// When scoped to a service Overview, only show widgets for THAT
// service instance's type (not all services' widgets).
.filter((s) => !serviceId || s.id === serviceId)
.flatMap((s) =>
(SERVICE_REGISTRY[s.service_type]?.widgets ?? []).map((w) => (
<Button