refactor: move system health procedure from system to admin router

- Rename `system.health` → `admin.systemHealth` and gate it behind the admin middleware
- Remove `tmdbConfigured` from `system.status` response; update description to reflect its purpose
- Update all call sites on web and native to use `orpc.admin.systemHealth`
- Rename and relocate the docs page from `system/system.health.mdx` to `admin/admin.systemHealth.mdx`
This commit is contained in:
2026-03-17 11:14:03 -04:00
parent 710c43a01f
commit ca44354240
11 changed files with 135 additions and 98 deletions
@@ -86,7 +86,7 @@ export default function SettingsScreen() {
hasPassword && !(authConfig.data?.passwordLoginDisabled ?? true); hasPassword && !(authConfig.data?.passwordLoginDisabled ?? true);
const systemHealth = useQuery({ const systemHealth = useQuery({
...orpc.system.health.queryOptions(), ...orpc.admin.systemHealth.queryOptions(),
enabled: isAdmin, enabled: isAdmin,
}); });
+9
View File
@@ -13,6 +13,7 @@ import {
purgeMetadataCache as purgeMetadataFn, purgeMetadataCache as purgeMetadataFn,
} from "@sofa/core/cache"; } from "@sofa/core/cache";
import { getSetting, setSetting } from "@sofa/core/settings"; import { getSetting, setSetting } from "@sofa/core/settings";
import { getSystemHealth } from "@sofa/core/system-health";
import { isTelemetryEnabled } from "@sofa/core/telemetry"; import { isTelemetryEnabled } from "@sofa/core/telemetry";
import { import {
getCachedUpdateCheck, getCachedUpdateCheck,
@@ -173,3 +174,11 @@ export const purgeMetadataCache = os.admin.purgeMetadataCache
export const purgeImageCache = os.admin.purgeImageCache export const purgeImageCache = os.admin.purgeImageCache
.use(admin) .use(admin)
.handler(async () => purgeImagesFn()); .handler(async () => purgeImagesFn());
// ─── System Health ───────────────────────────────────────────────
export const systemHealth = os.admin.systemHealth
.use(admin)
.handler(async () => {
return await getSystemHealth();
});
+1 -8
View File
@@ -1,15 +1,8 @@
import { getSystemHealth } from "@sofa/core/system-health";
import { isTmdbConfigured } from "@sofa/tmdb/config";
import { os } from "../context"; import { os } from "../context";
import { admin, authed } from "../middleware"; import { authed } from "../middleware";
export const status = os.system.status.use(authed).handler(() => { export const status = os.system.status.use(authed).handler(() => {
return { return {
tmdbConfigured: isTmdbConfigured(),
publicApiUrl: process.env.PUBLIC_API_URL || "https://public-api.sofa.watch", publicApiUrl: process.env.PUBLIC_API_URL || "https://public-api.sofa.watch",
}; };
}); });
export const health = os.system.health.use(admin).handler(async () => {
return await getSystemHealth();
});
+1 -1
View File
@@ -55,7 +55,6 @@ export const implementedRouter = {
publicInfo: system.publicInfo, publicInfo: system.publicInfo,
authConfig: system.authConfig, authConfig: system.authConfig,
status: status.status, status: status.status,
health: status.health,
}, },
integrations: { integrations: {
list: integrations.list, list: integrations.list,
@@ -81,6 +80,7 @@ export const implementedRouter = {
triggerJob: admin.triggerJob, triggerJob: admin.triggerJob,
purgeMetadataCache: admin.purgeMetadataCache, purgeMetadataCache: admin.purgeMetadataCache,
purgeImageCache: admin.purgeImageCache, purgeImageCache: admin.purgeImageCache,
systemHealth: admin.systemHealth,
}, },
account: { account: {
updateName: account.updateName, updateName: account.updateName,
@@ -29,7 +29,7 @@ export function CacheSection() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const invalidateHealth = () => const invalidateHealth = () =>
queryClient.invalidateQueries({ queryKey: orpc.system.health.key() }); queryClient.invalidateQueries({ queryKey: orpc.admin.systemHealth.key() });
const purgeMetadata = useMutation( const purgeMetadata = useMutation(
orpc.admin.purgeMetadataCache.mutationOptions({ orpc.admin.purgeMetadataCache.mutationOptions({
@@ -130,11 +130,11 @@ function LiveTimeAgo({
export function SystemHealthCards() { export function SystemHealthCards() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data, isPending, isFetching } = useQuery( const { data, isPending, isFetching } = useQuery(
orpc.system.health.queryOptions(), orpc.admin.systemHealth.queryOptions(),
); );
const isRefreshing = isFetching; const isRefreshing = isFetching;
const refresh = () => const refresh = () =>
queryClient.invalidateQueries({ queryKey: orpc.system.health.key() }); queryClient.invalidateQueries({ queryKey: orpc.admin.systemHealth.key() });
if (isPending || !data) return <SkeletonCards />; if (isPending || !data) return <SkeletonCards />;
@@ -9,11 +9,11 @@ _openapi:
contents: contents:
- content: >- - content: >-
Comprehensive health check covering database, TMDB connectivity, cron Comprehensive health check covering database, TMDB connectivity, cron
jobs, image cache, backups, and environment. Admin only. jobs, image cache, backups, and environment.
--- ---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment. Admin only. Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/system/health","method":"get"}]} /> <APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/system-health","method":"get"}]} />
@@ -1,5 +1,5 @@
--- ---
title: Get system status title: Get internal system config
full: true full: true
_openapi: _openapi:
method: GET method: GET
@@ -8,12 +8,12 @@ _openapi:
headings: [] headings: []
contents: contents:
- content: >- - content: >-
Quick check of whether TMDB is configured. Does not require Returns internal configuration such as the public API URL. Requires
authentication. authentication.
--- ---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Quick check of whether TMDB is configured. Does not require authentication. Returns internal configuration such as the public API URL. Requires authentication.
<APIPage document={"./public/openapi.json"} operations={[{"path":"/system/status","method":"get"}]} /> <APIPage document={"./public/openapi.json"} operations={[{"path":"/system/status","method":"get"}]} />
+97 -61
View File
@@ -4287,60 +4287,28 @@
"/system/status": { "/system/status": {
"get": { "get": {
"operationId": "system.status", "operationId": "system.status",
"summary": "Get system status", "summary": "Get internal system config",
"description": "Quick check of whether TMDB is configured. Does not require authentication.", "description": "Returns internal configuration such as the public API URL. Requires authentication.",
"tags": [ "tags": [
"System" "System"
], ],
"responses": { "responses": {
"200": { "200": {
"description": "TMDB configuration status", "description": "Internal system configuration",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"tmdbConfigured": {
"type": "boolean",
"description": "Whether a TMDB API token is configured"
},
"publicApiUrl": { "publicApiUrl": {
"type": "string", "type": "string",
"description": "Base URL of the centralized public API" "description": "Base URL of the centralized public API"
} }
}, },
"required": [ "required": [
"tmdbConfigured",
"publicApiUrl" "publicApiUrl"
], ],
"description": "Quick TMDB configuration check" "description": "Internal system configuration for authenticated clients"
}
}
}
}
},
"security": [
{
"session": []
}
]
}
},
"/system/health": {
"get": {
"operationId": "system.health",
"summary": "Get system health report",
"description": "Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment. Admin only.",
"tags": [
"System"
],
"responses": {
"200": {
"description": "Full system health report",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SystemHealth"
} }
} }
} }
@@ -5620,6 +5588,33 @@
] ]
} }
}, },
"/admin/system-health": {
"get": {
"operationId": "admin.systemHealth",
"summary": "Get system health report",
"description": "Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment.",
"tags": [
"Admin"
],
"responses": {
"200": {
"description": "Full system health report",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SystemHealth"
}
}
}
}
},
"security": [
{
"session": []
}
]
}
},
"/account/name": { "/account/name": {
"put": { "put": {
"operationId": "account.updateName", "operationId": "account.updateName",
@@ -5831,10 +5826,12 @@
}, },
"watchedAt": { "watchedAt": {
"type": "string", "type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp" "description": "ISO 8601 timestamp"
}, },
"watchedOn": { "watchedOn": {
"type": "string", "type": "string",
"format": "date",
"description": "YYYY-MM-DD date-only" "description": "YYYY-MM-DD date-only"
} }
}, },
@@ -5865,16 +5862,22 @@
"type": "number" "type": "number"
}, },
"seasonNumber": { "seasonNumber": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"episodeNumber": { "episodeNumber": {
"type": "number" "type": "integer",
"minimum": 1,
"maximum": 9007199254740991
}, },
"watchedAt": { "watchedAt": {
"type": "string" "type": "string",
"format": "date-time"
}, },
"watchedOn": { "watchedOn": {
"type": "string" "type": "string",
"format": "date"
} }
}, },
"required": [ "required": [
@@ -5951,10 +5954,12 @@
"description": "Sofa 1-5 star rating" "description": "Sofa 1-5 star rating"
}, },
"ratedAt": { "ratedAt": {
"type": "string" "type": "string",
"format": "date-time"
}, },
"ratedOn": { "ratedOn": {
"type": "string" "type": "string",
"format": "date"
} }
}, },
"required": [ "required": [
@@ -6087,10 +6092,12 @@
}, },
"watchedAt": { "watchedAt": {
"type": "string", "type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp" "description": "ISO 8601 timestamp"
}, },
"watchedOn": { "watchedOn": {
"type": "string", "type": "string",
"format": "date",
"description": "YYYY-MM-DD date-only" "description": "YYYY-MM-DD date-only"
} }
}, },
@@ -6121,16 +6128,22 @@
"type": "number" "type": "number"
}, },
"seasonNumber": { "seasonNumber": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"episodeNumber": { "episodeNumber": {
"type": "number" "type": "integer",
"minimum": 1,
"maximum": 9007199254740991
}, },
"watchedAt": { "watchedAt": {
"type": "string" "type": "string",
"format": "date-time"
}, },
"watchedOn": { "watchedOn": {
"type": "string" "type": "string",
"format": "date"
} }
}, },
"required": [ "required": [
@@ -6207,10 +6220,12 @@
"description": "Sofa 1-5 star rating" "description": "Sofa 1-5 star rating"
}, },
"ratedAt": { "ratedAt": {
"type": "string" "type": "string",
"format": "date-time"
}, },
"ratedOn": { "ratedOn": {
"type": "string" "type": "string",
"format": "date"
} }
}, },
"required": [ "required": [
@@ -6276,10 +6291,12 @@
}, },
"watchedAt": { "watchedAt": {
"type": "string", "type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp" "description": "ISO 8601 timestamp"
}, },
"watchedOn": { "watchedOn": {
"type": "string", "type": "string",
"format": "date",
"description": "YYYY-MM-DD date-only" "description": "YYYY-MM-DD date-only"
} }
}, },
@@ -6310,16 +6327,22 @@
"type": "number" "type": "number"
}, },
"seasonNumber": { "seasonNumber": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"episodeNumber": { "episodeNumber": {
"type": "number" "type": "integer",
"minimum": 1,
"maximum": 9007199254740991
}, },
"watchedAt": { "watchedAt": {
"type": "string" "type": "string",
"format": "date-time"
}, },
"watchedOn": { "watchedOn": {
"type": "string" "type": "string",
"format": "date"
} }
}, },
"required": [ "required": [
@@ -6396,10 +6419,12 @@
"description": "Sofa 1-5 star rating" "description": "Sofa 1-5 star rating"
}, },
"ratedAt": { "ratedAt": {
"type": "string" "type": "string",
"format": "date-time"
}, },
"ratedOn": { "ratedOn": {
"type": "string" "type": "string",
"format": "date"
} }
}, },
"required": [ "required": [
@@ -6532,10 +6557,12 @@
}, },
"watchedAt": { "watchedAt": {
"type": "string", "type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp" "description": "ISO 8601 timestamp"
}, },
"watchedOn": { "watchedOn": {
"type": "string", "type": "string",
"format": "date",
"description": "YYYY-MM-DD date-only" "description": "YYYY-MM-DD date-only"
} }
}, },
@@ -6566,16 +6593,22 @@
"type": "number" "type": "number"
}, },
"seasonNumber": { "seasonNumber": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"episodeNumber": { "episodeNumber": {
"type": "number" "type": "integer",
"minimum": 1,
"maximum": 9007199254740991
}, },
"watchedAt": { "watchedAt": {
"type": "string" "type": "string",
"format": "date-time"
}, },
"watchedOn": { "watchedOn": {
"type": "string" "type": "string",
"format": "date"
} }
}, },
"required": [ "required": [
@@ -6652,10 +6685,12 @@
"description": "Sofa 1-5 star rating" "description": "Sofa 1-5 star rating"
}, },
"ratedAt": { "ratedAt": {
"type": "string" "type": "string",
"format": "date-time"
}, },
"ratedOn": { "ratedOn": {
"type": "string" "type": "string",
"format": "date"
} }
}, },
"required": [ "required": [
@@ -7145,7 +7180,8 @@
"type": { "type": {
"enum": [ "enum": [
"progress", "progress",
"complete" "complete",
"timeout"
] ]
}, },
"job": { "job": {
+14 -14
View File
@@ -406,23 +406,12 @@ export const contract = {
method: "GET", method: "GET",
path: "/system/status", path: "/system/status",
tags: ["System"], tags: ["System"],
summary: "Get system status", summary: "Get internal system config",
description: description:
"Quick check of whether TMDB is configured. Does not require authentication.", "Returns internal configuration such as the public API URL. Requires authentication.",
successDescription: "TMDB configuration status", successDescription: "Internal system configuration",
}) })
.output(SystemStatusOutput), .output(SystemStatusOutput),
health: oc
.route({
method: "GET",
path: "/system/health",
tags: ["System"],
summary: "Get system health report",
description:
"Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment. Admin only.",
successDescription: "Full system health report",
})
.output(SystemHealthOutput),
}, },
integrations: { integrations: {
list: oc list: oc
@@ -653,6 +642,17 @@ export const contract = {
}) })
.input(z.void()) .input(z.void())
.output(PurgeImageCacheOutput), .output(PurgeImageCacheOutput),
systemHealth: oc
.route({
method: "GET",
path: "/admin/system-health",
tags: ["Admin"],
summary: "Get system health report",
description:
"Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment.",
successDescription: "Full system health report",
})
.output(SystemHealthOutput),
}, },
account: { account: {
updateName: oc updateName: oc
+3 -4
View File
@@ -884,12 +884,11 @@ export const SystemHealthSchema = z
export const SystemStatusOutput = z export const SystemStatusOutput = z
.object({ .object({
tmdbConfigured: z
.boolean()
.describe("Whether a TMDB API token is configured"),
publicApiUrl: z.string().describe("Base URL of the centralized public API"), publicApiUrl: z.string().describe("Base URL of the centralized public API"),
}) })
.meta({ description: "Quick TMDB configuration check" }); .meta({
description: "Internal system configuration for authenticated clients",
});
export const SystemHealthOutput = SystemHealthSchema; export const SystemHealthOutput = SystemHealthSchema;