fix: centralize app API URL fallbacks
Some checks failed
app-ci-deploy / test-build-deploy (push) Has been cancelled

This commit is contained in:
kcar 2026-05-28 19:26:00 +01:00
parent 65ce1bede8
commit fedbd903ff
27 changed files with 110 additions and 38 deletions

View File

@ -37,4 +37,4 @@ VITE_MICROSOFT_CLIENT_SECRET_ID=dummy_secret_id
VITE_MICROSOFT_CLIENT_SECRET=dummy_secret VITE_MICROSOFT_CLIENT_SECRET=dummy_secret
VITE_MICROSOFT_TENANT_ID=common VITE_MICROSOFT_TENANT_ID=common
VITE_SEARCH_URL=http://localhost:8888 VITE_SEARCH_URL=https://search.kevlarai.com

View File

@ -1,11 +1,12 @@
import axios from 'axios'; import axios from 'axios';
import { API_BASE as CONFIG_API_BASE } from './config/apiConfig';
import { logger } from './debugConfig'; import { logger } from './debugConfig';
// Use development backend URL if no custom URL is provided // Use development backend URL if no custom URL is provided
const baseURL = import.meta.env.VITE_API_BASE || 'http://localhost:8080'; const baseURL = CONFIG_API_BASE;
if (!import.meta.env.VITE_API_BASE) { if (!import.meta.env.VITE_API_BASE && !import.meta.env.VITE_API_URL) {
logger.warn('axios', '⚠️ VITE_API_BASE not set, defaulting to http://localhost:8080'); logger.warn('axios', '⚠️ VITE_API_BASE not set, using relative /api fallback');
} }
const instance = axios.create({ const instance = axios.create({

44
src/config/apiConfig.ts Normal file
View File

@ -0,0 +1,44 @@
/**
* Centralized API configuration for Classroom Copilot.
*
* Resolves the API base URL from environment variables with proper fallbacks.
* - In dev: VITE_API_BASE should always be set (e.g., http://192.168.0.64:18000)
* - In production: VITE_API_BASE should be set to the production API URL
* - Fallback: "/api" (relative path, works when API is served from same origin)
*
* IMPORTANT: Never hardcode localhost or 127.0.0.1 as fallbacks - they break
* remote dev access and do not work in containerized environments.
*/
/**
* Primary API base URL. Use this for all API calls.
* Resolved from VITE_API_BASE (preferred) or VITE_API_URL (legacy alias).
*/
export const API_BASE: string =
import.meta.env.VITE_API_BASE ||
import.meta.env.VITE_API_URL ||
"/api";
/**
* TLSync worker URL for tldraw multiplayer.
* Resolved from VITE_TLSYNC_URL (preferred) or derived from VITE_FRONTEND_SITE_URL.
*/
export const TLSYNC_URL: string =
import.meta.env.VITE_TLSYNC_URL ||
(import.meta.env.VITE_FRONTEND_SITE_URL?.startsWith("http")
? `${import.meta.env.VITE_FRONTEND_SITE_URL}/tldraw`
: `https://${import.meta.env.VITE_FRONTEND_SITE_URL}/tldraw`);
/**
* WhisperLive WebSocket URL for live transcription.
* Resolved from VITE_WHISPERLIVE_URL. Must be set in env.
*/
export const WHISPERLIVE_URL: string | undefined =
import.meta.env.VITE_WHISPERLIVE_URL;
/**
* Search proxy URL (searxng).
* Resolved from VITE_SEARCH_URL.
*/
export const SEARCH_URL: string =
import.meta.env.VITE_SEARCH_URL || "/searxng-api/";

View File

@ -8,7 +8,7 @@ import { Refresh, School, People, EventNote, HourglassEmpty } from '@mui/icons-m
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
interface SchoolEntry { interface SchoolEntry {
id: string; id: string;

View File

@ -123,7 +123,7 @@ const SimpleUploadTest: React.FC = () => {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const dirInputRef = useRef<HTMLInputElement>(null); const dirInputRef = useRef<HTMLInputElement>(null);
const API_BASE = import.meta.env.VITE_API_BASE || 'http://127.0.0.1:8080'; const API_BASE = import.meta.env.VITE_API_BASE || '/api';
const apiFetch = useCallback(async (url: string, init?: { method?: string; body?: FormData | string; headers?: Record<string, string> }) => { const apiFetch = useCallback(async (url: string, init?: { method?: string; body?: FormData | string; headers?: Record<string, string> }) => {
const session = await supabase.auth.getSession(); const session = await supabase.auth.getSession();

View File

@ -10,7 +10,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────

View File

@ -12,7 +12,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────

View File

@ -9,7 +9,7 @@ import {
import { Add, Search, LibraryBooks, Edit, Delete, FilterList } from '@mui/icons-material'; import { Add, Search, LibraryBooks, Edit, Delete, FilterList } from '@mui/icons-material';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────

View File

@ -13,7 +13,7 @@ import {
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
const DAY_TYPE_OPTIONS = ['Academic', 'Holiday', 'Staff', 'OffTimetable']; const DAY_TYPE_OPTIONS = ['Academic', 'Holiday', 'Staff', 'OffTimetable'];
const DAY_TYPE_COLORS: Record<string, 'default' | 'primary' | 'success' | 'warning' | 'error'> = { const DAY_TYPE_COLORS: Record<string, 'default' | 'primary' | 'success' | 'warning' | 'error'> = {

View File

@ -10,7 +10,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────

View File

@ -5,7 +5,7 @@ import {
import { ChevronLeft, ChevronRight, Today } from '@mui/icons-material'; import { ChevronLeft, ChevronRight, Today } from '@mui/icons-material';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────

View File

@ -9,7 +9,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
interface Student { interface Student {
profile_id: string; profile_id: string;

View File

@ -12,7 +12,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────

View File

@ -50,8 +50,8 @@ export const CCBundleViewer: React.FC<{
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const API_BASE = useMemo(() => import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'), []); const API_BASE = useMemo(() => import.meta.env.VITE_API_BASE || '/api', []);
const API_BASE_FALLBACK = 'http://127.0.0.1:8080'; const API_BASE_FALLBACK = '/api';
const proxyUrl = useCallback(async (bucket: string, relPath: string) => { const proxyUrl = useCallback(async (bucket: string, relPath: string) => {
const token = accessToken || ''; const token = accessToken || '';

View File

@ -182,7 +182,7 @@ export const CCDoclingViewer: React.FC<{
setError(null); setError(null);
try { try {
// Try page-images manifest first // Try page-images manifest first
const API_BASE = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); const API_BASE = import.meta.env.VITE_API_BASE || '/api';
try { try {
const mRes = await fetch(`${API_BASE}/database/files/${encodeURIComponent(fileId)}/page-images/manifest`, { const mRes = await fetch(`${API_BASE}/database/files/${encodeURIComponent(fileId)}/page-images/manifest`, {
headers: { 'Authorization': `Bearer ${accessToken || ''}` } headers: { 'Authorization': `Bearer ${accessToken || ''}` }
@ -199,7 +199,7 @@ export const CCDoclingViewer: React.FC<{
} }
// Legacy: Load artefacts for file to find docling JSON artefacts // Legacy: Load artefacts for file to find docling JSON artefacts
const artefactsRes = await fetch(`${import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api')}/database/files/${encodeURIComponent(fileId)}/artefacts`, { const artefactsRes = await fetch(`${import.meta.env.VITE_API_BASE || '/api'}/database/files/${encodeURIComponent(fileId)}/artefacts`, {
headers: { 'Authorization': `Bearer ${accessToken || ''}` } headers: { 'Authorization': `Bearer ${accessToken || ''}` }
}); });
if (!artefactsRes.ok) throw new Error(await artefactsRes.text()); if (!artefactsRes.ok) throw new Error(await artefactsRes.text());
@ -239,7 +239,7 @@ export const CCDoclingViewer: React.FC<{
run(); run();
}, [fileId]); /* eslint-disable-line react-hooks/exhaustive-deps */ }, [fileId]); /* eslint-disable-line react-hooks/exhaustive-deps */
const API_BASE = useMemo(() => import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'), []); const API_BASE = useMemo(() => import.meta.env.VITE_API_BASE || '/api', []);
const pageProxyUrl = useMemo(() => { const pageProxyUrl = useMemo(() => {
if (!manifest) return undefined; if (!manifest) return undefined;

View File

@ -148,7 +148,7 @@ export const CCDocumentIntelligence: React.FC = () => {
useEffect(() => { useEffect(() => {
const loadBundles = async () => { const loadBundles = async () => {
if (!validFileId) return; if (!validFileId) return;
const API_BASE = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); const API_BASE = import.meta.env.VITE_API_BASE || '/api';
const token = accessToken || ''; const token = accessToken || '';
const res = await fetch(`${API_BASE}/database/files/${encodeURIComponent(validFileId)}/artefacts`, { headers: { Authorization: `Bearer ${token}` } }); const res = await fetch(`${API_BASE}/database/files/${encodeURIComponent(validFileId)}/artefacts`, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) return; if (!res.ok) return;
@ -223,7 +223,7 @@ export const CCDocumentIntelligence: React.FC = () => {
const run = async () => { const run = async () => {
if (!validFileId) return; if (!validFileId) return;
setOutlineOptions([]); setOutlineOptions([]);
const API_BASE = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); const API_BASE = import.meta.env.VITE_API_BASE || '/api';
try { try {
const artsRes = await fetch(`${API_BASE}/database/files/${encodeURIComponent(validFileId)}/artefacts`, { const artsRes = await fetch(`${API_BASE}/database/files/${encodeURIComponent(validFileId)}/artefacts`, {
headers: { 'Authorization': `Bearer ${accessToken || ''}` } headers: { 'Authorization': `Bearer ${accessToken || ''}` }
@ -486,7 +486,7 @@ export const CCDocumentIntelligence: React.FC = () => {
<Button variant="contained" disabled={busy || !validFileId} onClick={async () => { <Button variant="contained" disabled={busy || !validFileId} onClick={async () => {
try { try {
setBusy(true); setBusy(true);
const API_BASE = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); const API_BASE = import.meta.env.VITE_API_BASE || '/api';
const token = accessToken || ''; const token = accessToken || '';
const body: CanonicalDoclingRequest = { const body: CanonicalDoclingRequest = {
use_split_map: selectedSectionId === 'full' ? autoSplit : false, use_split_map: selectedSectionId === 'full' ? autoSplit : false,

View File

@ -84,7 +84,7 @@ export const CCEnhancedFilePanel: React.FC<CCEnhancedFilePanelProps> = ({
const thumbnailsRef = useRef<HTMLDivElement>(null); const thumbnailsRef = useRef<HTMLDivElement>(null);
const API_BASE = useMemo(() => const API_BASE = useMemo(() =>
import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'), import.meta.env.VITE_API_BASE || '/api',
[] []
); );

View File

@ -65,7 +65,7 @@ export const CCFileDetailPanel: React.FC<{
const [showAdmin, setShowAdmin] = useState(false); const [showAdmin, setShowAdmin] = useState(false);
const [adminData, setAdminData] = useState<FileTasksResponse | null>(null); const [adminData, setAdminData] = useState<FileTasksResponse | null>(null);
const API_BASE = useMemo(() => import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'), []); const API_BASE = useMemo(() => import.meta.env.VITE_API_BASE || '/api', []);
useEffect(() => { useEffect(() => {
const run = async () => { const run = async () => {

View File

@ -26,14 +26,13 @@ import { allBindingUtils } from '../../utils/tldraw/bindings';
import { multiplayerEmbeds } from '../../utils/tldraw/embeds'; import { multiplayerEmbeds } from '../../utils/tldraw/embeds';
// Layout // Layout
import { HEADER_HEIGHT } from '../../pages/Layout'; import { HEADER_HEIGHT } from '../../pages/Layout';
import { TLSYNC_URL } from '../../config/apiConfig';
// Styles // Styles
import '../../utils/tldraw/tldraw.css'; import '../../utils/tldraw/tldraw.css';
// App debug // App debug
import { logger } from '../../debugConfig'; import { logger } from '../../debugConfig';
const SYNC_WORKER_URL = import.meta.env.VITE_FRONTEND_SITE_URL.startsWith('http') const SYNC_WORKER_URL = TLSYNC_URL;
? `${import.meta.env.VITE_FRONTEND_SITE_URL}/tldraw`
: `https://${import.meta.env.VITE_FRONTEND_SITE_URL}/tldraw`;
const apiBase = (import.meta.env.VITE_API_BASE as string) || ''; const apiBase = (import.meta.env.VITE_API_BASE as string) || '';

View File

@ -15,7 +15,7 @@ import {
} from '../types/timetable.types'; } from '../types/timetable.types';
// API base URL // API base URL
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
// ============================================================================ // ============================================================================
// Helper: Get auth headers // Helper: Get auth headers

View File

@ -577,7 +577,7 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => {
set({ isExporting: true, exportError: null }); set({ isExporting: true, exportError: null });
try { try {
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai'; const apiBaseUrl = import.meta.env.VITE_API_BASE || '/api';
const response = await fetch(`${apiBaseUrl}/transcribe/sessions/${sessionId}/export`, { const response = await fetch(`${apiBaseUrl}/transcribe/sessions/${sessionId}/export`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -627,7 +627,7 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => {
try { try {
const { _accessToken: token } = get(); const { _accessToken: token } = get();
if (!token) return; if (!token) return;
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai'; const apiBaseUrl = import.meta.env.VITE_API_BASE || '/api';
const response = await fetch(`${apiBaseUrl}/transcribe/keywords`, { const response = await fetch(`${apiBaseUrl}/transcribe/keywords`, {
headers: { 'Authorization': `Bearer ${token}` }, headers: { 'Authorization': `Bearer ${token}` },
}); });
@ -643,7 +643,7 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => {
try { try {
const { _accessToken: token, _userId: userId } = get(); const { _accessToken: token, _userId: userId } = get();
if (!token || !userId) return; if (!token || !userId) return;
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai'; const apiBaseUrl = import.meta.env.VITE_API_BASE || '/api';
const response = await fetch(`${apiBaseUrl}/transcribe/keywords`, { const response = await fetch(`${apiBaseUrl}/transcribe/keywords`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -669,7 +669,7 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => {
try { try {
const { _accessToken: token } = get(); const { _accessToken: token } = get();
if (!token) return; if (!token) return;
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai'; const apiBaseUrl = import.meta.env.VITE_API_BASE || '/api';
await fetch(`${apiBaseUrl}/transcribe/keywords/${watchId}`, { await fetch(`${apiBaseUrl}/transcribe/keywords/${watchId}`, {
method: 'DELETE', method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }, headers: { 'Authorization': `Bearer ${token}` },
@ -709,7 +709,7 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => {
if (activeSession) { if (activeSession) {
try { try {
const { _accessToken: kwToken } = get(); const { _accessToken: kwToken } = get();
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai'; const apiBaseUrl = import.meta.env.VITE_API_BASE || '/api';
await fetch(`${apiBaseUrl}/transcribe/keywords/events`, { await fetch(`${apiBaseUrl}/transcribe/keywords/events`, {
method: 'POST', method: 'POST',
headers: { headers: {

View File

@ -5,6 +5,7 @@
import { Editor, TLStoreEventInfo } from '@tldraw/tldraw'; import { Editor, TLStoreEventInfo } from '@tldraw/tldraw';
import { useTranscriptionStore } from '../../stores/transcriptionStore'; import { useTranscriptionStore } from '../../stores/transcriptionStore';
import { API_BASE } from '../../../../config/apiConfig';
export class CanvasEventLogger { export class CanvasEventLogger {
private editor: Editor | null = null; private editor: Editor | null = null;
@ -221,7 +222,7 @@ export class CanvasEventLogger {
this.buffer = []; this.buffer = [];
try { try {
const response = await fetch('http://192.168.0.64:8000/transcribe/canvas-events', { const response = await fetch(`${API_BASE}/transcribe/canvas-events`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -26,7 +26,7 @@ export const CCCabinetsPanel: React.FC = () => {
return createTheme({ palette: { mode, divider: 'var(--color-divider)' } }); return createTheme({ palette: { mode, divider: 'var(--color-divider)' } });
}, [tldrawPreferences?.colorScheme, prefersDarkMode]); }, [tldrawPreferences?.colorScheme, prefersDarkMode]);
const API_BASE: string = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); const API_BASE: string = import.meta.env.VITE_API_BASE || '/api';
type RequestInitLite = { method?: string; body?: string | FormData | Blob | null; headers?: Record<string, string> } | undefined; type RequestInitLite = { method?: string; body?: string | FormData | Blob | null; headers?: Record<string, string> } | undefined;
const apiFetch = useCallback(async (url: string, init?: RequestInitLite) => { const apiFetch = useCallback(async (url: string, init?: RequestInitLite) => {

View File

@ -141,7 +141,7 @@ export const CCFilesPanel: React.FC = () => {
type RequestInitLike = { method?: string; body?: FormData | string | Blob | null; headers?: Record<string, string> } | undefined; type RequestInitLike = { method?: string; body?: FormData | string | Blob | null; headers?: Record<string, string> } | undefined;
type HeadersInitLike = Record<string, string>; type HeadersInitLike = Record<string, string>;
const API_BASE: string = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); const API_BASE: string = import.meta.env.VITE_API_BASE || '/api';
const apiFetch = useCallback(async (url: string, init?: RequestInitLike) => { const apiFetch = useCallback(async (url: string, init?: RequestInitLike) => {
const headers: HeadersInitLike = { const headers: HeadersInitLike = {

View File

@ -106,7 +106,7 @@ export const CCFilesPanelEnhanced: React.FC = () => {
type RequestInitLike = { method?: string; body?: FormData | string | Blob | null; headers?: Record<string, string> } | undefined; type RequestInitLike = { method?: string; body?: FormData | string | Blob | null; headers?: Record<string, string> } | undefined;
type HeadersInitLike = Record<string, string>; type HeadersInitLike = Record<string, string>;
const API_BASE: string = (import.meta as unknown as { env?: { VITE_API_BASE?: string } })?.env?.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); const API_BASE: string = import.meta.env.VITE_API_BASE || '/api';
const apiFetch = async (url: string, init?: RequestInitLike) => { const apiFetch = async (url: string, init?: RequestInitLike) => {
const headers: HeadersInitLike = { const headers: HeadersInitLike = {

View File

@ -119,7 +119,7 @@ export const CCTranscriptionPanel: React.FC = () => {
useEffect(() => { useEffect(() => {
const detectTimetable = async () => { const detectTimetable = async () => {
try { try {
const apiBase = import.meta.env.VITE_API_URL || 'https://api.classroomcopilot.ai'; const apiBase = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api';
const response = await fetch(`${apiBase}/database/timetables/current-period`); const response = await fetch(`${apiBase}/database/timetables/current-period`);
const data = await response.json(); const data = await response.json();
if (data.period_id) { if (data.period_id) {

27
src/vite-env.d.ts vendored
View File

@ -41,6 +41,33 @@ interface ImportMetaEnv {
// Search URL // Search URL
readonly VITE_SEARCH_URL: string readonly VITE_SEARCH_URL: string
// TLSync
readonly VITE_TLSYNC_URL: string
readonly VITE_TLSYNC_SECRET: string
// WhisperLive
readonly VITE_WHISPERLIVE_URL: string
// LLM / Ollama
readonly VITE_LLM_PROVIDER: string
readonly VITE_OLLAMA_API_URL: string
readonly VITE_OLLAMA_BASE_URL: string
// API URL (legacy alias for VITE_API_BASE)
readonly VITE_API_URL: string
// Dev mode flag
readonly VITE_DEV: string
// Ports
readonly VITE_PORT_FRONTEND: string
readonly VITE_PORT_FRONTEND_HMR: string
// Neo4j
readonly VITE_NEO4J_URL: string
readonly VITE_NEO4J_USER: string
readonly VITE_NEO4J_PASSWORD: string
} }
interface ImportMeta { interface ImportMeta {