Add playground backend and proxy

This commit is contained in:
yaoyao-open-source committed 2026-09-10 17:45:46 -07:00
1 parent bac31b44c1
commit 70be3c37e5
43 files changed
+3140 -4

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
.git
.gitignore
certs/*.crt
certs/*.key
*.md
+49
View File
@@ -0,0 +1,49 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM nginx:1.27-alpine
# Install openssl for auto-generating self-signed dev certificates and curl for healthchecks
RUN apk add --no-cache openssl curl bash
# Create directory structure
RUN mkdir -p /etc/nginx/conf.d \
/etc/nginx/includes \
/etc/nginx/certs \
/usr/share/nginx/html
# Copy main Nginx configuration
COPY conf/nginx.conf /etc/nginx/nginx.conf
# Clear base image default configs and copy custom configs
RUN rm -rf /etc/nginx/conf.d/*
COPY conf/conf.d/ /etc/nginx/conf.d/
COPY conf/includes/ /etc/nginx/includes/
# Copy custom error templates and fallback static files
COPY html/ /usr/share/nginx/html/
# Copy entrypoint script
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Expose standard HTTP and HTTPS ports
EXPOSE 80 443
# Healthcheck to ensure Nginx is responding
HEALTHCHECK --interval=15s --timeout=3s --start-period=5s --retries=3 \
CMD curl -k -f https://localhost/healthz || curl -f http://localhost:80/healthz || exit 1
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
+127
View File
@@ -0,0 +1,127 @@
# 🌐 Artemis Ingress Reverse Proxy Container
Production-ready, high-performance Nginx Reverse Proxy container designed for running Artemis on a **Container-Optimized OS (COS)** VM in Google Cloud.
---
## 📌 Architecture & Routing Matrix
The proxy acts as the single TLS/SSL termination gateway on standard ports (`80` $\rightarrow$ `443`), dynamically routing traffic to persistent platform services and on-demand ephemeral Artemis session containers without needing Nginx reload or restart:
| Path Prefix | Destination Upstream | Protocol | Purpose |
| :--- | :--- | :--- | :--- |
| `GET /healthz` | Nginx Internal | HTTP/HTTPS | Ingress health check probe |
| `/` | `http://backend:8000/` | HTTP/1.1 | Static frontend Single Page Application (React/Vue/Angular) |
| `/api/*` | `http://backend:8000/api/*` | HTTP/1.1 / WS | Management API, OTP auth verification, session lifecycle |
| `/session/{id}/artemis/stream/*` | `http://artemis-session-{id}:8080/stream/*` | **SSE / WebSocket** | Real-time agent thought stream (zero buffering, 1h timeout) |
| `/session/{id}/artemis/*` | `http://artemis-session-{id}:8080/*` | HTTP/1.1 / WS | Artemis REST API and task execution endpoints |
| `/session/{id}/cuttlefish/*` | `http://host.docker.internal:8443/*` | **WebRTC / WS** | Cuttlefish WebRTC signaling & interactive device controls |
---
## 📁 File Structure
```
playground/nginx_container/
├── Dockerfile # Nginx alpine image with openssl and health checks
├── docker-entrypoint.sh # Auto-generates dev certificates & tests nginx syntax
├── docker-compose.yml # Compose specification with artemis-net bridge network
├── .dockerignore
├── README.md # Detailed operational documentation
├── certs/
│ ├── generate_dev_certs.sh # Manual SSL certificate generation helper
│ └── .gitkeep
├── conf/
│ ├── nginx.conf # Master Nginx configuration (JSON logging, WebSocket map)
│ ├── conf.d/
│ │ ├── http_redirect.conf # Port 80 redirect to Port 443 + ACME challenge
│ │ └── artemis_ssl.conf # Port 443 SSL server block & dynamic Docker DNS routing
│ └── includes/
│ ├── proxy_headers.conf # Standard forwarding headers (Host, X-Real-IP, X-Forwarded-*)
│ ├── websocket_params.conf# WebSocket upgrade headers & 3600s timeouts
│ └── sse_params.conf # SSE streaming parameters (proxy_buffering off)
└── html/
├── index.html # Fallback landing page
├── 502.json # JSON error response for provisioning containers
└── 504.json # JSON error response for timeouts
```
---
## 🚀 Quick Start
### 1. Build the Docker Image
```bash
docker build -t artemis-nginx-proxy:latest .
```
### 2. Run with Docker Compose
```bash
docker-compose up -d
```
### 3. Run Standalone on COS VM
```bash
# Create shared bridge network if not exists
docker network create artemis-net || true
# Run Nginx Reverse Proxy Container
docker run -d \
--name artemis-nginx-proxy \
--restart unless-stopped \
--network artemis-net \
--add-host host.docker.internal:host-gateway \
-p 80:80 \
-p 443:443 \
-v $(pwd)/certs:/etc/nginx/certs:ro \
artemis-nginx-proxy:latest
```
---
## 🔑 Key Features & Technical Highlights
### 1. Dynamic Zero-Reload Upstream Resolution
When a user launches a new session, the backend starts a new Docker container named `artemis-session-<session_id>` on the `artemis-net` network.
Using Docker's embedded DNS server (`127.0.0.11 valid=5s`) combined with variable-based `proxy_pass`:
```nginx
resolver 127.0.0.11 valid=5s ipv6=off;
location ~ ^/session/(?<session_id>[a-zA-Z0-9_-]+)/artemis/(?<artemis_path>.*)$ {
set $artemis_host "artemis-session-$session_id";
proxy_pass http://$artemis_host:8080/$artemis_path$is_args$args;
}
```
Nginx resolves the container hostname at **request-time**, allowing session containers to be created and destroyed without touching or reloading Nginx configuration.
### 2. Real-Time Streaming (SSE & WebSocket)
- **SSE Streams** (`/stream/*`): Uses `proxy_buffering off;`, `proxy_cache off;`, and `chunked_transfer_encoding on;` so the browser receives agent reasoning tokens with zero buffering delay.
- **WebRTC Signaling & WebSockets**: Employs `map $http_upgrade $connection_upgrade` with 1-hour idle timeouts (`proxy_read_timeout 3600s;`).
### 3. Automated SSL/TLS Management
- **Development/Staging**: The container entrypoint automatically generates a self-signed certificate if no certificates exist in `/etc/nginx/certs/`.
- **Production**: Mount your Let's Encrypt or GCP managed certificates to `/etc/nginx/certs/tls.crt` and `/etc/nginx/certs/tls.key`.
### 4. Resilient Error Handling
If a requested session container is still booting or unavailable, Nginx returns a clean JSON error response (`502.json` / `504.json`) instead of standard Nginx HTML error pages, enabling the frontend UI to gracefully poll or display a loading spinner.
---
## 🧪 Verification & Testing
1. **Verify Health Endpoint**:
```bash
curl -k https://localhost/healthz
# Output: OK
```
2. **Test Dynamic Routing**:
```bash
# Launch a mock session container
docker run -d --name artemis-session-test123 --network artemis-net hashicorp/http-echo -text="Hello from Session test123" -listen=:8080
# Test via Nginx proxy
curl -k https://localhost/session/test123/artemis/
# Output: Hello from Session test123
```
@@ -0,0 +1 @@
# Keep certs directory in repository while ignoring .crt and .key files
@@ -0,0 +1,35 @@
#!/bin/bash
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CERT_FILE="${DIR}/tls.crt"
KEY_FILE="${DIR}/tls.key"
echo "Generating self-signed SSL certificates for development..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout "${KEY_FILE}" \
-out "${CERT_FILE}" \
-subj "/CN=localhost/O=Artemis Development/C=US" \
-addext "subjectAltName=DNS:localhost,DNS:*.localhost,IP:127.0.0.1"
chmod 600 "${KEY_FILE}"
chmod 644 "${CERT_FILE}"
echo "Development certificates generated:"
echo " Private Key: ${KEY_FILE}"
echo " Certificate: ${CERT_FILE}"
@@ -0,0 +1,118 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
http2 on;
server_name _;
# TLS / SSL Configuration
ssl_certificate /etc/nginx/certs/tls.crt;
ssl_certificate_key /etc/nginx/certs/tls.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# Security Headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Embedded Docker DNS Resolver for dynamic container discovery
# 127.0.0.11 is Docker's internal DNS server
resolver 127.0.0.11 valid=5s ipv6=off;
resolver_timeout 5s;
# Health check endpoint
location = /healthz {
access_log off;
default_type text/plain;
return 200 "OK\n";
}
# --------------------------------------------------------------------------
# 1. Backend Core Management API (Auth, OTP, Session Lifecycle)
# --------------------------------------------------------------------------
location /api/ {
set $backend_upstream "backend:8000";
proxy_pass http://$backend_upstream;
include /etc/nginx/includes/proxy_headers.conf;
include /etc/nginx/includes/websocket_params.conf;
}
# --------------------------------------------------------------------------
# 2. Dynamic Artemis Session Containers: Real-time Trace & Event Stream (SSE/WS)
# Target container: artemis-session-<session_id>:8080
# --------------------------------------------------------------------------
location ~ ^/session/(?<session_id>[a-zA-Z0-9_-]+)/artemis/stream/(?<stream_path>.*)$ {
set $artemis_host "artemis-session-$session_id";
proxy_pass http://$artemis_host:8080/stream/$stream_path$is_args$args;
include /etc/nginx/includes/sse_params.conf;
include /etc/nginx/includes/websocket_params.conf;
}
# --------------------------------------------------------------------------
# 3. Dynamic Artemis Session Containers: REST APIs & Admin Console
# Target container: artemis-session-<session_id>:8080
# --------------------------------------------------------------------------
location ~ ^/session/(?<session_id>[a-zA-Z0-9_-]+)/artemis/(?<artemis_path>.*)$ {
set $artemis_host "artemis-session-$session_id";
proxy_pass http://$artemis_host:8080/$artemis_path$is_args$args;
include /etc/nginx/includes/proxy_headers.conf;
include /etc/nginx/includes/websocket_params.conf;
}
# --------------------------------------------------------------------------
# 4. Cuttlefish Emulator WebRTC Signaling & Stream Proxy
# Proxies signaling requests to the Cloud Orchestrator / CVD daemon
# --------------------------------------------------------------------------
location ~ ^/session/(?<session_id>[a-zA-Z0-9_-]+)/cuttlefish/(?<cf_path>.*)$ {
# Host gateway communication or cloud-orchestrator container
set $cuttlefish_upstream "host.docker.internal:8443";
proxy_pass http://$cuttlefish_upstream/$cf_path$is_args$args;
include /etc/nginx/includes/proxy_headers.conf;
include /etc/nginx/includes/websocket_params.conf;
}
# --------------------------------------------------------------------------
# 5. Static Frontend Single Page Application (Served via Backend or Static Dist)
# --------------------------------------------------------------------------
location / {
set $backend_upstream "backend:8000";
proxy_pass http://$backend_upstream;
include /etc/nginx/includes/proxy_headers.conf;
}
# --------------------------------------------------------------------------
# 6. Custom JSON Error Pages for Ephemeral Container Boot & Teardown States
# --------------------------------------------------------------------------
error_page 502 /502.json;
error_page 504 /504.json;
location = /502.json {
root /usr/share/nginx/html;
internal;
default_type application/json;
}
location = /504.json {
root /usr/share/nginx/html;
internal;
default_type application/json;
}
}
@@ -0,0 +1,37 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
# Health check endpoint over plain HTTP
location = /healthz {
access_log off;
default_type text/plain;
return 200 "OK\n";
}
# ACME Challenge endpoint for automated Let's Encrypt SSL renewal
location /.well-known/acme-challenge/ {
root /var/www/certbot;
allow all;
}
# Enforce HTTPS redirect for all incoming web traffic
location / {
return 301 https://$host$request_uri;
}
}
@@ -0,0 +1,25 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Buffer tuning for standard API calls
proxy_buffers 8 64k;
proxy_buffer_size 128k;
proxy_busy_buffers_size 128k;
@@ -0,0 +1,18 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Critical directives for real-time Server-Sent Events (SSE) streaming
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
@@ -0,0 +1,16 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
@@ -0,0 +1,92 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# High-performance event I/O settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# Client payload size (allows large artifact, image, APK uploads)
client_max_body_size 100M;
client_body_buffer_size 128k;
# Enhanced JSON-structured access logging for easy parsing & cloud monitoring
log_format artemis_json escape=json '{'
'"time_local":"$time_local",'
'"remote_addr":"$remote_addr",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"http_referrer":"$http_referer",'
'"http_user_agent":"$http_user_agent"'
'}';
access_log /var/log/nginx/access.log artemis_json;
# Global proxy defaults
proxy_http_version 1.1;
proxy_connect_timeout 10s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
# Gzip compression for static assets and API payloads
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/x-javascript
application/xml
application/xml+rss
image/svg+xml;
# WebSocket Upgrade Header Mapping
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Load modular server configurations
include /etc/nginx/conf.d/*.conf;
}
@@ -0,0 +1,44 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
version: '3.8'
services:
nginx-proxy:
build:
context: .
dockerfile: Dockerfile
container_name: artemis-nginx-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
# Mount certificates from host (or let entrypoint generate dev certs)
- ./certs:/etc/nginx/certs:ro
networks:
- artemis-net
extra_hosts:
# Allows reaching Cuttlefish / Cloud Orchestrator running on the COS host
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD", "curl", "-k", "-f", "https://localhost/healthz"]
interval: 15s
timeout: 3s
retries: 3
networks:
artemis-net:
name: artemis-net
driver: bridge
@@ -0,0 +1,46 @@
#!/bin/bash
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -e
CERT_DIR="/etc/nginx/certs"
CERT_FILE="${CERT_DIR}/tls.crt"
KEY_FILE="${CERT_DIR}/tls.key"
echo "[Nginx Entrypoint] Initializing Artemis Ingress Proxy..."
# Check if SSL certificates exist; if not, auto-generate a self-signed fallback cert
if [ ! -f "$CERT_FILE" ] || [ ! -f "$KEY_FILE" ]; then
echo "[Nginx Entrypoint] No SSL certificate found at ${CERT_FILE}. Generating self-signed certificate for development/staging..."
mkdir -p "${CERT_DIR}"
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout "${KEY_FILE}" \
-out "${CERT_FILE}" \
-subj "/CN=localhost/O=Artemis/C=US" \
-addext "subjectAltName=DNS:localhost,DNS:*.localhost,IP:127.0.0.1" \
2>/dev/null
chmod 600 "${KEY_FILE}"
chmod 644 "${CERT_FILE}"
echo "[Nginx Entrypoint] Self-signed certificate generated successfully."
else
echo "[Nginx Entrypoint] SSL certificate verified at ${CERT_FILE}."
fi
# Test Nginx configuration validity before starting
echo "[Nginx Entrypoint] Verifying Nginx configuration syntax..."
nginx -t
echo "[Nginx Entrypoint] Starting Nginx daemon..."
exec "$@"
+6
View File
@@ -0,0 +1,6 @@
{
"status": 502,
"error": "bad_gateway",
"message": "The requested Artemis session container or Cuttlefish device is provisioning, booting, or currently unreachable. Please retry shortly.",
"timestamp": "auto"
}
+6
View File
@@ -0,0 +1,6 @@
{
"status": 504,
"error": "gateway_timeout",
"message": "The upstream Artemis container or device service took too long to respond.",
"timestamp": "auto"
}
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Artemis Reverse Proxy</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #0f172a;
color: #f8fafc;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
.card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 16px;
padding: 40px;
max-width: 540px;
width: 100%;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
text-align: center;
}
h1 {
font-size: 24px;
margin-bottom: 12px;
color: #38bdf8;
}
p {
color: #94a3b8;
font-size: 15px;
line-height: 1.6;
}
.status {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
background: #064e3b;
color: #34d399;
border-radius: 9999px;
font-size: 13px;
font-weight: 600;
margin-bottom: 20px;
}
.dot {
width: 8px;
height: 8px;
background: #10b981;
border-radius: 50%;
}
</style>
</head>
<body>
<div class="card">
<div class="status">
<span class="dot"></span> Ingress Proxy Active
</div>
<h1>Artemis Cloud Platform</h1>
<p>The reverse proxy is operational and listening on ports 80 &amp; 443. Connect your backend service to begin routing user sessions and Cuttlefish devices.</p>
</div>
</body>
</html>