This commit is contained in:
lovebird 2026-04-11 21:06:22 +02:00
parent 138068a3d9
commit b5dbf55c71
45 changed files with 112 additions and 123 deletions

View File

@ -19,6 +19,7 @@ import TopNavigation from "@/components/TopNavigation";
import Footer from "@/components/Footer";
import { DragDropProvider } from "@/contexts/DragDropContext";
import { useAppStore } from "@/store/appStore";
import { serverUrl } from "@/lib/db";
const GlobalDragDrop = React.lazy(() => import("@/components/GlobalDragDrop"));
// Register all widgets on app boot
@ -66,6 +67,7 @@ let SupportChat: any;
GridSearch = React.lazy(() => import("./modules/places/gridsearch/GridSearch"));
LocationDetail = React.lazy(() => import("./modules/places/LocationDetail"));
TypesPlayground = React.lazy(() => import("@/modules/types/TypesPlayground"));
PlaygroundAuth = React.lazy(() => import("./playground/auth"));
if (enablePlaygrounds) {
PlaygroundEditor = React.lazy(() => import("./pages/PlaygroundEditor"));
@ -77,8 +79,7 @@ if (enablePlaygrounds) {
VariablePlayground = React.lazy(() => import("./components/variables/VariablesEditor").then(module => ({ default: module.VariablesEditor })));
I18nPlayground = React.lazy(() => import("./components/playground/I18nPlayground"));
PlaygroundChat = React.lazy(() => import("./pages/PlaygroundChat"));
PlaygroundVfs = React.lazy(() => import("./pages/PlaygroundVfs"));
PlaygroundAuth = React.lazy(() => import("./playground/auth"));
PlaygroundVfs = React.lazy(() => import("./pages/PlaygroundVfs"));
SupportChat = React.lazy(() => import("./pages/SupportChat"));
}
@ -203,12 +204,13 @@ const AppWrapper = () => {
<Route path="/playground/i18n" element={<React.Suspense fallback={<div>Loading...</div>}><I18nPlayground /></React.Suspense>} />
<Route path="/playground/chat" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundChat /></React.Suspense>} />
<Route path="/playground/vfs" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundVfs /></React.Suspense>} />
<Route path="/playground/auth" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundAuth /></React.Suspense>} />
</>
)}
{enablePlaygrounds && <Route path="/support-chat" element={<React.Suspense fallback={<div>Loading...</div>}><SupportChat /></React.Suspense>} />}
<Route path="/playground/auth" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundAuth /></React.Suspense>} />
{/* Logs */}
<Route path="/logs" element={<React.Suspense fallback={<div>Loading...</div>}><LogsPage /></React.Suspense>} />
@ -284,8 +286,8 @@ const App = () => {
<BrowserRouter future={{ v7_relativeSplatPath: true }}>
<DragDropProvider>
<ProfilesProvider>
<WebSocketProvider url={import.meta.env.VITE_SERVER_IMAGE_API_URL}>
<StreamProvider url={import.meta.env.VITE_SERVER_IMAGE_API_URL}>
<WebSocketProvider url={serverUrl}>
<StreamProvider>
<StreamInvalidator />
<FeedCacheProvider>
<AppWrapper />

View File

@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import { useAuth } from "@/hooks/useAuth";
import { getCurrentLang, translate } from "@/i18n";
import { serverUrl } from "@/lib/db";
const EcommerceBundle = React.lazy(() => import("@polymech/ecommerce").then(m => ({ default: m.EcommerceBundle })));
@ -68,7 +69,7 @@ export const EcommerceBundleWrapper = () => {
siteName: "PolyMech",
contactEmail: "sales@plastic-hub.com",
stripePublishableKey: import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY,
apiBaseUrl: import.meta.env.VITE_SERVER_IMAGE_API_URL || "",
apiBaseUrl: serverUrl,
getAuthToken: async () => session?.access_token ?? null,
locale,
t: translate,

View File

@ -351,7 +351,6 @@ export const CreationWizardPopup: React.FC<CreationWizardPopupProps> = ({
const videoToken = await getAuthToken();
return new Promise((resolve, reject) => {
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const formData = new FormData();
formData.append('file', file);

View File

@ -4,7 +4,7 @@ import { set } from 'idb-keyval';
import { toast } from 'sonner';
import { Upload } from 'lucide-react';
import { T, translate } from '@/i18n';
import { getAuthToken } from '@/lib/db';
import { apiClient } from '@/lib/db';
import { useDragDrop } from '@/contexts/DragDropContext';
const GlobalDragDrop = () => {
@ -50,19 +50,13 @@ const GlobalDragDrop = () => {
} else if (isUrl) {
// URL workflow
toast.info(translate('Processing link...'));
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const token = await getAuthToken();
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
try {
const response = await fetch(`${serverUrl}/api/serving/site-info?url=${encodeURIComponent(url)}`, {
headers
});
if (!response.ok) throw new Error('Failed to fetch site info');
const siteInfo = await response.json();
const siteInfo = await apiClient<{
page?: { image?: string; title?: string; description?: string };
title?: string;
description?: string;
}>(`/api/serving/site-info?url=${encodeURIComponent(url)}`);
const virtualItem = {
id: crypto.randomUUID(),

View File

@ -77,6 +77,7 @@ const getPlainText = (children: React.ReactNode): string => {
};
import { substitute } from '@/lib/variables';
import { serverUrl } from '@/lib/db';
// Helper to strip YAML frontmatter
const stripFrontmatter = (text: string) => {
@ -98,7 +99,7 @@ const MarkdownRenderer = React.memo(({ content, className = "", variables, baseU
// If baseUrl is relative, make it absolute using the API origin so the server can fetch it
let absoluteBase = baseUrl;
if (baseUrl.startsWith('/')) {
const apiOrigin = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
const apiOrigin = serverUrl || window.location.origin;
// if API url is absolute (http://...), use it as the base.
// fallback to window.location.origin for relative API configs.
const originToUse = apiOrigin.startsWith('http') ? apiOrigin : window.location.origin;

View File

@ -3,6 +3,7 @@ import { User as UserIcon } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useNavigate } from "react-router-dom";
import { useProfiles } from "@/contexts/ProfilesContext";
import { serverUrl } from "@/lib/db";
interface UserAvatarBlockProps {
userId: string;
@ -44,9 +45,7 @@ const UserAvatarBlock: React.FC<UserAvatarBlockProps> = ({
// Construct optimized URL
// We use the render endpoint: /api/images/render?url=...&width=128&format=webp
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
if (!serverUrl) return url;
try {
const optimized = new URL(`${serverUrl}/api/images/render`);
optimized.searchParams.set('url', url);

View File

@ -18,6 +18,7 @@ import { formatDate, isLikelyFilename } from "@/utils/textUtils";
// MediaPlayer, MediaProvider, type MediaPlayerInstance
// } from '@vidstack/react';
import type { MediaPlayerInstance } from '@vidstack/react';
import { serverUrl } from "@/lib/db";
// Import Vidstack styles
// import '@vidstack/react/player/styles/default/theme.css';
@ -120,7 +121,7 @@ const VideoCard = ({
// Extracts the /api/ path and prepends the configured server base.
const rewriteUrl = (url: string): string => {
if (!url) return url;
const serverBase = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const serverBase = serverUrl;
if (!serverBase) return url;
const apiIdx = url.indexOf('/api/');
if (apiIdx !== -1) return `${serverBase}${url.slice(apiIdx)}`;
@ -198,7 +199,7 @@ const VideoCard = ({
if (!match) return;
const jobId = match[1];
const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const baseUrl = serverUrl || '';
let eventSource: EventSource | null = null;
let isMounted = true;
@ -278,8 +279,7 @@ const VideoCard = ({
if (!match) return;
const jobId = match[1];
const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const baseUrl = serverUrl || '';
try {
await fetch(`${baseUrl}/api/videos/jobs/${jobId}`, {
method: 'DELETE'

View File

@ -23,6 +23,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { serverUrl } from "@/lib/db";
interface BanList {
bannedIPs: string[];
@ -42,7 +43,7 @@ export const BansManager = ({ session }: { session: any }) => {
const fetchBanList = async () => {
try {
setLoading(true);
const res = await fetch(`${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/admin/bans`, {
const res = await fetch(`${serverUrl}/api/admin/bans`, {
headers: {
'Authorization': `Bearer ${session?.access_token || ''}`
}
@ -75,7 +76,7 @@ export const BansManager = ({ session }: { session: any }) => {
? { ip: unbanTarget.value }
: { userId: unbanTarget.value };
const res = await fetch(`${import.meta.env.VITE_SERVER_IMAGE_API_URL}${endpoint}`, {
const res = await fetch(`${serverUrl}${endpoint}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${session?.access_token || ''}`,

View File

@ -17,6 +17,7 @@ import {
} from "@/components/ui/popover";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useAuth } from "@/hooks/useAuth";
import { serverUrl } from "@/lib/db";
interface UserPickerProps {
value?: string;
@ -48,8 +49,8 @@ export function UserPicker({ value, onSelect, disabled }: UserPickerProps) {
try {
setLoading(true);
const url = q
? `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/profiles?q=${encodeURIComponent(q)}`
: `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/profiles`;
? `${serverUrl}/api/profiles?q=${encodeURIComponent(q)}`
: `${serverUrl}/api/profiles`;
// Remove early return to allow fetching defaults
// if (!q) { ... }

View File

@ -9,11 +9,12 @@ import {
fetchVideos, deleteVideos, scanVideoDisk, deleteVideoFolders,
type VideoEntry, type DiskVideoFolder
} from '@/modules/posts/client-videos';
import { serverUrl } from '@/lib/db';
/** Normalize URLs: DB may store relative paths or old localhost URLs. */
const rewriteUrl = (url: string): string => {
if (!url) return url;
const serverBase = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const serverBase = serverUrl;
if (!serverBase) return url;
const apiIdx = url.indexOf('/api/');
if (apiIdx !== -1) return `${serverBase}${url.slice(apiIdx)}`;

View File

@ -5,6 +5,7 @@ import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { AlertTriangle, RefreshCw } from "lucide-react";
import { T, translate } from "@/i18n";
import { serverUrl } from "@/lib/db";
import {
Table,
TableBody,
@ -36,7 +37,7 @@ export const ViolationsMonitor = ({ session }: { session: any }) => {
const fetchViolationStats = async () => {
try {
setLoading(true);
const res = await fetch(`${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/admin/bans/violations`, {
const res = await fetch(`${serverUrl}/api/admin/bans/violations`, {
headers: {
'Authorization': `Bearer ${session?.access_token || ''}`
}

View File

@ -20,6 +20,7 @@ import { FileBrowserWidget } from '@/modules/storage/FileBrowserWidget';
import { vfsUrl } from '@/modules/storage/helpers';
import type { INode } from '@/modules/storage/types';
import { useAuth } from '@/hooks/useAuth';
import { serverUrl } from '@/lib/db';
export const OPEN_LINK_EDITOR_COMMAND: LexicalCommand<void> = createCommand('OPEN_LINK_EDITOR_COMMAND');
@ -222,7 +223,6 @@ function LinkEditorComponent() {
}, []);
const handleImageSelectPicture = useCallback((picture: { id: string; title: string; image_url: string }) => {
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const linkUrl = serverUrl
? `${serverUrl}/api/images/render?url=${encodeURIComponent(picture.image_url)}`
: picture.image_url;

View File

@ -45,6 +45,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '
import { LayoutGrid, GalleryVerticalEnd, TrendingUp, Clock, List, FolderTree, FileText, Image as ImageIcon, EyeOff, Lock, SlidersHorizontal, Layers, Camera, MapPin } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { serverUrl } from '@/lib/db';
const SIDEBAR_KEY = 'categorySidebarSize';
const DEFAULT_SIDEBAR = 15;
@ -299,7 +300,7 @@ const HomeWidget: React.FC<HomeWidgetProps> = ({
if (pictures.length === 0) {
return <div className="py-8 text-center text-muted-foreground"><T>No pictures yet</T></div>;
}
const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const SERVER_URL = serverUrl || '';
const isAuto = columns === 'auto';
const gridColsClass = isAuto ? 'grid-cols-[repeat(auto-fit,minmax(250px,350px))] justify-center' :
Number(columns) === 1 ? 'grid-cols-1' :

View File

@ -7,6 +7,7 @@ import { Plus, Trash2, Video, Image as ImageIcon, FileText } from 'lucide-react'
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import ResponsiveImage from '@/components/ResponsiveImage';
import { serverUrl } from '@/lib/db';
export interface ButtonConfig {
id: string;
@ -203,7 +204,7 @@ const VideoBannerWidget: React.FC<VideoBannerWidgetProps> = ({
// Supabase bucket URLs are proxied through the server's image render API.
const rewriteUrl = (url: string): string => {
if (!url) return url;
const serverBase = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const serverBase = serverUrl;
if (!serverBase) return url;
const apiIdx = url.indexOf('/api/');
if (apiIdx !== -1) return `${serverBase}${url.slice(apiIdx)}`;

View File

@ -1,5 +1,6 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { UserProfile } from "@/modules/posts/views/types";
import { serverUrl } from '@/lib/db';
interface ProfilesContextType {
profiles: Record<string, UserProfile>;
@ -31,7 +32,7 @@ export const ProfilesProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setIsLoading(true);
try {
// Use the new batch API endpoint
const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const SERVER_URL = serverUrl;
const res = await fetch(`${SERVER_URL}/api/profiles?ids=${uniqueMissingIds.join(',')}`);
if (!res.ok) {
console.error('Error fetching profiles context:', res.statusText);

View File

@ -1,6 +1,7 @@
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { AppEvent } from '../types-server';
import logger from '@/Logger';
import { serverUrl } from '@/lib/db';
type StreamStatus = 'DISCONNECTED' | 'CONNECTING' | 'CONNECTED' | 'ERROR';
@ -41,18 +42,13 @@ export const StreamProvider: React.FC<StreamProviderProps> = ({ children, url })
}, []);
useEffect(() => {
if (!url) return;
let eventSource: EventSource | null = null;
let reconnectTimer: NodeJS.Timeout | null = null;
const connect = () => {
setStatus('CONNECTING');
const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const streamUrl = SERVER_URL
? `${SERVER_URL}/api/stream`
: `/api/stream`;
const streamUrl = `${serverUrl}/api/stream`;
try {
eventSource = new EventSource(streamUrl);
@ -117,7 +113,7 @@ export const StreamProvider: React.FC<StreamProviderProps> = ({ children, url })
eventSource?.close();
if (reconnectTimer) clearTimeout(reconnectTimer);
};
}, [url]);
}, []);
return (
<StreamContext.Provider value={{ status, lastEvent, isConnected: status === 'CONNECTED', subscribe }}>

View File

@ -121,7 +121,9 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ children,
}, [disconnectWebSocket]);
const connectToServer = useCallback(async (urlToUse?: string): Promise<boolean> => {
const targetUrl = urlToUse || apiUrl;
const raw = urlToUse ?? apiUrl;
const targetUrl =
raw || (typeof window !== 'undefined' ? window.location.origin : '');
if (urlToUse && urlToUse !== apiUrl) {
setApiUrl(targetUrl);
@ -162,10 +164,12 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ children,
disconnectWebSocket(true);
}, [disconnectWebSocket]);
// Initial connection effect
// Initial connection effect (same-origin when initialUrl is empty)
useEffect(() => {
if (initialUrl && !isConnected && !isConnecting && !connectionAborted) {
connectToServer(initialUrl);
const base =
initialUrl || (typeof window !== 'undefined' ? window.location.origin : '');
if (base && !isConnected && !isConnecting && !connectionAborted) {
connectToServer(base);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialUrl]);

View File

@ -1,6 +1,7 @@
import { useState, useEffect, useMemo } from 'react';
import { ResponsiveData } from '@/components/ResponsiveImage';
import { serverUrl } from '@/lib/db';
interface UseResponsiveImageProps {
src: string | File | null;
@ -22,7 +23,7 @@ export const useResponsiveImage = ({
responsiveSizes = DEFAULT_SIZES,
formats = DEFAULT_FORMATS,
enabled = true,
apiUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL,
apiUrl = serverUrl,
}: UseResponsiveImageProps) => {
const [data, setData] = useState<ResponsiveData | null>(null);
const [loading, setLoading] = useState(false);
@ -78,7 +79,7 @@ export const useResponsiveImage = ({
if (!requestCache.has(cacheKey)) {
const requestPromise = (async () => {
const serverUrl = apiUrl || import.meta.env.VITE_SERVER_IMAGE_API_URL;
const serverBase = apiUrl;
// Resolve relative URLs to absolute so the server-side API can fetch them
let resolvedSrc = src;
if (src.startsWith('/')) {
@ -96,7 +97,7 @@ export const useResponsiveImage = ({
formats: JSON.stringify(formats),
});
const response = await fetch(`${serverUrl}/api/images/responsive?${params.toString()}`);
const response = await fetch(`${serverBase}/api/images/responsive?${params.toString()}`);
if (!response.ok) {
const txt = await response.text();
@ -122,7 +123,6 @@ export const useResponsiveImage = ({
formData.append('sizes', JSON.stringify(responsiveSizes));
formData.append('formats', JSON.stringify(formats));
const serverUrl = apiUrl || import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const response = await fetch(`${serverUrl}/api/images/responsive`, {
method: 'POST',
body: formData,

View File

@ -1,22 +1,16 @@
import { useQuery } from '@tanstack/react-query';
import type { AppConfig } from '../../shared/src/config/config';
import { apiClient, serverUrl } from '@/lib/db.js';
interface SystemInfo {
env: Record<string, string> & { appConfig?: AppConfig };
}
export const useSystemInfo = () => {
console.log(`useSystemInfo : ${serverUrl}`);
return useQuery<SystemInfo>({
queryKey: ['system-info'],
queryFn: async () => {
const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const fetchUrl = SERVER_URL
? `${SERVER_URL}/api/system-info`
: '/api/system-info';
const res = await fetch(fetchUrl);
if (!res.ok) throw new Error('Failed to fetch system info');
return res.json();
},
queryFn: () => apiClient<SystemInfo>('/api/system-info'),
staleTime: Infinity,
gcTime: Infinity,
});

View File

@ -10,7 +10,7 @@
* See PRESET_TOOLS mapping below for tool combinations.
*/
import OpenAI from 'openai';
import { getAuthToken as getZitadelToken } from "@/lib/db";
import { getAuthToken as getZitadelToken, serverUrl } from "@/lib/db";
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { RunnableToolFunctionWithParse } from 'openai/lib/RunnableFunction';
@ -114,7 +114,7 @@ export const createOpenAIClient = async (apiKey?: string): Promise<OpenAI | null
console.log('[createOpenAIClient] resolved token:', token ? token.substring(0, 10) + '...' : 'null');
return new OpenAI({
apiKey: token, // This is sent as Bearer token to our proxy
baseURL: `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/openai/v1`,
baseURL: `${serverUrl}/api/openai/v1`,
dangerouslyAllowBrowser: true // Required for client-side usage
});
} catch (error) {
@ -522,10 +522,8 @@ export const transcribeAudio = async (
// Convert audio file to base64
const base64Audio = await encodeWav(audioFile);
const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
// Use the OpenRouter proxy endpoint
const response = await fetch(`${baseUrl}/api/openrouter/v1/chat/completions`, {
const response = await fetch(`${serverUrl}/api/openrouter/v1/chat/completions`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
@ -1291,7 +1289,7 @@ export const runTools = async (options: RunToolsOptions): Promise<RunToolsResult
}
client = new OpenAI({
apiKey: token,
baseURL: `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/openrouter/v1`,
baseURL: `${serverUrl}/api/openrouter/v1`,
dangerouslyAllowBrowser: true,
});
} else {

View File

@ -1,5 +1,6 @@
import { GoogleGenAI } from "@google/genai";
import { getGoogleApiKey } from "@/image-api";
import { serverUrl } from "./db";
export interface VideoGenerationResult {
videoUrl: string;
@ -203,7 +204,6 @@ export const generateVideo = async (
const targetUrl = `${videoFile.uri}${separator}key=${apiKey}`;
// Use the proxy endpoint to fetch the video, as direct access might be blocked by CORS or require specific headers.
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const proxyUrl = `${serverUrl}/api/videos/proxy?url=${encodeURIComponent(targetUrl)}`;
return { videoUrl: proxyUrl, metadata: operation.response };

View File

@ -11,14 +11,13 @@
import { z } from 'zod';
import { searchContent, type SearchOptions } from '@/modules/search/client-search';
import type { RunnableToolFunctionWithParse } from 'openai/lib/RunnableFunction';
import { serverUrl } from '@/lib/db';
type LogFunction = (level: string, message: string, data?: any) => void;
const defaultLog: LogFunction = (level, message, data) => console.log(`[${level}] ${message}`, data);
/** Production server base URL for constructing public links */
const SERVER_BASE = (import.meta.env.VITE_SERVER_IMAGE_API_URL
|| import.meta.env.VITE_SERVER_IMAGE_API_URL
|| '').replace(/\/$/, '');
const SERVER_BASE = serverUrl;
/** Extract the best image URL from a FeedPost-shaped search result */
const extractImageUrl = (item: any): string | null => {

View File

@ -2,6 +2,8 @@
* Chat Playground shared types & helpers
*/
import { serverUrl } from "@/lib/db";
export interface ImageAttachment {
id: string;
url: string; // data URL for local files, or remote URL from picker
@ -41,9 +43,7 @@ export const fileToDataUrl = (file: File): Promise<string> =>
/** Convert a remote image URL to a resized proxy URL via the production server.
* Local images (data URLs, blobs) pass through unchanged. */
const RENDER_API_BASE = import.meta.env.VITE_SERVER_IMAGE_API_URL_R
|| import.meta.env.VITE_SERVER_IMAGE_API_URL
|| '';
const RENDER_API_BASE = serverUrl;
export const getResizedImageUrl = (img: ImageAttachment, maxWidth = 1000): string => {
if (img.isLocal || img.url.startsWith('data:') || img.url.startsWith('blob:')) {

View File

@ -12,6 +12,7 @@ import { useAuth } from '@/hooks/useAuth';
import { usePromptHistory } from '@/hooks/usePromptHistory';
import { useDragDrop } from '@/contexts/DragDropContext';
import { getProviderConfig } from '@/modules/user/client-user';
import { serverUrl } from '@/lib/db';
import { createOpenAIClient } from '@/lib/openai';
import { createSearchToolPreset, createWebSearchToolPreset } from '@/modules/ai/searchTools';
import { createPageTool } from '@/lib/pageTools';
@ -177,8 +178,7 @@ export function useChatEngine(namespace = 'chat') {
const token = await getAuthToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
} catch { }
const apiBase = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const res = await fetch(`${apiBase}/api/vfs/read/${mount}/${clean}`, { headers });
const res = await fetch(`${serverUrl}/api/vfs/read/${mount}/${clean}`, { headers });
if (!res.ok) {
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
addChatLog('error', `Failed to read file: ${err.error || res.status}`);
@ -262,14 +262,14 @@ export function useChatEngine(namespace = 'chat') {
return new OpenAI({
apiKey: token, // This is sent as Bearer token to our proxy
baseURL: `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/openrouter/v1`,
baseURL: `${serverUrl}/api/openrouter/v1`,
dangerouslyAllowBrowser: true
});
}
if (provider === 'support') {
return new OpenAI({
apiKey: 'support-token-placeholder', // Ignored by proxy, but required by OpenAI SDK
baseURL: `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/support/v1`,
baseURL: `${serverUrl}/api/support/v1`,
dangerouslyAllowBrowser: true
});
}

View File

@ -1,5 +1,7 @@
// ─── Types ────────────────────────────────────────────────────────────────────
import { serverUrl } from '@/lib/db';
export interface Campaign {
id: string;
owner_id: string;
@ -32,7 +34,7 @@ async function authHeaders(contentType?: string): Promise<HeadersInit> {
return h;
}
const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const SERVER_URL = serverUrl;
async function apiFetch(path: string, init?: RequestInit) {
const url = SERVER_URL ? `${SERVER_URL}${path}` : path;

View File

@ -1,4 +1,4 @@
import { fetchWithDeduplication } from "@/lib/db";
import { fetchWithDeduplication, serverUrl } from "@/lib/db";
// ─── Types ────────────────────────────────────────────────────────────────────
@ -60,7 +60,7 @@ async function authHeaders(contentType?: string): Promise<HeadersInit> {
return h;
}
const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const SERVER_URL = serverUrl;
async function apiFetch(path: string, init?: RequestInit) {
const url = SERVER_URL ? `${SERVER_URL}${path}` : path;

View File

@ -3,8 +3,10 @@
* Calls server routes at /api/contacts/mailboxes
*/
const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const API_BASE = '/api/contacts/mailboxes';
import { serverUrl } from '@/lib/db';
const SERVER_URL = serverUrl;
const API_BASE = `${serverUrl}/api/contacts/mailboxes`;
async function authHeaders(contentType?: string): Promise<HeadersInit> {
const { getAuthToken } = await import('@/lib/db');

View File

@ -17,6 +17,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { updatePage, updatePageMeta, deletePage } from "./client-pages";
import { serverUrl } from "@/lib/db";
const CategoryManager = React.lazy(() => import("@/components/widgets/CategoryManager").then(module => ({ default: module.CategoryManager })));
const VersionManager = React.lazy(() => import("./VersionManager").then(module => ({ default: module.VersionManager })));
@ -59,7 +60,7 @@ export const PageActions = ({
const addItem = useCartStore((s) => s.addItem);
const navigate = useNavigate();
const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
const baseUrl = serverUrl;
// Extract product price from typeValues (first entry with a numeric price > 0)
const productPrice = (() => {

View File

@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { ArrowLeft, Monitor, Smartphone } from "lucide-react";
import { Page } from "../types";
import { getCurrentLang } from "@/i18n";
import { serverUrl } from "@/lib/db";
interface EmailPreviewPanelProps {
page: Page;
@ -21,7 +22,6 @@ export const EmailPreviewPanel = ({
iframeRef,
authToken,
}: EmailPreviewPanelProps) => {
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const previewPath = orgSlug
? `/org/${orgSlug}/user/${page.owner}/pages/${page.slug}/email-preview`
: `/user/${page.owner}/pages/${page.slug}/email-preview`;

View File

@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from "react";
import { getAuthToken } from "@/lib/db";
import { getAuthToken, serverUrl } from "@/lib/db";
import { toast } from "sonner";
import { translate, getCurrentLang } from "@/i18n";
@ -33,7 +33,6 @@ export function useEmailActions({ page, orgSlug }: UseEmailActionsParams) {
setIsSendingEmail(true);
try {
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const lang = getCurrentLang();
const endpoint = orgSlug
? `${serverUrl}/org/${orgSlug}/user/${page.owner}/pages/${page.slug}/email-send?lang=${lang}`

View File

@ -2,8 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import { useDraggable } from '@dnd-kit/core';
import { useNavigate, useParams } from 'react-router-dom';
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { getAuthToken } from "@/lib/db";
import { getAuthToken, serverUrl } from "@/lib/db";
import { toast } from "sonner";
import {
LayoutTemplate,
@ -439,7 +438,7 @@ export const PageRibbonBar = ({
const { updateAction, getActionsByGroup } = useActions();
// Logic duplicated from PageActions
const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
const baseUrl = serverUrl;
useEffect(() => {
if (activeTab === 'advanced') {

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { ExternalLink, BookOpen, X, Info, Globe } from 'lucide-react';
import CollapsibleSection from '../../components/CollapsibleSection';
import { serverUrl } from '@/lib/db';
interface WikiResult {
pageid: number;
@ -44,8 +45,7 @@ export const InfoPanel = React.memo(({ isOpen, onClose, lat, lng, locationName }
setLoadingWiki(true);
setErrorWiki(null);
try {
const apiUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const res = await fetch(`${apiUrl}/api/locations/wiki?lat=${stableLat}&lon=${stableLng}&limit=20`);
const res = await fetch(`${serverUrl}/api/locations/wiki?lat=${stableLat}&lon=${stableLng}&limit=20`);
if (res.ok) {
const json = await res.json();
setArticles(json.data || []);
@ -72,8 +72,7 @@ export const InfoPanel = React.memo(({ isOpen, onClose, lat, lng, locationName }
setLoadingLlm(true);
setLlmInfo(null);
try {
const apiUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const res = await fetch(`${apiUrl}/api/locations/llm-info?location=${encodeURIComponent(locationName)}`);
const res = await fetch(`${serverUrl}/api/locations/llm-info?location=${encodeURIComponent(locationName)}`);
if (res.ok) {
const json = await res.json();
setLlmInfo(json.data);

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import maplibregl from 'maplibre-gl';
import { POSTER_THEMES, applyPosterTheme } from '../utils/poster-themes';
import { serverUrl } from '@/lib/db';
// Fallback feather icons if lucide-react unavailable, or just raw SVG
const XIcon = ({ className }: { className?: string }) => (
@ -40,8 +41,7 @@ export function MapPosterOverlay({ map, pickerRegions, pickerPolygons, posterThe
const fetchGeo = async () => {
const c = map.getCenter();
try {
const apiUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const res = await fetch(`${apiUrl}/api/regions/reverse?lat=${c.lat}&lon=${c.lng}`);
const res = await fetch(`${serverUrl}/api/regions/reverse?lat=${c.lat}&lon=${c.lng}`);
if (res.ok) {
const json = await res.json();
if (json.data) {

View File

@ -1,5 +1,5 @@
import React from "react";
import { getAuthToken } from "@/lib/db";
import { getAuthToken, serverUrl } from "@/lib/db";
import { useAuth } from "@/hooks/useAuth";
import { useNavigate, Navigate } from "react-router-dom";
import { CreationWizardPopup } from "@/components/CreationWizardPopup";
@ -28,7 +28,6 @@ const NewPost = () => {
try {
// Use relative path for API calls to support mobile/PWA proxying
// Fallback to localhost only if strictly needed in dev without proxy, but empty string is safer for Vite proxy
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
addLog(`Fetching site info from: ${serverUrl || '/api'}/serving/site-info`);
const token = await getAuthToken();

View File

@ -1,6 +1,6 @@
import { PostMediaItem } from "@/modules/posts/views/types";
import { MediaItem } from "@/types";
import { fetchWithDeduplication, apiClient, getAuthHeaders } from "@/lib/db";
import { fetchWithDeduplication, apiClient, getAuthHeaders, serverUrl } from "@/lib/db";
import { uploadImage } from "@/lib/uploadUtils";
import { FetchMediaOptions } from "@/utils/mediaUtils";
@ -260,7 +260,6 @@ export const transformImage = async (file: File | Blob, operations: any[]): Prom
formData.append('file', file);
formData.append('operations', JSON.stringify(operations));
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
const response = await fetch(`${serverUrl}/api/images/transform`, {
method: 'POST',
headers,

View File

@ -1,6 +1,6 @@
import { getAuthToken } from "@/lib/db";
import { getAuthToken, serverUrl } from "@/lib/db";
const API_BASE = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const API_BASE = serverUrl || '';
export interface VideoEntry {
id: string;

View File

@ -12,10 +12,10 @@ import {
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { toast } from "sonner";
import { generateOfflineZip } from "@/utils/zipGenerator";
import { T } from "@/i18n";
import { PostItem, UserProfile } from "../types";
import { Picture } from "@/types-server";
import { serverUrl } from "../db";
interface ExportDropdownProps {
post: PostItem | null;
@ -37,7 +37,7 @@ export const ExportDropdown: React.FC<ExportDropdownProps> = ({
const [isEmailDialogOpen, setIsEmailDialogOpen] = useState(false);
const [emailHtml, setEmailHtml] = useState('');
const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
const baseUrl = serverUrl;
const embedUrl = `${baseUrl}/embed/${post?.id}`;
const embedCode = `<iframe src="${embedUrl}" width="100%" height="600" frameborder="0"></iframe>`;

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { apiClient } from "@/lib/db";
import { apiClient, serverUrl } from "@/lib/db";
import { Link, useNavigate } from "react-router-dom";
import { User as UserIcon, LayoutGrid, StretchHorizontal, FileText, Save, X, Edit3, MoreVertical, Trash2, ArrowUp, ArrowDown, Heart, MessageCircle, Maximize, ImageIcon, Youtube, Music, Wand2, Map, Brush, Mail, Archive } from 'lucide-react';
import { useOrganization } from "@/contexts/OrganizationContext";
@ -75,7 +75,7 @@ export const ArticleRenderer: React.FC<PostRendererProps> = (props) => {
const navigate = useNavigate();
const { orgSlug } = useOrganization();
const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
const baseUrl = serverUrl || window.location.origin;
const embedUrl = `${baseUrl}/embed/${post?.id || mediaItem.id}`;
const embedCode = `<iframe src="${embedUrl}" width="100%" height="600" frameborder="0"></iframe>`;
const [emailHtml, setEmailHtml] = useState('');

View File

@ -1,4 +1,4 @@
const SERVER_API_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
import { serverUrl } from "@/lib/db";
export interface SearchOptions {
q: string;
@ -23,7 +23,7 @@ export const searchContent = async (options: SearchOptions): Promise<any[]> => {
if (options.formats) params.append('formats', options.formats);
if (options.visibilityFilter) params.append('visibilityFilter', options.visibilityFilter);
const url = `${SERVER_API_URL}/api/search?${params.toString()}`;
const url = `${serverUrl}/api/search?${params.toString()}`;
const headers: Record<string, string> = {};
if (options.token) headers['Authorization'] = `Bearer ${options.token}`;
const res = await fetch(url, { headers });

View File

@ -1,8 +1,8 @@
import { UserProfile } from "@/modules/posts/views/types";
import { fetchWithDeduplication, apiClient, getAuthToken as getZitadelToken } from "@/lib/db";
import { fetchWithDeduplication, apiClient, getAuthToken as getZitadelToken, serverUrl as serverBaseUrl } from "@/lib/db";
const serverUrl = (path: string) => {
const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
const baseUrl = serverBaseUrl || window.location.origin;
return `${baseUrl}${path}`;
};

View File

@ -15,6 +15,7 @@ import { BansManager } from "@/components/admin/BansManager";
import { ViolationsMonitor } from "@/components/admin/ViolationsMonitor";
import React, { Suspense } from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import { serverUrl } from "@/lib/db";
// Lazy load AnalyticsDashboard
const AnalyticsDashboard = React.lazy(() => import("@/modules/analytics").then(module => ({ default: module.AnalyticsDashboard })));
@ -108,7 +109,7 @@ const ServerSection = ({ session }: { session: any }) => {
const handleFlushCache = async () => {
try {
setLoading(true);
const res = await fetch(`${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/flush-cache`, {
const res = await fetch(`${serverUrl}/api/flush-cache`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${session?.access_token || ''}`
@ -138,7 +139,7 @@ const ServerSection = ({ session }: { session: any }) => {
}
try {
const res = await fetch(`${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/admin/system/restart`, {
const res = await fetch(`${serverUrl}/api/admin/system/restart`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${session?.access_token || ''}`

View File

@ -1,15 +1,14 @@
import React, { useState, useRef, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { ImagePickerDialog } from '@/components/widgets/ImagePickerDialog';
import { RotateCw, RotateCcw, Crop as CropIcon, Download, Sliders, Image as ImageIcon, X, Check, Save } from 'lucide-react';
import { fetchPictureById, updatePicture } from '@/modules/posts/client-pictures';
import { useAuth } from '@/hooks/useAuth';
import { useToast } from '@/components/ui/use-toast';
import { cn } from '@/lib/utils';
import { Slider } from '@/components/ui/slider';
import { uploadImage } from '@/lib/uploadUtils';
import { serverUrl } from '@/lib/db';
const PlaygroundImageEditor = () => {
const [pickerOpen, setPickerOpen] = useState(false);
@ -82,7 +81,6 @@ const PlaygroundImageEditor = () => {
formData.append('file', file);
formData.append('operations', JSON.stringify(ops));
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const res = await fetch(`${serverUrl}/api/images/transform`, {
method: 'POST',
body: formData

View File

@ -8,6 +8,7 @@ import VideoCard from "@/components/VideoCard";
import { toast } from "sonner";
import { useAuth } from "@/hooks/useAuth";
import { Loader2, AlertCircle, CheckCircle2 } from "lucide-react";
import { serverUrl } from "@/lib/db";
type UploadStatus = 'idle' | 'uploading' | 'processing' | 'ready' | 'error';
@ -47,9 +48,7 @@ const VideoPlayerPlaygroundIntern = () => {
};
const trackProgress = (id: string) => {
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const eventSource = new EventSource(`${serverUrl}/api/videos/jobs/${id}/progress`);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
@ -77,7 +76,6 @@ const VideoPlayerPlaygroundIntern = () => {
};
const pollJob = async (id: string) => {
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
let attempts = 0;
const maxAttempts = 60; // 2 minutes
@ -132,8 +130,6 @@ const VideoPlayerPlaygroundIntern = () => {
const formData = new FormData();
formData.append('file', file);
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
try {
const xhr = new XMLHttpRequest();
xhr.open('POST', `${serverUrl}/api/videos/upload?userId=${user.id}&title=${encodeURIComponent(title)}&preset=original`);

View File

@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Shield, RefreshCw, ExternalLink, Copy, Check } from 'lucide-react';
import { serverUrl } from '@/lib/db';
type AuthzDebugResponse = {
verified: boolean;
@ -108,7 +109,7 @@ function Row({
*/
export default function PlaygroundAuth() {
const auth = useAuth();
const base = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const base = serverUrl || '';
const [loading, setLoading] = useState(false);
const [last, setLast] = useState<AuthzDebugResponse | null>(null);
const [fetchError, setFetchError] = useState<string | null>(null);

View File

@ -2,7 +2,7 @@
* Upload Utility Functions
*/
import { getAuthToken } from '@/lib/db';
import { getAuthToken, serverUrl } from '@/lib/db';
// Helper to upload internal video
export const uploadInternalVideo = async (
@ -13,7 +13,6 @@ export const uploadInternalVideo = async (
const token = await getAuthToken();
return new Promise((resolve, reject) => {
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const formData = new FormData();
formData.append('file', file);
const title = file.name.replace(/\.[^/.]+$/, '');