Spaces:
Running
Running
visilog
Browse files- Dockerfile +0 -13
- client/src/pages/VisitLog.jsx +31 -28
- server/main.py +16 -0
Dockerfile
CHANGED
|
@@ -2,25 +2,12 @@
|
|
| 2 |
# Use Node.js 20 as required by Vite and its dependencies
|
| 3 |
FROM node:20-alpine AS client-builder
|
| 4 |
|
| 5 |
-
# Explicitly declare the build argument for the Google Maps API key.
|
| 6 |
-
# This allows Hugging Face to pass the secret during the build process.
|
| 7 |
-
ARG VITE_GOOGLE_MAPS_API_KEY
|
| 8 |
-
|
| 9 |
WORKDIR /app/client
|
| 10 |
COPY client/package*.json ./
|
| 11 |
RUN npm install
|
| 12 |
|
| 13 |
COPY client/ .
|
| 14 |
# Build for production
|
| 15 |
-
# The SPACE_ID env var is automatically provided by Hugging Face for correct asset paths.
|
| 16 |
-
|
| 17 |
-
# Create a .env file with the API key just before building.
|
| 18 |
-
# Vite will automatically load this file. This is a robust way to inject secrets.
|
| 19 |
-
RUN echo "VITE_GOOGLE_MAPS_API_KEY=${VITE_GOOGLE_MAPS_API_KEY}" > .env
|
| 20 |
-
|
| 21 |
-
# --- DEBUG: Print the contents of the .env file to confirm it was written correctly ---
|
| 22 |
-
RUN cat .env
|
| 23 |
-
|
| 24 |
RUN npm run build
|
| 25 |
|
| 26 |
# Stage 2: Final Production Image
|
|
|
|
| 2 |
# Use Node.js 20 as required by Vite and its dependencies
|
| 3 |
FROM node:20-alpine AS client-builder
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
WORKDIR /app/client
|
| 6 |
COPY client/package*.json ./
|
| 7 |
RUN npm install
|
| 8 |
|
| 9 |
COPY client/ .
|
| 10 |
# Build for production
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
RUN npm run build
|
| 12 |
|
| 13 |
# Stage 2: Final Production Image
|
client/src/pages/VisitLog.jsx
CHANGED
|
@@ -66,7 +66,6 @@ export default function VisitLog() {
|
|
| 66 |
const mapRef = useRef(null);
|
| 67 |
const markersRef = useRef([]);
|
| 68 |
const pendingFocusRef = useRef("");
|
| 69 |
-
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
|
| 70 |
|
| 71 |
const visitCount = visits.length;
|
| 72 |
const lastSeen = useMemo(() => {
|
|
@@ -160,41 +159,45 @@ export default function VisitLog() {
|
|
| 160 |
}, []);
|
| 161 |
|
| 162 |
useEffect(() => {
|
| 163 |
-
if (!apiKey) {
|
| 164 |
-
setMapError("Missing Google Maps API key.");
|
| 165 |
-
return;
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
let cancelled = false;
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
if (cancelled) return;
|
|
|
|
| 172 |
if (!mapRef.current && mapContainerRef.current) {
|
| 173 |
-
mapRef.current = new window.google.maps.Map(
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
fullscreenControl: false,
|
| 181 |
-
}
|
| 182 |
-
);
|
| 183 |
}
|
| 184 |
-
if (pendingFocusRef.current)
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
});
|
| 193 |
|
| 194 |
return () => {
|
| 195 |
cancelled = true;
|
| 196 |
};
|
| 197 |
-
}, [
|
| 198 |
|
| 199 |
useEffect(() => {
|
| 200 |
if (!mapRef.current || !window.google) {
|
|
|
|
| 66 |
const mapRef = useRef(null);
|
| 67 |
const markersRef = useRef([]);
|
| 68 |
const pendingFocusRef = useRef("");
|
|
|
|
| 69 |
|
| 70 |
const visitCount = visits.length;
|
| 71 |
const lastSeen = useMemo(() => {
|
|
|
|
| 159 |
}, []);
|
| 160 |
|
| 161 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
let cancelled = false;
|
| 163 |
+
|
| 164 |
+
const initializeMap = async () => {
|
| 165 |
+
try {
|
| 166 |
+
const configResponse = await fetch(getApiUrl("/api/config"));
|
| 167 |
+
if (!configResponse.ok) throw new Error("Failed to fetch client configuration.");
|
| 168 |
+
|
| 169 |
+
const config = await configResponse.json();
|
| 170 |
+
const apiKey = config.googleMapsApiKey;
|
| 171 |
+
|
| 172 |
+
if (!apiKey) {
|
| 173 |
+
throw new Error("Missing Google Maps API key from server configuration.");
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
await loadGoogleMaps(apiKey);
|
| 177 |
if (cancelled) return;
|
| 178 |
+
|
| 179 |
if (!mapRef.current && mapContainerRef.current) {
|
| 180 |
+
mapRef.current = new window.google.maps.Map(mapContainerRef.current, {
|
| 181 |
+
center: { lat: 20, lng: 0 },
|
| 182 |
+
zoom: 2,
|
| 183 |
+
mapTypeControl: false,
|
| 184 |
+
streetViewControl: false,
|
| 185 |
+
fullscreenControl: false,
|
| 186 |
+
});
|
|
|
|
|
|
|
|
|
|
| 187 |
}
|
| 188 |
+
if (pendingFocusRef.current) focusMapOnVisitor(pendingFocusRef.current);
|
| 189 |
+
|
| 190 |
+
} catch (err) {
|
| 191 |
+
if (!cancelled) setMapError(err.message || "Failed to initialize map.");
|
| 192 |
+
}
|
| 193 |
+
};
|
| 194 |
+
|
| 195 |
+
initializeMap();
|
|
|
|
| 196 |
|
| 197 |
return () => {
|
| 198 |
cancelled = true;
|
| 199 |
};
|
| 200 |
+
}, []);
|
| 201 |
|
| 202 |
useEffect(() => {
|
| 203 |
if (!mapRef.current || !window.google) {
|
server/main.py
CHANGED
|
@@ -5,6 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 5 |
from starlette.background import BackgroundTask
|
| 6 |
from starlette.responses import StreamingResponse, FileResponse
|
| 7 |
from fastapi.staticfiles import StaticFiles
|
|
|
|
| 8 |
|
| 9 |
app = FastAPI()
|
| 10 |
|
|
@@ -13,6 +14,7 @@ app = FastAPI()
|
|
| 13 |
PRIVATE_SERVER_URL = os.environ.get("PRIVATE_SERVER_URL")
|
| 14 |
# The broker will read your HF_TOKEN from the Space's secrets.
|
| 15 |
HF_TOKEN = os.environ.get("HF_TOKEN")
|
|
|
|
| 16 |
|
| 17 |
if not PRIVATE_SERVER_URL:
|
| 18 |
print("WARNING: The `PRIVATE_SERVER_URL` environment variable is not set. The broker will not be able to forward requests.")
|
|
@@ -20,6 +22,9 @@ if not PRIVATE_SERVER_URL:
|
|
| 20 |
if not HF_TOKEN:
|
| 21 |
print("WARNING: The `HF_TOKEN` environment variable is not set. Requests to the private server will not be authenticated.")
|
| 22 |
|
|
|
|
|
|
|
|
|
|
| 23 |
# Read timeout from env var, default to 5 minutes (300 seconds) to accommodate slow model conversions.
|
| 24 |
BROKER_TIMEOUT = float(os.environ.get("BROKER_TIMEOUT", 300.0))
|
| 25 |
# Use a persistent client for performance (connection pooling).
|
|
@@ -62,6 +67,17 @@ async def _reverse_proxy(request: Request):
|
|
| 62 |
print(f"Broker: CRITICAL - {error_message}")
|
| 63 |
return Response(status_code=502, content=f"Bad Gateway: The broker could not connect to the private server. Details: {error_message}")
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
# Add routes to capture all API and data requests and forward them.
|
| 66 |
app.add_route("/api/{path:path}", _reverse_proxy, ["GET", "POST", "PUT", "DELETE"])
|
| 67 |
app.add_route("/data/{path:path}", _reverse_proxy, ["GET"])
|
|
|
|
| 5 |
from starlette.background import BackgroundTask
|
| 6 |
from starlette.responses import StreamingResponse, FileResponse
|
| 7 |
from fastapi.staticfiles import StaticFiles
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
|
| 10 |
app = FastAPI()
|
| 11 |
|
|
|
|
| 14 |
PRIVATE_SERVER_URL = os.environ.get("PRIVATE_SERVER_URL")
|
| 15 |
# The broker will read your HF_TOKEN from the Space's secrets.
|
| 16 |
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 17 |
+
GOOGLE_MAPS_API_KEY = os.environ.get("VITE_GOOGLE_MAPS_API_KEY")
|
| 18 |
|
| 19 |
if not PRIVATE_SERVER_URL:
|
| 20 |
print("WARNING: The `PRIVATE_SERVER_URL` environment variable is not set. The broker will not be able to forward requests.")
|
|
|
|
| 22 |
if not HF_TOKEN:
|
| 23 |
print("WARNING: The `HF_TOKEN` environment variable is not set. Requests to the private server will not be authenticated.")
|
| 24 |
|
| 25 |
+
if not GOOGLE_MAPS_API_KEY:
|
| 26 |
+
print("WARNING: The `VITE_GOOGLE_MAPS_API_KEY` environment variable is not set. The map on the visit log page will not load.")
|
| 27 |
+
|
| 28 |
# Read timeout from env var, default to 5 minutes (300 seconds) to accommodate slow model conversions.
|
| 29 |
BROKER_TIMEOUT = float(os.environ.get("BROKER_TIMEOUT", 300.0))
|
| 30 |
# Use a persistent client for performance (connection pooling).
|
|
|
|
| 67 |
print(f"Broker: CRITICAL - {error_message}")
|
| 68 |
return Response(status_code=502, content=f"Bad Gateway: The broker could not connect to the private server. Details: {error_message}")
|
| 69 |
|
| 70 |
+
# --- Client Configuration Endpoint ---
|
| 71 |
+
# This endpoint must be defined *before* the reverse proxy routes.
|
| 72 |
+
class ClientConfig(BaseModel):
|
| 73 |
+
googleMapsApiKey: str | None
|
| 74 |
+
|
| 75 |
+
@app.get("/api/config", response_model=ClientConfig)
|
| 76 |
+
async def get_client_config():
|
| 77 |
+
"""Provides public-safe configuration variables to the client at runtime."""
|
| 78 |
+
return ClientConfig(googleMapsApiKey=GOOGLE_MAPS_API_KEY)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
# Add routes to capture all API and data requests and forward them.
|
| 82 |
app.add_route("/api/{path:path}", _reverse_proxy, ["GET", "POST", "PUT", "DELETE"])
|
| 83 |
app.add_route("/data/{path:path}", _reverse_proxy, ["GET"])
|