Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ============================================================
# World Monitor — Docker Build
# https://github.com/koala73/worldmonitor
#
# Architecture:
# Stage 1 (builder) — installs deps and builds the Vite frontend
# Stage 2 (runner) — serves the static build with nginx,
# plus a Node.js sidecar that mirrors the
# 60+ Vercel Edge Functions locally
# ============================================================

# ── Stage 1: Build ──────────────────────────────────────────
FROM node:20-alpine AS builder

WORKDIR /app

# Install dependencies first (better layer caching)
COPY package*.json ./
RUN npm ci --ignore-scripts
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The npm ci --ignore-scripts flag (line 19) prevents lifecycle scripts from running during installation. While this is a security best practice for Docker builds, ensure that none of the dependencies require postinstall scripts for proper functioning. If any dependencies need setup scripts, they should be run explicitly after line 19 or the --ignore-scripts flag should be removed with appropriate security review.

Suggested change
RUN npm ci --ignore-scripts
RUN npm ci

Copilot uses AI. Check for mistakes.

# Copy source
COPY . .

# Build-time environment variables.
# Override these at build time with --build-arg:
# docker build --build-arg VITE_VARIANT=world .
#
# VITE_VARIANT options: world | tech | finance | happy
ARG VITE_VARIANT=world
ARG VITE_MAPTILER_KEY=""
ARG VITE_MAPBOX_TOKEN=""
ARG VITE_POSTHOG_KEY=""

ENV VITE_VARIANT=${VITE_VARIANT}
ENV VITE_MAPTILER_KEY=${VITE_MAPTILER_KEY}
ENV VITE_MAPBOX_TOKEN=${VITE_MAPBOX_TOKEN}
ENV VITE_POSTHOG_KEY=${VITE_POSTHOG_KEY}

# Compile the sebuf RPC handlers (api/[domain]/v1/[rpc].ts → .js)
# Required so the local API sidecar can dynamically load them at runtime
RUN npm run build:sidecar-sebuf

# Build the Vite SPA
RUN npm run build

# ── Stage 2: Runtime ─────────────────────────────────────────
# Pin to amd64 so the image runs on x86_64 cluster nodes regardless of
# the build host platform (avoids QEMU emulation for the npm build stage).
FROM --platform=linux/amd64 node:20-alpine AS runner
Comment on lines +47 to +49
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The platform is pinned to linux/amd64 in the runner stage with the comment explaining it avoids QEMU during npm build. However, the builder stage (lines 12-44) is not platform-pinned, which means the builder stage will use the host platform. If cross-compilation is needed (e.g., building on Apple Silicon for linux/amd64), consider also pinning the builder stage to --platform=linux/amd64, or clarify that native builds are expected.

Copilot uses AI. Check for mistakes.

WORKDIR /app

# Install nginx to serve the static frontend
RUN apk add --no-cache nginx

# Copy the built frontend
COPY --from=builder /app/dist /usr/share/nginx/html

# Copy the API handlers and their dependencies
COPY --from=builder /app/api ./api
COPY --from=builder /app/server ./server
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json

# Copy the local API sidecar (repurposed from Tauri desktop sidecar)
COPY --from=builder /app/src-tauri/sidecar/local-api-server.mjs ./sidecar/local-api-server.mjs

# Copy nginx config
COPY docker/nginx.conf /etc/nginx/http.d/default.conf

# Copy the entrypoint script
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# Runtime secrets — pass these via `docker run -e` or a .env file.
# The dashboard works without most keys; missing panels simply won't appear.
# See .env.example in the repo for full descriptions and registration links.
ENV NODE_ENV=production
# Port the Node.js API sidecar listens on (must match nginx proxy_pass upstream)
ENV LOCAL_API_PORT=3001

# Runtime API keys (all optional — dashboard degrades gracefully)
# See .env.example for full descriptions and registration links.
ENV GROQ_API_KEY=""
ENV OPENROUTER_API_KEY=""
ENV UPSTASH_REDIS_REST_URL=""
ENV UPSTASH_REDIS_REST_TOKEN=""
ENV FINNHUB_API_KEY=""
ENV EIA_API_KEY=""
ENV FRED_API_KEY=""
ENV WINGBITS_API_KEY=""
ENV ACLED_ACCESS_TOKEN=""
ENV CLOUDFLARE_API_TOKEN=""
ENV NASA_FIRMS_API_KEY=""
ENV AISSTREAM_API_KEY=""
ENV OPENSKY_CLIENT_ID=""
ENV OPENSKY_CLIENT_SECRET=""
ENV WS_RELAY_URL=""
ENV RELAY_SHARED_SECRET=""
Comment on lines +82 to +99
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN environment variables are missing from the Dockerfile's ENV declarations but are present in the Kubernetes Secret. For consistency and documentation purposes, these should be added to the Dockerfile's ENV section (lines 82-99) even if set to empty strings as defaults, since they're used by the API sidecar for cross-user caching (as noted in .env.example). This ensures the runtime contract is clear and complete.

Copilot uses AI. Check for mistakes.

# Expose ports:
# 80 — nginx (frontend + /api proxy)
# 3001 — Node.js API sidecar (internal; proxied by nginx)
EXPOSE 80 3001

ENTRYPOINT ["/entrypoint.sh"]
135 changes: 135 additions & 0 deletions deploy/k8s/worldmonitor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# ============================================================
# World Monitor — Kubernetes Manifests (example / template)
#
# Fill in all <PLACEHOLDER> values for your cluster before applying.
#
# Deploy:
# kubectl apply -f deploy/k8s/worldmonitor.yaml
#
# Build & push image first:
# docker buildx build --provenance=false --sbom=false \
# -t <your-registry>/worldmonitor:latest --load .
# skopeo copy \
# 'docker-daemon:<your-registry>/worldmonitor:latest' \
# 'docker://<your-registry>/worldmonitor:latest'
# ============================================================
---
# Secret — API keys injected at runtime.
# Populate values before applying, or replace with an ExternalSecret/SealedSecret.
# All keys are optional; missing ones degrade specific panels gracefully.
# See .env.example for full descriptions and registration links.
apiVersion: v1
kind: Secret
metadata:
name: worldmonitor-env
namespace: <your-namespace>
type: Opaque
stringData:
GROQ_API_KEY: ""
OPENROUTER_API_KEY: ""
UPSTASH_REDIS_REST_URL: ""
UPSTASH_REDIS_REST_TOKEN: ""
FINNHUB_API_KEY: ""
EIA_API_KEY: ""
FRED_API_KEY: ""
WINGBITS_API_KEY: ""
ACLED_ACCESS_TOKEN: ""
CLOUDFLARE_API_TOKEN: ""
NASA_FIRMS_API_KEY: ""
AISSTREAM_API_KEY: ""
OPENSKY_CLIENT_ID: ""
OPENSKY_CLIENT_SECRET: ""
WS_RELAY_URL: ""
RELAY_SHARED_SECRET: ""
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: worldmonitor
namespace: <your-namespace>
labels:
app: worldmonitor
spec:
replicas: 1
selector:
matchLabels:
app: worldmonitor
template:
metadata:
labels:
app: worldmonitor
spec:
containers:
- name: worldmonitor
image: <your-registry>/worldmonitor:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
protocol: TCP
env:
- name: NODE_ENV
value: "production"
- name: LOCAL_API_PORT
value: "3001"
# Site variant: world | tech | finance | happy
- name: VITE_VARIANT
value: "world"
envFrom:
- secretRef:
name: worldmonitor-env
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The memory limit is set to 512Mi, which may be insufficient for the Node.js runtime when handling multiple concurrent API requests, especially given the AI summarization and data processing features described in the README. The API sidecar loads dynamic handler modules and may use libraries like Transformers.js for local LLM inference. Consider increasing the memory limit to at least 1Gi for production workloads, or add documentation explaining these limits are suitable only for low-traffic deployments.

Suggested change
memory: "512Mi"
memory: "1Gi"

