Below md, both the message-compose Dialog and the WidgetConfigDialog
render inside a SheetForm instead of a centered Dialog.
Message compose (UsersPage.impl.tsx): the form body (subject, formatting
toolbar, HTML textarea, preview, attachments) is extracted into a shared
composeBody const consumed by both SheetForm (mobile) and Dialog
(desktop). SheetForm wired with title, onSave=handleSend (which already
closes on success per R4.5), onCancel=closeCompose, isPending,
saveDisabled, saveLabel='Send message'.
WidgetConfigDialog: the draftBody const is shared between branches. The
two-mode flow (list vs draft) maps to dynamic SheetForm props -- list
mode ('Dashboard widgets' / Done / Cancel both close), draft mode
('Add/Edit widget' / Save widget / Cancel=reset back to list). The
inline Back/Save buttons are hidden on mobile (!isMobile) since the
SheetForm footer provides them.
Desktop (md+) is token-identical for both components -- the
isComposeMobile (900px) fullscreen styling on compose is preserved for
the 768-900px band. The large diff (~860 lines) is dominated by
extraction/re-indentation of shared form bodies into consts; the
behavioral delta is ~80 lines.
Tests: 3 new (compose mobile send/subject, WidgetConfigDialog desktop +
mobile titles/Done). 116 tests pass; lint/build green.
Refs openspec/changes/mobile-responsive-parity/ (spec R4, tasks slice 8).
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).
Below md, the Users directory and the three Backups tables render as
MobileCardRow cards:
- UsersPage: display name primary; username/activity/email fields. Each
card carries a selection checkbox (44px via mobile-touch-target) in the
actions slot with stopPropagation so toggling selection does not open
the drawer; card-body tap still opens the drawer.
- BackupAlertsTable: alert message primary; severity/type/created fields;
Acknowledge action preserved in actions slot.
- BackupJobsTable: job name primary; source/schedule/last-status fields
(joins latestRuns into a JobCardRow).
- BackupRunsTable: run job_id primary; status/duration/size/started fields;
status-filter Select renders above both layouts (preserved on mobile).
Desktop (md+) is byte-for-byte identical for all four components -- the
UsersPage diff is dominated by re-indenting the existing Table into the
isMobile ternary else branch.
Fix from Slice 5 review: MobileCardRow now renders the clickable card as
<div role=button tabIndex=0> with Enter/Space keyboard handling instead
of <button>, so nesting a Radix Checkbox (which renders a <button>) in
the actions slot produces valid HTML. The desktop-parity argument for
<button>-in-<button> did not hold (desktop rows are <tr>, not buttons).
Cross-cutting: useIsMobile hardened with typeof window.matchMedia guard
(safe in real browsers; only changes jsdom crash -> false). The file-local
900px compose hook was renamed useComposeViewport to avoid collision with
the shared 768px useIsMobile.
Tests: BackupJobsTable test file added (was untested), UsersPage mobile
selection round-trip + stopPropagation, mobile card render across all
four components. 105 tests pass; lint/build green.
Refs openspec/changes/mobile-responsive-parity/ (spec R3, tasks slice 5).
Foundation for the mobile-responsive-parity change. Adds:
- useIsMobile() hook: single source of truth for the md:768px cut (SSR-safe)
- MobileCardRow<T>: stacked card list for wide tables below md, with getRowId
stable keys, primary field as title, optional onRowClick + actions slot
- SheetForm: full-height form host (h-[100dvh], flex column, sticky header +
footer via flex not position:sticky) for mobile edit flows
- HoverEditButton: mobile prop (default 'always') -- always visible below md,
hover-revealed at md+; desktop aesthetic preserved
- .mobile-touch-target CSS utility: 44x44 min hit area below md (WCAG 2.5.5)
- App.tsx refactored to use useIsMobile(); shell behavior unchanged
Tests cover primary/field rendering, onRowClick, actions slot, empty rows,
no-primary, stable keys (no duplicate-key warning), and all SheetForm
interactions. 86 tests pass; lint/build green.
MobileCardRow key strategy: uses getRowId when provided (falls back to index);
per design §trade-offs, fields are declared per-table to prioritize by mobile
importance rather than auto-derived from column defs.
Refs openspec/changes/mobile-responsive-parity/ (design §Shared primitives,
spec R1/R5/R6, tasks slice 1).
- 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.
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
- 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.
Backend:
- FastAPI app with 17 REST endpoints covering dashboard, monitoring,
media index, file browser, and jobs
- Reuses existing clients/domain/services unchanged
- pydantic-settings config, dependency injection, CORS setup
- Auto-generated OpenAPI docs at /docs
Frontend:
- Vite + React + TypeScript SPA
- @tanstack/react-query for data fetching with polling
- ag-grid-react for media table and file browser
- recharts for monitoring charts
- Tailwind CSS styling
- 4 pages: Dashboard, Monitoring, Media, File Browser
- Typed API client matching all backend endpoints
Also:
- docs/MIGRATION_PLAN.md with full architecture plan
- Updated .gitignore for both subprojects
- Streamlit app preserved for now (can coexist)