Settings.tsx: ServiceConfigEditor derived editable state (name, config,
secrets) from the instance prop via useState, but the parent rendered it
without a key. Switching services in the rail reused the same component, so
name/config stayed pinned to the previously selected service while
instance.id/service_type (read live from props) pointed at the new one —
saving then wrote the stale values onto the wrong row (e.g. saving qBittorrent
renamed it "Jellyfin" with Jellyfin's URL). Add key={selectedService.id} so
the editor remounts and resets on switch.
ServicePage: add a Settings shortcut in the header that deep-links to
/settings?tab=services&service=<id>. Settings now reads tab + service query
params (useSearchParams) to open the Services tab with that service
pre-selected, via a new initialServiceId prop on ServicesAdminCard.
Tests: new Settings.services.test.tsx regression test (fails without the key,
passes with it); wrap existing Settings tests in MemoryRouter since Settings
now uses useSearchParams. 166/166 frontend tests pass; typecheck + ESLint clean.
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.
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.
Four coupled changes to the services-as-hub IA:
1. Nav entries use service TYPE names (Jellyfin, SSH Tasks, Alertmanager,
Grafana, Prometheus, Backups, Authentik) instead of conceptual names
(Media, Files, Actions, Alerts, Users). ssh_tasks collapses to one
entry ('SSH Tasks') instead of two. The content tabs inside each
service page surface the concepts (Files, Actions).
2. Service page gains a two-level tab structure when multiple enabled
instances of the same type exist: instance tabs on top ([Main Jellyfin]
[Backup Jellyfin]), content tabs below ([Overview] [Media] [Requests]
[Widgets]). Clicking an instance tab navigates to the sibling's route.
Single instance: no instance tabs. Replaces the dropdown switcher.
3. Config tab (connection fields, secrets, enable/disable, delete) moves
from the service page to Settings > Services tab. The service page
becomes a PURE operational view (Overview + content tabs + Widgets) --
no save/delete/config state. Settings gains a 4th tab 'Services' with
ServiceConfigEditor per instance (schema-driven config fields, secrets
with leave-blank-to-keep semantics, ConfirmDialog on delete).
4. Overview tab is now a configurable widget grid per service instance.
Each instance manages its own set of widgets on its Overview. Backend
widget list endpoints gain ?service_id= and ?scope= (dashboard|service)
filter params; the main Dashboard uses scope=dashboard to exclude
service-scoped widgets. The OverviewTab reuses WidgetInstanceCard +
WidgetConfigDialog. Empty state CTA for instances with no widgets.
All service-tab stubs are replaced; stubs.tsx deleted.
272 backend tests pass (+1 widget filter); 121 frontend tests pass (+3
instance-tabs + OverviewTab); lint/build green both sides.
Applies .mobile-touch-target to 32 default-size <Button> elements (32px
tall, below the mobile minimum) across 9 files for strict WCAG 2.5.5
compliance: Save, Cancel, Delete, Validate SSH, Run job, Build index,
Update connection, Add service, etc. Plus the shared DialogFooter Cancel
+ Confirm buttons (used by every ConfirmDialog).
The class applies min-height/min-width: 44px only below md
(max-width: 767px); no-op at md+, so desktop sizing is unchanged.
Completes the touch-target audit started in Slice 9 (which covered icon
buttons, size=sm buttons, checkboxes, switches). 122 tests pass; lint/
build green. No new tests (@media queries aren't honored by jsdom).
Refs openspec/changes/mobile-responsive-parity/verify-report.md residual
risk #2.
SheetForm gains an isDirty prop. When true, any close attempt (Cancel
button, header X, Radix overlay click, Escape) opens a 'Discard changes?'
ConfirmDialog instead of discarding unsaved edits. Radix dismiss callbacks
(onEscapeKeyDown, onPointerDownOutside) are intercepted when dirty so the
guard applies uniformly.
All four form consumers now compute and pass isDirty:
- ServicePage: name/enabled/config differ from the persisted instance.
- Settings machine editor: field-by-field draft vs editingMachine
(create mode is always dirty; secret write-only fields excluded).
- Message compose: subject non-empty, body differs from default, or
attachments present.
- WidgetConfigDialog: draft !== null (only draft mode is guarded; list
mode has nothing to discard).
Tests: 3 new SheetForm dirty-guard cases (prompt on cancel, abort discard,
clean close when not dirty) + one focused dirty-guard test per consumer.
122 tests pass; lint/build green.
Refs openspec/changes/mobile-responsive-parity/verify-report.md residual
risk #1.
Below md, the machine editor Dialog renders as a SheetForm (triggered by
the same Edit/Add buttons via machineDialogOpen state). The shared
MachineEditor body (fields + SSH validate button) renders inside the
sheet; the ConfirmDialog is a sibling outside. Desktop Dialog is
byte-for-byte identical.
No navigation needed on close -- the Settings page content (tabbed cards,
machine list) is always visible behind the sheet, so there is no stranding
risk (unlike ServicePage where the sheet was the whole page).
Added saveDisabled prop to SheetForm (additive, default false) so the
machine editor can gate Save on required fields (name + host for SSH
mode), matching the desktop DialogFooter confirmDisabled semantics.
Scope note: SSHKeyManager is an inline two-panel layout (SelectionRailCard
+ SectionCard), not a dialog, and already stacks responsively via
grid-cols-1 md:grid-cols-[...]. Wrapping it in SheetForm would break its
always-visible selection rail. Left as-is.
Tests: 3 new mobile cases (SheetForm render, save payload, cancel closes)
+ desktop unchanged. 113 tests pass; lint/build green.
Refs openspec/changes/mobile-responsive-parity/ (spec R4, tasks slice 7).
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 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.
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.
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