Copilot uses AI. Check for mistakes.
cpu: "500m"
livenessProbe:
httpGet:
path: /api/local-service-status
port: http
initialDelaySeconds: 15
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/local-service-status
port: http
initialDelaySeconds: 5
periodSeconds: 10
# Remove if your registry is public or credentials are handled differently
imagePullSecrets:
- name: <your-registry-pull-secret>
---
apiVersion: v1
kind: Service
metadata:
name: worldmonitor
namespace: <your-namespace>
spec:
selector:
app: worldmonitor
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
---
# HTTPRoute — requires Gateway API CRDs and a compatible gateway controller.
# Remove or replace with an Ingress resource if not using Gateway API.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: worldmonitor
namespace: <your-namespace>
spec:
parentRefs:
- name: <your-gateway-name>
namespace: <your-gateway-namespace>
sectionName: https
hostnames:
- "<your-hostname>"
rules:
Comment on lines +125 to +132
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTPRoute references sectionName: https (line 129) but does not configure TLS or HTTPS-specific routing rules. If the gateway is configured with TLS termination, this is correct, but if not, consider whether http should be used instead. Additionally, there's no configuration for redirecting HTTP to HTTPS, which may be desired for production deployments. Consider documenting the expected gateway TLS configuration or adding an HTTP-to-HTTPS redirect rule.

Copilot uses AI. Check for mistakes.
- backendRefs:
- name: worldmonitor
port: 80
26 changes: 26 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/sh
# World Monitor — container entrypoint
# Starts the Node.js API sidecar, then nginx in the foreground.

set -e

API_PORT="${LOCAL_API_PORT:-3001}"

echo "==> Starting World Monitor API sidecar on port ${API_PORT}..."

# The API sidecar: prefer the repurposed Tauri local-api-server (primary),
# then fall back to a compiled server/index.js (legacy), then warn.
if [ -f /app/sidecar/local-api-server.mjs ]; then
LOCAL_API_PORT="${API_PORT}" node /app/sidecar/local-api-server.mjs &
elif [ -f /app/server/index.js ]; then
node /app/server/index.js &
elif [ -f /app/server/index.ts ]; then
# Dev image fallback only — ts-node is not installed in production images
npx ts-node /app/server/index.ts &
Comment on lines +17 to +19
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using npx ts-node in the container entrypoint can cause the runtime to download and execute the latest ts-node package from the npm registry whenever it is not installed locally, without version pinning or integrity verification. If this fallback branch is hit (for example when a server/index.ts exists), a compromised or hijacked npm package could execute arbitrary code in the container and exfiltrate environment variables or other secrets. Consider removing this fallback for production images or ensuring ts-node is a pinned, vendored dependency that is invoked from node_modules instead of via a bare npx call.

Copilot uses AI. Check for mistakes.
else
echo " [warn] No API sidecar found — API calls will fall back to worldmonitor.app (cloud)."
fi

