From c174d4af49498c1b2e737bfe5e9fd427931fea97 Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Thu, 1 Jan 2026 16:19:18 -0600 Subject: [PATCH] feat(demo): add read-only demo dockerfile and nginx api mocking. --- Dockerfile.demo | 30 +++++++ docker-compose.demo.yml | 29 +++++++ nginx.demo.conf | 168 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 Dockerfile.demo create mode 100644 docker-compose.demo.yml create mode 100644 nginx.demo.conf diff --git a/Dockerfile.demo b/Dockerfile.demo new file mode 100644 index 0000000..9221ca8 --- /dev/null +++ b/Dockerfile.demo @@ -0,0 +1,30 @@ +# Build arguments +ARG NODE_VERSION=20 +ARG NODE_ALPINE_SHA256=sha256:6a91081a440be0b57336fbc4ee87f3dab1a2fd6f80cdb355dcf960e13bda3b59 + +# Build the frontend +FROM node:${NODE_VERSION}-alpine@${NODE_ALPINE_SHA256} AS build-frontend + +WORKDIR /src + +COPY package.json vite.config.js pnpm-lock.yaml tailwind.config.js postcss.config.js ./ +COPY meshchatx ./meshchatx + +RUN corepack enable && corepack prepare pnpm@latest --activate + +RUN pnpm install + +RUN pnpm run build-frontend + +RUN find /src/meshchatx/public -type d -exec chmod 755 {} + && \ + find /src/meshchatx/public -type f -exec chmod 644 {} + + +# Runtime stage +FROM nginxinc/nginx-unprivileged:alpine + +COPY --from=build-frontend --chown=101:101 /src/meshchatx/public /usr/share/nginx/html +COPY nginx.demo.conf /etc/nginx/conf.d/default.conf + +EXPOSE 8080 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml new file mode 100644 index 0000000..6396ac7 --- /dev/null +++ b/docker-compose.demo.yml @@ -0,0 +1,29 @@ +services: + meshchatx-demo: + build: + context: . + dockerfile: Dockerfile.demo + image: ${MESHCHAT_DEMO_IMAGE:-reticulum-meshchatx-demo:local} + container_name: reticulum-meshchatx-demo + restart: unless-stopped + ports: + - "8080:8080" + + # Security Hardening + security_opt: + - no-new-privileges:true + read_only: true + tmpfs: + - /tmp:mode=1777 + - /var/cache/nginx:mode=1777 + - /var/run:mode=1777 + cap_drop: + - ALL + + # Resource Limits + deploy: + resources: + limits: + memory: 128M + reservations: + memory: 64M diff --git a/nginx.demo.conf b/nginx.demo.conf new file mode 100644 index 0000000..76c8c52 --- /dev/null +++ b/nginx.demo.conf @@ -0,0 +1,168 @@ +server { + listen 8080; + server_name localhost; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + root /usr/share/nginx/html; + index index.html; + + # Support for SPA routing + location / { + try_files $uri $uri/ /index.html; + } + + # Don't serve index.html for missing assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|wasm|json)$ { + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # Handle manifest.json specifically if needed + location = /manifest.json { + try_files $uri =404; + } + + # Mock API responses for demo mode + location /api/v1/auth/status { + return 200 '{"auth_enabled": false, "authenticated": true}'; + add_header Content-Type application/json; + } + + location /api/v1/config { + return 200 '{"config": {"theme": "dark", "language": "en", "display_name": "Demo User", "identity_hash": "effe95b0482d7b6c04844977d17e0c36", "lxmf_address_hash": "c0ffee00000000000000000000000000", "auto_announce_interval_seconds": 0, "lxmf_user_icon_name": "account", "lxmf_user_icon_foreground_colour": "#ffffff", "lxmf_user_icon_background_colour": "#000000"}}'; + add_header Content-Type application/json; + } + + location /api/v1/app/info { + return 200 '{"app_info": {"version": "demo", "rns_version": "0.0.0", "lxmf_version": "0.0.0", "python_version": "3.11", "is_connected_to_shared_instance": false, "is_transport_enabled": false, "reticulum_config_path": "/demo/rns.conf", "database_path": "/demo/meshchat.db", "database_file_size": 1048576, "is_demo": true, "memory_usage": {"rss": 52428800, "vms": 104857600}, "network_stats": {"bytes_sent": 102400, "bytes_recv": 204800, "packets_sent": 150, "packets_recv": 300}, "reticulum_stats": {"total_paths": 42, "announces_per_second": 0.1, "announces_per_minute": 6, "announces_per_hour": 360}}}'; + add_header Content-Type application/json; + } + + location /api/v1/database/health { + return 200 '{"database": {"quick_check": "ok", "journal_mode": "wal", "wal_autocheckpoint": 1000, "page_size": 4096, "page_count": 256, "freelist_pages": 10, "estimated_free_bytes": 40960}}'; + add_header Content-Type application/json; + } + + location /api/v1/reticulum/interfaces { + return 200 '{"interfaces": { + "RNode-LoRa-V3": {"type": "RNodeInterface", "enabled": "true", "port": "/dev/ttyUSB0", "frequency": 868000000, "bandwidth": 125000, "spreadingfactor": 7, "codingrate": 5, "txpower": 7}, + "Internet": {"type": "TCPClientInterface", "enabled": "true", "target_host": "hub.reticulum.network", "target_port": 4242}, + "I2C-Bus-0": {"type": "I2CInterface", "enabled": "false"}, + "LAN Mesh": {"type": "UDPInterface", "enabled": "true", "listen_ip": "0.0.0.0", "listen_port": 4242, "forward_ip": "255.255.255.255", "forward_port": 4242} + }}'; + add_header Content-Type application/json; + } + + location /api/v1/interface-stats { + return 200 '{"interface_stats": {"interfaces": [ + {"short_name": "RNode-LoRa-V3", "name": "AutoInterface[RNode]", "status": true, "bitrate": 1200, "txb": 1024, "rxb": 5120}, + {"short_name": "Internet", "name": "AutoInterface[TCP]", "status": true, "bitrate": 10000000, "txb": 45678, "rxb": 987654}, + {"short_name": "I2C-Bus-0", "name": "AutoInterface[I2C]", "status": false, "bitrate": 400000, "txb": 0, "rxb": 0}, + {"short_name": "LAN Mesh", "name": "AutoInterface[UDP]", "status": true, "bitrate": 100000000, "txb": 12345, "rxb": 67890} + ]}}'; + add_header Content-Type application/json; + } + + location /api/v1/lxmf/conversations { + return 200 '{"conversations": [ + {"destination_hash": "8b5cf6", "display_name": "Alice", "updated_at": "2026-01-01T12:00:00Z", "latest_message_preview": "Hello from the mesh!", "unread_count": 1, "lxmf_user_icon": {"icon_name": "account", "foreground_colour": "#ffffff", "background_colour": "#3b82f6"}}, + {"destination_hash": "3b82f6", "display_name": "Bob", "updated_at": "2026-01-01T11:30:00Z", "latest_message_preview": "Did you see the new RNode update?", "unread_count": 0, "lxmf_user_icon": {"icon_name": "robot", "foreground_colour": "#ffffff", "background_colour": "#ef4444"}}, + {"destination_hash": "10b981", "display_name": "Regional Hub", "updated_at": "2026-01-01T10:00:00Z", "latest_message_preview": "Status: All interfaces operational", "unread_count": 0, "lxmf_user_icon": {"icon_name": "server", "foreground_colour": "#ffffff", "background_colour": "#10b981"}} + ]}'; + add_header Content-Type application/json; + } + + location ~ ^/api/v1/lxmf-messages/conversation/(.*)$ { + return 200 '{"lxmf_messages": [ + {"hash": "msg1", "source_hash": "8b5cf6", "destination_hash": "c0ffee00000000000000000000000000", "content": "Hey there!", "timestamp": 1735732500, "created_at": "2026-01-01T11:55:00Z", "state": "delivered", "method": "propagated"}, + {"hash": "msg2", "source_hash": "8b5cf6", "destination_hash": "c0ffee00000000000000000000000000", "content": "Hello from the mesh!", "timestamp": 1735732800, "created_at": "2026-01-01T12:00:00Z", "state": "delivered", "method": "propagated"} + ]}'; + add_header Content-Type application/json; + } + + location ~ ^/api/v1/lxmf/conversations/(.*)/mark-as-read$ { + return 200 '{"success": true}'; + add_header Content-Type application/json; + } + + location /api/v1/blocked-destinations { + return 200 '{"blocked_destinations": []}'; + add_header Content-Type application/json; + } + + location /api/v1/telephone/status { + return 200 '{"active_call": null}'; + add_header Content-Type application/json; + } + + location /api/v1/lxmf/propagation-node/status { + return 200 '{"propagation_node_status": {"state": "idle", "connected": true, "address": "8b5cf6", "messages_received": 123, "messages_sent": 45}}'; + add_header Content-Type application/json; + } + + location /api/v1/announces { + return 200 '{"announces": [ + {"destination_hash": "8b5cf6", "display_name": "Alice", "aspect": "lxmf.delivery", "updated_at": "2026-01-01T12:00:00Z", "hops": 1, "snr": 12.5}, + {"destination_hash": "3b82f6", "display_name": "Bob", "aspect": "lxmf.delivery", "updated_at": "2026-01-01T11:30:00Z", "hops": 2, "snr": 8.2}, + {"destination_hash": "10b981", "display_name": "Regional Hub", "aspect": "nomadnetwork.node", "updated_at": "2026-01-01T10:00:00Z", "hops": 1, "snr": 15.0}, + {"destination_hash": "ef4444", "display_name": "Echo Bot", "aspect": "lxmf.delivery", "updated_at": "2026-01-01T09:00:00Z", "hops": 3}, + {"destination_hash": "f68b5c", "display_name": "Nomad Portal", "aspect": "nomadnetwork.node", "updated_at": "2026-01-01T12:05:00Z", "hops": 1}, + {"destination_hash": "a1b2c3", "display_name": "Charlie", "aspect": "lxmf.delivery", "updated_at": "2026-01-01T08:00:00Z", "hops": 4}, + {"destination_hash": "d4e5f6", "display_name": "Mesh Gateway", "aspect": "nomadnetwork.node", "updated_at": "2026-01-01T12:10:00Z", "hops": 1} + ], "total_count": 7}'; + add_header Content-Type application/json; + } + + location /api/v1/path-table { + return 200 '{"path_table": [ + {"hash": "8b5cf6", "hops": 1, "interface": "AutoInterface[RNode]"}, + {"hash": "3b82f6", "hops": 2, "interface": "AutoInterface[TCP]"}, + {"hash": "10b981", "hops": 1, "interface": "AutoInterface[RNode]"}, + {"hash": "ef4444", "hops": 3, "interface": "AutoInterface[TCP]"}, + {"hash": "f68b5c", "hops": 1, "interface": "AutoInterface[TCP]"}, + {"hash": "a1b2c3", "hops": 4, "interface": "AutoInterface[UDP]"}, + {"hash": "d4e5f6", "hops": 1, "interface": "AutoInterface[UDP]"} + ], "total_count": 7}'; + add_header Content-Type application/json; + } + + location /api/v1/notifications { + return 200 '{"notifications": [ + {"id": 1, "type": "new_message", "title": "New Message", "content": "Alice: Hello from the mesh!", "created_at": "2026-01-01T12:00:00Z"} + ]}'; + add_header Content-Type application/json; + } + + location /api/v1/announce { + return 200 '{"message": "Announce sent (Demo Mode)"}'; + add_header Content-Type application/json; + } + + location /api/v1/lxmf/propagation-node/sync { + return 200 '{"message": "Sync started (Demo Mode)"}'; + add_header Content-Type application/json; + } + + location /api/v1/identity/backup/base32 { + return 200 '{"identity_base32": "demo_base32_identity_key_do_not_use"}'; + add_header Content-Type application/json; + } + + # Catch-all for other API calls + location /api { + return 404 '{"error": "Endpoint not available in demo mode"}'; + add_header Content-Type application/json; + } + + location /ws { + return 503; + } + + # Error pages + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +}