refactor(web): implement nginx reverse proxy for telemetry dashboard

Transition telemetry dashboard integration from an external URL approach to a local reverse proxy via Nginx. This simplifies client-side connectivity by routing all telemetry requests through the existing web server's domain.

- Configure Nginx to proxy `/telemetry/` sub-paths to the internal telemetry service
- Implement `sub_filter` in Nginx to rewrite root-relative paths for the SPA
- Remove `VITE_TELEMETRY_URL` build arguments and environment variable dependencies
- Simplify `TelemetryPage` to use a relative `/telemetry` base path
- Clean up deployment workflows and Dockerfile by removing unused telemetry build args
This commit is contained in:
MythEclipse
2026-06-07 23:44:57 +07:00
parent 9602a64e18
commit 4dde9d8408
4 changed files with 72 additions and 46 deletions
-2
View File
@@ -11,8 +11,6 @@ COPY packages/shared packages/shared
COPY apps/web apps/web
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ARG VITE_TELEMETRY_URL
ENV VITE_TELEMETRY_URL=$VITE_TELEMETRY_URL
RUN bun run --cwd packages/shared build
RUN bun run --cwd apps/web build
+57
View File
@@ -21,6 +21,63 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
}
# ── Telemetry Dashboard Proxy (orange VPS via Tailscale) ──────────────
# Telemetry UI SPA assets (fingerprinted by Vite, cache aggressively)
location /telemetry/assets/ {
proxy_pass http://100.96.248.86:8181/assets/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Telemetry API calls (query proxy + target management)
location /telemetry/proxy/ {
proxy_pass http://100.96.248.86:8181/proxy/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_send_timeout 35s;
proxy_read_timeout 35s;
}
location /telemetry/api/ {
proxy_pass http://100.96.248.86:8181/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
}
# Telemetry SPA — proxy to orange, rewrite root-relative paths to prefix
location /telemetry/ {
proxy_pass http://100.96.248.86:8181/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_send_timeout 35s;
proxy_read_timeout 35s;
# Rewrite root-relative paths in HTML/JS to /telemetry/ prefix
sub_filter_types text/html application/javascript;
sub_filter_once off;
sub_filter 'src="/' 'src="/telemetry/';
sub_filter 'href="/' 'href="/telemetry/';
sub_filter "url('/" "url('/telemetry/";
sub_filter 'fetch("/' 'fetch("/telemetry/');
}
location / {
try_files $uri $uri/ /index.html;
}
+15 -43
View File
@@ -1,21 +1,13 @@
import { useMemo, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { BarChart3, ExternalLink, AlertTriangle } from "lucide-react";
import { BarChart3, AlertTriangle } from "lucide-react";
/**
* The Telemetry Iframe Page
*
* Options for TELEMETRY_URL (set via VITE_TELEMETRY_URL env var):
* - https://telemetry.zeavisedu.asepharyana.my.id (public, via Coolify/Traefik — BEST)
* - http://100.96.248.86:8181 (direct Tailscale IP — internal only)
*
* The public URL is accessible from anywhere and needs no Tailscale.
* The Tailscale IP only works if the browser is on the Tailscale network.
* Telemetry Dashboard — proxied through nginx at /telemetry/
* Nginx rewrites root-relative paths so the SPA works from the sub-path.
*/
const TELEMETRY_URL =
import.meta.env.VITE_TELEMETRY_URL ??
"https://telemetry.zeavisedu.asepharyana.my.id";
const TELEMETRY_BASE = "/telemetry";
const EMBED_PAGES = [
{ id: "dashboard", label: "Dashboard", path: "" },
@@ -29,21 +21,15 @@ export function TelemetryPage() {
const [iframeState, setIframeState] = useState<"loading" | "loaded" | "error">("loading");
const embedUrl = useMemo(() => {
const base = `${TELEMETRY_URL}${currentPage.path}`;
return base;
return `${TELEMETRY_BASE}${currentPage.path}`;
}, [currentPage]);
const handleIframeLoad = () => {
// Only transition to loaded if we got a real page, not an error page
try {
const iframe = document.getElementById("telemetry-iframe") as HTMLIFrameElement | null;
if (iframe?.contentWindow?.location?.href) {
setIframeState("loaded");
}
} catch {
// Cross-origin access error — means iframe loaded from different origin, which is normal
setIframeState("loaded");
}
setIframeState("loaded");
};
const handleIframeError = () => {
setIframeState("error");
};
const handleRetry = () => {
@@ -64,15 +50,6 @@ export function TelemetryPage() {
telemetry pipeline (Prometheus ClickHouse).
</p>
</div>
<a
href={embedUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-full bg-[#48A111] px-4 py-2 text-sm font-semibold text-white hover:bg-[#306D29] transition-colors"
>
<ExternalLink className="h-4 w-4" />
Open in New Tab
</a>
</div>
{/* Navigation tabs */}
@@ -119,11 +96,8 @@ export function TelemetryPage() {
Failed to load telemetry dashboard
</p>
<p className="text-sm text-muted-foreground">
Cannot reach <code className="text-xs bg-slate-200 px-1.5 py-0.5 rounded">{TELEMETRY_URL}</code>.
</p>
<p className="text-sm text-muted-foreground">
Make sure the telemetry stack is running on the orange VPS
and the domain/Tailscale IP is accessible.
The telemetry backend may be unreachable. Make sure the
telemetry stack is running on the orange VPS.
</p>
<button
onClick={handleRetry}
@@ -135,16 +109,14 @@ export function TelemetryPage() {
</div>
)}
{/* Iframe — hidden until loaded or explicitly shown */}
{/* Iframe */}
<iframe
id="telemetry-iframe"
src={embedUrl}
title="Telemetry Dashboard"
className={`w-full border-0 ${iframeState === "loaded" ? "block" : "hidden"}`}
className={`w-full border-0 ${iframeState === "loaded" || iframeState === "error" ? "block" : "hidden"}`}
style={{ height: "calc(100vh - 280px)", minHeight: "600px" }}
onLoad={handleIframeLoad}
onError={() => setIframeState("error")}
allow="same-origin"
onError={handleIframeError}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
/>
</CardContent>