diff --git a/apps/web/src/components/settings/SystemInfoPanel.tsx b/apps/web/src/components/settings/SystemInfoPanel.tsx index 5275872b..fd29f9a6 100644 --- a/apps/web/src/components/settings/SystemInfoPanel.tsx +++ b/apps/web/src/components/settings/SystemInfoPanel.tsx @@ -1,9 +1,12 @@ import { useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import { CircleCheck, Copy, ExternalLink } from "lucide-react"; import { Trans, useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { useDeployedUpdateNotice } from "@/hooks/useDeployedUpdateNotice"; import { detectWebClientKind } from "@/lib/client-environment"; +import { api, getConfiguredDesktopApiBaseUrl } from "@/lib/api"; +import { resolveDeploymentPlatform } from "@/lib/instance-runtime"; import { cn } from "@/lib/utils"; import { getReleaseTagForVersion, resolveLocalizedReleaseChanges } from "@/lib/version-check"; import { copyTextToClipboard } from "./settings-utils"; @@ -36,7 +39,11 @@ const getDeploymentDescription = (t: (key: string) => string) => { return `${trigger} · ${method}`; }; -export const getWebSystemInfoItems = (t: (key: string) => string, language: string): SystemInfoItem[] => { +export const getWebSystemInfoItems = ( + t: (key: string) => string, + language: string, + instanceRuntime?: string | null, +): SystemInfoItem[] => { const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || t("systemInfo.unknown"); const userAgent = navigator.userAgent; const clientKind = detectWebClientKind({ @@ -57,6 +64,10 @@ export const getWebSystemInfoItems = (t: (key: string) => string, language: stri }, { label: t("systemInfo.build"), value: __EDGEEVER_BUILD_LABEL__ }, { label: t("systemInfo.client"), value: t(`systemInfo.clients.${clientKind}`) }, + { + label: t("systemInfo.deploymentPlatform"), + value: t(`systemInfo.deploymentPlatforms.${resolveDeploymentPlatform(instanceRuntime)}`), + }, { label: t("systemInfo.deployment"), value: getDeploymentDescription(t) }, ...(clientKind === "desktopApp" ? [] @@ -71,7 +82,18 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => { const { t, i18n } = useTranslation(); const [copied, setCopied] = useState(false); const { release } = useDeployedUpdateNotice(); - const infoItems = useMemo(() => getWebSystemInfoItems(t, i18n.language), [i18n.language, t]); + const instanceUrl = window.edgeeverDesktop?.isAvailable === true ? getConfiguredDesktopApiBaseUrl() : window.location.origin; + const healthQuery = useQuery({ + queryKey: ["instance-health", instanceUrl], + queryFn: () => api.getInstanceHealth(), + enabled: active && Boolean(instanceUrl), + staleTime: 5 * 60 * 1000, + retry: 1, + }); + const infoItems = useMemo( + () => getWebSystemInfoItems(t, i18n.language, healthQuery.data?.runtime), + [healthQuery.data?.runtime, i18n.language, t], + ); const releaseTag = release ? getReleaseTagForVersion(release.version) : null; const releaseHighlights = resolveLocalizedReleaseChanges( release?.changes ?? {}, diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index 472a0587..00e9d8b7 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -51,6 +51,13 @@ export type InstanceRelease = { changes: Record; }; +export type InstanceHealth = { + ok: true; + name: string; + runtime: string; + authMode: string; +}; + type ListMemosResponse = { memos: MemoSummary[]; totalCount: number; @@ -457,6 +464,8 @@ const requestArrayBuffer = async (path: string) => { }; export const api = { + getInstanceHealth: () => request("/api/health"), + getInstanceRelease: () => request("/api/release"), getSession: () => request("/api/v1/auth/session"), diff --git a/apps/web/src/lib/instance-runtime.test.mjs b/apps/web/src/lib/instance-runtime.test.mjs new file mode 100644 index 00000000..260186c0 --- /dev/null +++ b/apps/web/src/lib/instance-runtime.test.mjs @@ -0,0 +1,17 @@ +import { describe, expect, test } from "bun:test"; +import { resolveDeploymentPlatform } from "./instance-runtime.ts"; + +describe("resolveDeploymentPlatform", () => { + test("maps the Cloudflare Workers runtime", () => { + expect(resolveDeploymentPlatform("cloudflare-workers")).toBe("cloudflare"); + }); + + test("maps the self-hosted Bun runtime to Docker", () => { + expect(resolveDeploymentPlatform("self-hosted-bun")).toBe("docker"); + }); + + test("keeps missing and future runtimes explicit", () => { + expect(resolveDeploymentPlatform(undefined)).toBe("unknown"); + expect(resolveDeploymentPlatform("future-runtime")).toBe("unknown"); + }); +}); diff --git a/apps/web/src/lib/instance-runtime.ts b/apps/web/src/lib/instance-runtime.ts new file mode 100644 index 00000000..d51910ca --- /dev/null +++ b/apps/web/src/lib/instance-runtime.ts @@ -0,0 +1,12 @@ +export type DeploymentPlatform = "cloudflare" | "docker" | "unknown"; + +export const resolveDeploymentPlatform = (runtime: string | null | undefined): DeploymentPlatform => { + switch (runtime) { + case "cloudflare-workers": + return "cloudflare"; + case "self-hosted-bun": + return "docker"; + default: + return "unknown"; + } +}; diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index bf8e2424..e42fee01 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -559,6 +559,12 @@ export const enUS = { pwa: "PWA", web: "Web", }, + deploymentPlatform: "Deployment platform", + deploymentPlatforms: { + cloudflare: "Cloudflare", + docker: "Docker", + unknown: "Unknown", + }, deployment: "Deployment", deploymentTriggers: { github_release: "GitHub Release trigger", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 1dedf67a..c40bac33 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -559,6 +559,12 @@ export const zhCN = { pwa: "PWA", web: "网页", }, + deploymentPlatform: "部署平台", + deploymentPlatforms: { + cloudflare: "Cloudflare", + docker: "Docker", + unknown: "未知", + }, deployment: "自动部署方式", deploymentTriggers: { github_release: "正式 Release 触发",