diff --git a/apps/web/src/app/[locale]/awooop/approvals/[run_id]/page.tsx b/apps/web/src/app/[locale]/awooop/approvals/[run_id]/page.tsx index 8a9f275b1..c1a0d4aa7 100644 --- a/apps/web/src/app/[locale]/awooop/approvals/[run_id]/page.tsx +++ b/apps/web/src/app/[locale]/awooop/approvals/[run_id]/page.tsx @@ -28,6 +28,7 @@ import { type AwoooPStatusChain, } from "@/components/awooop/status-chain"; import { cn } from "@/lib/utils"; +import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; import { publicAgentText, publicBoundaryText, @@ -85,7 +86,7 @@ interface RunDetailResponse { awooop_status_chain?: AwoooPStatusChain | null; } -const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; +const API_BASE = getRuntimeApiBaseUrl(); const ADR100_GATE5_PROJECTION_TRIGGER = "adr100_runtime_replay_gate5"; const ownerResponseValidationDecisionRefs: OwnerResponseValidationDecisionRef[] = [ diff --git a/apps/web/src/app/[locale]/awooop/approvals/page.tsx b/apps/web/src/app/[locale]/awooop/approvals/page.tsx index 2089705c7..cbcd121ee 100644 --- a/apps/web/src/app/[locale]/awooop/approvals/page.tsx +++ b/apps/web/src/app/[locale]/awooop/approvals/page.tsx @@ -24,6 +24,7 @@ import { GitBranch, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; import { Link } from "@/i18n/routing"; import { AwoooPStatusChainPanel, @@ -110,7 +111,7 @@ interface LegacyApproval { // 常數 // ============================================================================= -const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; +const API_BASE = getRuntimeApiBaseUrl(); const AUTO_REFRESH_INTERVAL = 10_000; // 10 秒 // ============================================================================= diff --git a/apps/web/src/app/[locale]/awooop/contracts/page.tsx b/apps/web/src/app/[locale]/awooop/contracts/page.tsx index ae6bfcc7e..755f6c8b6 100644 --- a/apps/web/src/app/[locale]/awooop/contracts/page.tsx +++ b/apps/web/src/app/[locale]/awooop/contracts/page.tsx @@ -21,6 +21,7 @@ import { } from "lucide-react"; import { cn } from "@/lib/utils"; import { publicBoundaryText, publicContractText, publicProjectText } from "@/lib/public-security-redaction"; +import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; // ============================================================================= // Types @@ -51,7 +52,7 @@ interface ContractsResponse { // 常數 // ============================================================================= -const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; +const API_BASE = getRuntimeApiBaseUrl(); const STATUS_CONFIG: Record< ContractStatus, diff --git a/apps/web/src/app/[locale]/awooop/page.tsx b/apps/web/src/app/[locale]/awooop/page.tsx index c55dd11eb..e1ecd9e64 100644 --- a/apps/web/src/app/[locale]/awooop/page.tsx +++ b/apps/web/src/app/[locale]/awooop/page.tsx @@ -25,6 +25,7 @@ import { import { Link } from "@/i18n/routing"; import { cn } from "@/lib/utils"; import { publicBoundaryText, publicContractText } from "@/lib/public-security-redaction"; +import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; type Tenant = { project_id: string; @@ -172,7 +173,7 @@ type SourceFlowResponse = { summary: SourceFlowSummary; }; -const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; +const API_BASE = getRuntimeApiBaseUrl(); const emptySnapshot: Snapshot = { tenants: 0, diff --git a/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx b/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx index 1b74210a8..cdba4bb88 100644 --- a/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx @@ -36,6 +36,7 @@ import { type AwoooPStatusChain, } from "@/components/awooop/status-chain"; import { cn } from "@/lib/utils"; +import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; import { publicAgentText, publicBoundaryText, @@ -257,7 +258,7 @@ interface IncidentTimelineResponse { reconciliation?: Record; } -const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; +const API_BASE = getRuntimeApiBaseUrl(); const AUTO_REFRESH_INTERVAL = 30_000; const ownerResponseValidationDetailRefs: OwnerResponseValidationDetailRef[] = [ diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx index 41f9ea4fa..252646cc4 100644 --- a/apps/web/src/app/[locale]/awooop/runs/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx @@ -8,6 +8,7 @@ import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useTranslations } from "next-intl"; import { Link } from "@/i18n/routing"; +import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; import { AwoooPStatusChainPanel, type AwoooPStatusChain, @@ -696,7 +697,7 @@ type OwnerResponseValidationRunRef = { // 常數 // ============================================================================= -const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; +const API_BASE = getRuntimeApiBaseUrl(); const PER_PAGE = 50; const AUTO_REFRESH_INTERVAL = 30_000; // 30 秒 const INCIDENT_ID_FILTER_RE = /^INC-\d{8}-[A-Z0-9]{4,}$/; diff --git a/apps/web/src/app/[locale]/awooop/tenants/page.tsx b/apps/web/src/app/[locale]/awooop/tenants/page.tsx index adb6b5500..8c81e3fb7 100644 --- a/apps/web/src/app/[locale]/awooop/tenants/page.tsx +++ b/apps/web/src/app/[locale]/awooop/tenants/page.tsx @@ -26,6 +26,7 @@ import { ShieldCheck, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; // ============================================================================= // Types @@ -157,7 +158,7 @@ type AssetTone = "steady" | "warn" | "locked"; // 常數 // ============================================================================= -const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; +const API_BASE = getRuntimeApiBaseUrl(); const githubTenantReadinessMetrics: GitHubTenantReadinessMetric[] = [ { key: "candidateRepos", value: "10", icon: GitBranch, tone: "neutral" }, diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index c93f408e5..bf94e52d3 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -30,6 +30,7 @@ import { import { Link } from "@/i18n/routing"; import { IncidentEvidenceHeader } from "@/components/awooop/incident-evidence-header"; +import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; import { AwoooPStatusChainPanel, type AwoooPStatusChain, @@ -1440,7 +1441,7 @@ type RepairCandidateDraftFocus = { lane: string; }; -const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; +const API_BASE = getRuntimeApiBaseUrl(); const REPAIR_CANDIDATE_DRAFT_PREFIX = "repair-candidate-draft"; const REPAIR_CANDIDATE_DRAFT_REQUIRED_FIELDS = [ diff --git a/apps/web/src/components/layout/app-layout.tsx b/apps/web/src/components/layout/app-layout.tsx index 102db462a..60e188143 100644 --- a/apps/web/src/components/layout/app-layout.tsx +++ b/apps/web/src/components/layout/app-layout.tsx @@ -20,6 +20,7 @@ import { Sidebar } from './sidebar' import { Header } from './header' import { DotMatrixBg } from '@/components/ui/dot-matrix-bg' import { Z_INDEX } from '@/lib/constants/z-index' +import { getRuntimeApiBaseUrl } from '@/lib/runtime-api-base' import { cn } from '@/lib/utils' import { useDashboardStore } from '@/stores/dashboard.store' @@ -92,7 +93,7 @@ export function AppLayout({ // Phase 19 修復: 全局啟動 SSE 連接 (所有頁面共享) useEffect(() => { - const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || '' + const apiBaseUrl = getRuntimeApiBaseUrl() if (apiBaseUrl) { console.log('[AppLayout] 全局 SSE 連接啟動...', apiBaseUrl) connect(apiBaseUrl) diff --git a/apps/web/src/hooks/useSSE.ts b/apps/web/src/hooks/useSSE.ts index a97ae03d4..ad26bfca2 100644 --- a/apps/web/src/hooks/useSSE.ts +++ b/apps/web/src/hooks/useSSE.ts @@ -17,6 +17,7 @@ */ import { useEffect, useRef, useCallback } from 'react' +import { getRuntimeApiBaseUrl } from '@/lib/runtime-api-base' import { useDashboardStore } from '@/stores/dashboard.store' // ============================================================================= @@ -54,7 +55,7 @@ const getApiBaseUrl = (): string => { if (typeof window === 'undefined') return '' // 專案鐵律: 禁止任何 Fallback IP - const url = process.env.NEXT_PUBLIC_API_URL + const url = getRuntimeApiBaseUrl() if (!url) { console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL. SSE will not connect.') return '' diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 6fd963519..0a47f57ea 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -6,10 +6,11 @@ */ import { CURRENT_USER } from '@/lib/constants/user' +import { getRuntimeApiV1BaseUrl } from '@/lib/runtime-api-base' // 絕對純化: 環境變數缺失時直接拋出致命錯誤,嚴禁任何 Fallback const getApiBaseUrl = (): string => { - const url = process.env.NEXT_PUBLIC_API_URL + const url = getRuntimeApiV1BaseUrl() if (!url) { const fatalMsg = '[AWOOOI FATAL] Missing NEXT_PUBLIC_API_URL configuration.' console.error(fatalMsg) @@ -18,7 +19,7 @@ const getApiBaseUrl = (): string => { } throw new Error(fatalMsg) } - return url.endsWith('/api/v1') ? url : `${url}/api/v1` + return url } const API_BASE_URL = getApiBaseUrl() diff --git a/apps/web/src/lib/runtime-api-base.ts b/apps/web/src/lib/runtime-api-base.ts new file mode 100644 index 000000000..405a7b87d --- /dev/null +++ b/apps/web/src/lib/runtime-api-base.ts @@ -0,0 +1,15 @@ +const PUBLIC_AWOOOI_HOSTS = new Set(["aiops.wooo.work", "awoooi.wooo.work"]); + +export function getRuntimeApiBaseUrl(): string { + if (typeof window !== "undefined" && PUBLIC_AWOOOI_HOSTS.has(window.location.hostname)) { + return window.location.origin; + } + + return (process.env.NEXT_PUBLIC_API_URL ?? "").replace(/\/+$/, ""); +} + +export function getRuntimeApiV1BaseUrl(): string { + const baseUrl = getRuntimeApiBaseUrl(); + if (!baseUrl) return ""; + return baseUrl.endsWith("/api/v1") ? baseUrl : `${baseUrl}/api/v1`; +} diff --git a/apps/web/src/stores/dashboard.store.ts b/apps/web/src/stores/dashboard.store.ts index af361a11b..1d3a928be 100644 --- a/apps/web/src/stores/dashboard.store.ts +++ b/apps/web/src/stores/dashboard.store.ts @@ -15,6 +15,7 @@ import { create } from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' +import { getRuntimeApiBaseUrl } from '@/lib/runtime-api-base' // ============================================================================= // Types @@ -203,7 +204,7 @@ export const useDashboardStore = create()( // 專案鐵律: 禁止任何 Fallback IP const resolvedApiBaseUrl = apiBaseUrl || - (typeof window !== 'undefined' ? process.env.NEXT_PUBLIC_API_URL : '') + (typeof window !== 'undefined' ? getRuntimeApiBaseUrl() : '') if (!resolvedApiBaseUrl) { console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL. SSE will not connect.') @@ -353,7 +354,7 @@ export const useDashboardStore = create()( // 專案鐵律: 禁止任何 Fallback IP const resolvedApiBaseUrl = apiBaseUrl || - (typeof window !== 'undefined' ? process.env.NEXT_PUBLIC_API_URL : '') + (typeof window !== 'undefined' ? getRuntimeApiBaseUrl() : '') if (!resolvedApiBaseUrl) { console.error('[AWOOOI ERROR] Missing API URL for snapshot fetch')