diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md
index 1841864e9..6f29419e2 100644
--- a/docs/release-control/v6/internal/subsystems/deployment-installability.md
+++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md
@@ -187,6 +187,12 @@ server-side update execution surfaces.
open the dev browser shell, while local browser/proof URLs normalize wildcard
binds back to `127.0.0.1` and advertise the detected LAN browser entrypoint
separately.
+ The hot-dev supervisor must also recover its managed PID file from a live
+ `hot-dev-bg.sh supervise` process before treating the runtime as unmanaged.
+ Backend health monitoring must distinguish HTTP startup grace from a missing
+ backend process: a missing `./pulse` process may be tolerated only for the
+ short configured missing-process grace, after which the managed runtime must
+ restart it instead of waiting for the full HTTP warmup window.
6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, the canonical version file, prerelease feedback intake prompts, artifact publication lineage enforcement, release note or changelog packet composition, or stable-promotion rehearsal summaries through `.github/workflows/create-release.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `scripts/trigger-release.sh`, and `scripts/trigger-release-dry-run.sh`
That release-promotion boundary also owns prerelease note packet lineage:
shipped RC notes must remain historically accurate, the top-level
diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md
index 7b1b7e7bb..8727078b8 100644
--- a/docs/release-control/v6/internal/subsystems/monitoring.md
+++ b/docs/release-control/v6/internal/subsystems/monitoring.md
@@ -136,6 +136,12 @@ truth for live infrastructure data.
same native-query rule: ZFS snapshots prefer `zfs.resource.snapshot.query`
with older `pool.snapshot.query`/REST compatibility fallback, and
replication tasks prefer `replication.query`.
+ TrueNAS system services are also native appliance inventory: `service.query`
+ is the preferred source for service name, boot enablement, runtime state,
+ and process IDs. Pulse must publish that data through `TrueNASData.Services`
+ on the owning top-level TrueNAS system resource instead of inventing a
+ generic service resource type or rendering services as Docker/container
+ rows.
## Forbidden Paths
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index 729e8d754..02eef2a8e 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -145,13 +145,14 @@ read-only evidence: it must not become a storage/recovery-owned command path,
secret source, restore entitlement, or frontend-only fixture separate from the
canonical `/api/discovery` payload.
Storage and recovery may also consume unified-resource TrueNAS app and VM
-metadata, plus TrueNAS network-share metadata, as read-only workload and
-storage-access context when explaining appliance-owned storage or protected
-items. `TrueNASData.App`, `TrueNASData.VM`, and `TrueNASData.Share` remain
+metadata, TrueNAS native service metadata, plus TrueNAS network-share metadata,
+as read-only workload, system, and storage-access context when explaining
+appliance-owned storage or protected items. `TrueNASData.App`,
+`TrueNASData.VM`, `TrueNASData.Share`, and `TrueNASData.Services` remain
unified-resource/platform truth sourced from the TrueNAS API; storage/recovery
-must not reinterpret app containers, VM device inventory, share paths, volumes,
-or update posture as storage ownership, restore entitlement, or a separate
-Docker-only or TrueNAS-local inventory path.
+must not reinterpret app containers, VM device inventory, service PIDs, share
+paths, volumes, or update posture as storage ownership, restore entitlement, or
+a separate Docker-only or TrueNAS-local inventory path.
1. Add or change recovery-point persistence, rollups, or series derivation through `internal/recovery/`
2. Add or change recovery page UX through `frontend-modern/src/components/Recovery/` and keep canonical route/query/filter state ownership in `frontend-modern/src/features/recovery/useRecoverySurfaceState.ts`
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index 54ed42034..51141607a 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -209,6 +209,11 @@ reintroduce false Swarm capability surfaces.
resources, redact filesystem paths through policy metadata, and derive
storage-parent relationships without being flattened into generic Docker or
storage rows.
+ TrueNAS service inventory remains a system-owned native facet rather than a
+ new generic resource type. `service.query` output must project into
+ `TrueNASData.Services` on the canonical top-level `agent` resource,
+ preserving service name, boot enablement, runtime state, and process IDs
+ through clone, merge, transport, and frontend decode paths.
2. Add typed accessors and views in `internal/unifiedresources/views.go`
3. Add source ingestion/adaptation in the adapter layer only
Frontend resource platform contracts in
@@ -1225,9 +1230,11 @@ guest-config routing or inventing a TrueNAS-local config type.
That projection contract is the product-definition floor for TrueNAS support:
Pulse supports TrueNAS only when it lands on the shared `agent`, `vm`,
`app-container`, `storage`, `physical-disk`, and recovery-linked resource
-shapes. The unified agent may augment a TrueNAS system later, but baseline
-support does not depend on it, and product surfaces must not reopen a parallel
-`truenas-*` resource model just to add another capability.
+shapes, with native system-owned service inventory carried as
+`TrueNASData.Services` on the canonical `agent`. The unified agent may augment
+a TrueNAS system later, but baseline support does not depend on it, and product
+surfaces must not reopen a parallel `truenas-*` resource model just to add
+another capability.
The canonical resource timeline now also owns durable incident-response facts
that materially changed resource investigation state. `ResourceChange` kinds
such as `alert_fired`, `alert_acknowledged`, `alert_unacknowledged`,
@@ -1554,9 +1561,11 @@ The same change emitter now also classifies canonical incident changes as
anomalies stay attached to the canonical incident surface instead of being
inferred later from metric noise or alert-adjacent heuristics.
That store also now migrates legacy `resource_changes` tables that still carry
-the pre-v6 `timestamp` column by backfilling canonical `observed_at` values,
-adding the newer `occurred_at` field, and preserving the legacy timestamp on
-write when the target database still requires it.
+the pre-v6 `timestamp` column by adding canonical columns without rewriting
+large historical tables during startup. Timeline reads must resolve legacy
+`timestamp` and nullable `observed_at` values through read-time fallback
+expressions, while writes preserve the legacy timestamp on target databases
+that still require it.
`internal/api/resources.go` now exposes that same history through dedicated
`/api/resources/{id}/timeline` reads, while the bundled `/api/resources/{id}/facets`
surface keeps the facet summary and recent-change history available without
diff --git a/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx b/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx
index 9c5aac0f0..906daefc5 100644
--- a/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx
+++ b/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx
@@ -13,6 +13,7 @@ import { TrueNASAppsTable } from './TrueNASAppsTable';
import { TrueNASAlertsTable } from './TrueNASAlertsTable';
import { TrueNASNetworkSharesTable } from './TrueNASNetworkSharesTable';
import { TrueNASProtectionTable } from './TrueNASProtectionTable';
+import { TrueNASServicesTable } from './TrueNASServicesTable';
import { TrueNASStorageTopologyTable } from './TrueNASStorageTopologyTable';
import { TrueNASSystemsTable } from './TrueNASSystemsTable';
import { TrueNASVirtualMachinesTable } from './TrueNASVirtualMachinesTable';
@@ -90,6 +91,9 @@ export function TrueNASPageSurface() {
+
+
+
@@ -137,6 +141,17 @@ function TrueNASApps(props: TrueNASOverviewProps) {
);
}
+function TrueNASServices(props: TrueNASOverviewProps) {
+ return (
+
+ );
+}
+
function TrueNASVMs(props: TrueNASOverviewProps) {
return (
[] = [
+ { value: 'all', label: 'All' },
+ { value: 'running', label: 'Running', tone: 'success' },
+ { value: 'attention', label: 'Attention', tone: 'warning' },
+ { value: 'stopped', label: 'Stopped', tone: 'danger' },
+ { value: 'disabled', label: 'Disabled' },
+];
+
+const SERVICE_NAME_LABELS: Record = {
+ ftp: 'FTP',
+ nfs: 'NFS',
+ rsync: 'Rsync',
+ s3: 'S3',
+ smartd: 'SMART',
+ smb: 'SMB',
+ snmp: 'SNMP',
+ ssh: 'SSH',
+ ups: 'UPS',
+};
+
+const titleCase = (value: string | undefined): string => {
+ const normalized = asTrimmedString(value);
+ if (!normalized) return 'Unknown';
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase();
+};
+
+const formatServiceName = (value: string | undefined): string => {
+ const normalized = asTrimmedString(value);
+ if (!normalized) return 'Unknown';
+ return SERVICE_NAME_LABELS[normalized.toLowerCase()] ?? normalized;
+};
+
+const formatPIDs = (pids: number[] | undefined): { label: string; title: string } => {
+ const values = (pids ?? []).filter((pid) => Number.isFinite(pid) && pid > 0);
+ if (values.length === 0) return { label: '-', title: '' };
+ const visible = values.slice(0, 3).map(String);
+ const suffix = values.length > visible.length ? ` +${values.length - visible.length}` : '';
+ return { label: `${visible.join(', ')}${suffix}`, title: values.join(', ') };
+};
+
+const serviceStatusVariant = (
+ status: Exclude,
+): 'success' | 'warning' | 'danger' | 'muted' => {
+ if (status === 'running') return 'success';
+ if (status === 'attention') return 'warning';
+ if (status === 'stopped') return 'danger';
+ return 'muted';
+};
+
+export const TrueNASServicesTable: Component<{
+ services: TrueNASServiceRow[];
+ emptyIcon: JSX.Element;
+ emptyTitle: string;
+ emptyDescription: string;
+ showToolbar?: boolean;
+}> = (props) => {
+ const tableState = createPlatformTableFilterState({
+ resources: () => props.services,
+ initialStatus: 'all' as TrueNASServiceStatusFilter,
+ filter: filterTrueNASServices,
+ });
+
+ return (
+ 0}
+ fallback={
+
+ }
+ >
+