Comment on lines +7 to +23
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API sidecar is started in the background without any health check or wait mechanism before starting nginx. If the sidecar fails to start or crashes immediately, nginx will start anyway and /api/* requests will fail silently. Consider adding a brief wait loop or health check (e.g., curl http://127.0.0.1:3001/api/local-service-status with retries) after line 22 to ensure the sidecar is responding before starting nginx. This will make container startup failures more visible and debuggable.

Suggested change
API_PORT="${LOCAL_API_PORT:-3001}"
echo "==> Starting World Monitor API sidecar on port ${API_PORT}..."
# The API sidecar: prefer the repurposed Tauri local-api-server (primary),
# then fall back to a compiled server/index.js (legacy), then warn.
if [ -f /app/sidecar/local-api-server.mjs ]; then
LOCAL_API_PORT="${API_PORT}" node /app/sidecar/local-api-server.mjs &
elif [ -f /app/server/index.js ]; then
node /app/server/index.js &
elif [ -f /app/server/index.ts ]; then
# Dev image fallback only — ts-node is not installed in production images
npx ts-node /app/server/index.ts &
else
echo " [warn] No API sidecar found — API calls will fall back to worldmonitor.app (cloud)."
fi
API_PORT="${LOCAL_API_PORT:-3001}"
SIDECAR_STARTED=0
echo "==> Starting World Monitor API sidecar on port ${API_PORT}..."
# The API sidecar: prefer the repurposed Tauri local-api-server (primary),
# then fall back to a compiled server/index.js (legacy), then warn.
if [ -f /app/sidecar/local-api-server.mjs ]; then
SIDECAR_STARTED=1
LOCAL_API_PORT="${API_PORT}" node /app/sidecar/local-api-server.mjs &
elif [ -f /app/server/index.js ]; then
SIDECAR_STARTED=1
node /app/server/index.js &
elif [ -f /app/server/index.ts ]; then
# Dev image fallback only — ts-node is not installed in production images
SIDECAR_STARTED=1
npx ts-node /app/server/index.ts &
else
echo " [warn] No API sidecar found — API calls will fall back to worldmonitor.app (cloud)."
fi
if [ "$SIDECAR_STARTED" -eq 1 ]; then
echo "==> Waiting for API sidecar to become healthy..."
ATTEMPTS=0
MAX_ATTEMPTS=30
until curl -fsS "http://127.0.0.1:${API_PORT}/api/local-service-status" >/dev/null 2>&1; do
ATTEMPTS=$((ATTEMPTS + 1))
if [ "$ATTEMPTS" -ge "$MAX_ATTEMPTS" ]; then
echo " [error] API sidecar failed to start or become healthy; exiting."
exit 1
fi
sleep 1
done
echo " [ok] API sidecar is responding."
fi

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +23
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback checks for non-existent files. According to the Dockerfile, only local-api-server.mjs is copied to /app/sidecar/, and there are no /app/server/index.js or /app/server/index.ts files built or copied. The server directory in the repo contains only TypeScript modules that are not compiled to a standalone server entry point. These fallback branches will never execute successfully and should be removed to avoid confusion.

Suggested change
# The API sidecar: prefer the repurposed Tauri local-api-server (primary),
# then fall back to a compiled server/index.js (legacy), then warn.
if [ -f /app/sidecar/local-api-server.mjs ]; then
LOCAL_API_PORT="${API_PORT}" node /app/sidecar/local-api-server.mjs &
elif [ -f /app/server/index.js ]; then
node /app/server/index.js &
elif [ -f /app/server/index.ts ]; then
# Dev image fallback only — ts-node is not installed in production images
npx ts-node /app/server/index.ts &
else
echo " [warn] No API sidecar found — API calls will fall back to worldmonitor.app (cloud)."
fi
# The API sidecar: use the repurposed Tauri local-api-server when present,
# otherwise warn and allow API calls to fall back to worldmonitor.app (cloud).
if [ -f /app/sidecar/local-api-server.mjs ]; then
LOCAL_API_PORT="${API_PORT}" node /app/sidecar/local-api-server.mjs &
else
echo " [warn] No API sidecar found — API calls will fall back to worldmonitor.app (cloud)."
fi

Copilot uses AI. Check for mistakes.
echo "==> Starting nginx..."
# Remove default PID file location issue on alpine
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 25 states "Remove default PID file location issue on alpine" but nginx is started with default configuration. Alpine's nginx package doesn't have a default PID file issue when running in foreground mode with "daemon off". This comment may be misleading or outdated. If there was a specific PID file issue, the fix should be documented or the nginx.conf should explicitly set the pid directive. Consider removing or clarifying this comment.

Suggested change
# Remove default PID file location issue on alpine
# Run nginx in the foreground (suitable for containerized environments)

Copilot uses AI. Check for mistakes.
nginx -g "daemon off;"
42 changes: 42 additions & 0 deletions docker/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;

Comment on lines +1 to +6
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nginx configuration lacks security headers that are important for production web applications. Consider adding headers like X-Content-Type-Options: nosniff, X-Frame-Options: DENY or SAMEORIGIN, and Referrer-Policy: strict-origin-when-cross-origin in a server-level add_header directive. These headers help protect against common web vulnerabilities like clickjacking and MIME type confusion.

Copilot uses AI. Check for mistakes.
# Gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gzip_types directive doesn't include application/x-javascript, text/javascript, or application/json+sebuf (if the sebuf RPC format uses a custom content-type). Modern best practice is to include text/javascript explicitly alongside application/javascript. Consider expanding the gzip_types list to: text/plain text/css text/javascript application/json application/javascript application/x-javascript text/xml application/xml image/svg+xml

Suggested change
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript text/xml application/xml image/svg+xml;

Copilot uses AI. Check for mistakes.
gzip_min_length 1000;

# Cache static assets with content hashes (Vite output)
location ~* \.(js|css|woff2?|ttf|otf|svg|png|jpg|webp|ico|wasm)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}

Comment on lines +12 to +18
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The static asset caching pattern uses expires 1y and Cache-Control "public, immutable" (lines 14-15), which is appropriate for content-hashed files. However, the regex pattern includes file types that might not always be content-hashed (svg, png, jpg, webp, ico). If any of these assets are not content-hashed by Vite, they could be cached for a year even after updates. Verify that all matched file types are indeed content-hashed in the build output, or split the caching rules into separate blocks for hashed vs non-hashed assets.

Suggested change
# Cache static assets with content hashes (Vite output)
location ~* \.(js|css|woff2?|ttf|otf|svg|png|jpg|webp|ico|wasm)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Cache static assets that include a content hash in the filename (Vite output)
# Matches files like app-abcdef12.js, style.1234abcd.css, icon-0f1e2d3c.svg, etc.
location ~* [.-][0-9a-f]{8}\.(js|css|woff2?|ttf|otf|svg|png|jpg|webp|ico|wasm)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Cache image/icon assets that might not be content-hashed with a shorter duration
location ~* \.(svg|png|jpg|webp|ico)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
try_files $uri =404;
}

Copilot uses AI. Check for mistakes.
# Proxy /api/* to the Node.js sidecar
location /api/ {
proxy_pass http://127.0.0.1:3001;
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;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proxy configuration is missing the X-Forwarded-Proto header, which is important for the Node.js backend to determine whether the original request came over HTTP or HTTPS. This is especially critical when the application is behind a load balancer or ingress controller that terminates TLS. Add the following line after line 25: proxy_set_header X-Forwarded-Proto $scheme;

Suggested change
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Copilot uses AI. Check for mistakes.
proxy_set_header Connection "";
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proxy_read_timeout is set to 30 seconds, but there's no corresponding proxy_connect_timeout or proxy_send_timeout configured. For production reliability, consider adding proxy_connect_timeout 10s and proxy_send_timeout 30s to handle connection establishment and request transmission separately from response reading. This provides better control over different phases of the proxy lifecycle.

Suggested change
proxy_set_header Connection "";
proxy_set_header Connection "";
proxy_connect_timeout 10s;
proxy_send_timeout 30s;

Copilot uses AI. Check for mistakes.
proxy_read_timeout 30s;
}

# Proxy /ingest (PostHog analytics relay)
location /ingest/ {
proxy_pass https://us.i.posthog.com/;
proxy_http_version 1.1;
proxy_set_header Host us.i.posthog.com;
proxy_ssl_server_name on;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PostHog analytics proxy at /ingest/ forwards requests to an external HTTPS endpoint (us.i.posthog.com) without configuring SSL verification or timeout settings. Consider adding proxy_ssl_verify on and a reasonable proxy_connect_timeout (e.g., 10s) for production reliability. Also consider whether you want to expose this analytics endpoint in production containers, as it could be used by third parties if the container is publicly accessible.

Suggested change
proxy_ssl_server_name on;
proxy_ssl_server_name on;
proxy_ssl_verify on;
proxy_connect_timeout 10s;

Copilot uses AI. Check for mistakes.
}

# SPA fallback — all other routes serve index.html
location / {
try_files $uri $uri/ /index.html;
}
}