Docker set up and fixes for build

This commit is contained in:
Daniel Pupius
2025-05-01 15:34:16 -07:00
parent 68fc353673
commit e9fa0104e3
27 changed files with 7035 additions and 151 deletions

25
.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
# Version control
.git
.gitignore
# Build artifacts
dist
bin
logs
node_modules
web/node_modules
web/dist
# Editor/IDE files
.vscode
.idea
*.swp
*.swo
# Temporary files
*.log
tmp
# Documentation
*.md
LICENSE

View File

@@ -1,12 +1,45 @@
# MQTT Broker Configuration
MQTT_BROKER=mqtt.bayme.sh
MQTT_PORT=1883
MQTT_USERNAME=meshdev
MQTT_PASSWORD=large4cats
# Environment variables for docker-compose
# Copy this file to .env and customize with your own values
# Topic Configuration
MQTT_TOPIC_PREFIX=msh/US/CA/Motherlode
###########################################
# Web app variables (BUILD-TIME ONLY)
# These must be set at build time and are
# compiled into the static HTML/JS files
###########################################
# Development setup - enables more detailed logging and developer features
MESHSTREAM_APP_ENV=development
# MESHSTREAM_APP_ENV=production # Uncomment for production build
# Application Settings
# Set to "true" to enable debug logging
DEBUG=false
# Site customization
MESHSTREAM_SITE_TITLE=Bay Area Mesh - Dev
MESHSTREAM_SITE_DESCRIPTION=Development instance - Meshtastic activity in Bay Area region
# For local development, point to local server
# (empty for production where API is on same domain)
MESHSTREAM_API_BASE_URL=http://localhost:8080
# Google Maps API configuration - required for maps to work
# Get keys at: https://developers.google.com/maps/documentation/javascript/get-api-key
MESHSTREAM_GOOGLE_MAPS_ID=your mapid
MESHSTREAM_GOOGLE_MAPS_API_KEY=your api
# IMPORTANT: To change these values after building, you must rebuild the image
###########################################
# Runtime environment variables
###########################################
# MQTT connection settings
MESHSTREAM_MQTT_BROKER=mqtt.bayme.sh
MESHSTREAM_MQTT_USERNAME=meshdev
MESHSTREAM_MQTT_PASSWORD=large4cats
# Topic to monitor - customize for your region
MESHSTREAM_MQTT_TOPIC_PREFIX=msh/US/bayarea
# Server configuration
MESHSTREAM_SERVER_HOST=0.0.0.0 # Listen on all interfaces
MESHSTREAM_SERVER_PORT=8080 # Standard web port
MESHSTREAM_STATIC_DIR=/app/static
# Logging and debugging
MESHSTREAM_LOG_LEVEL=debug # Options: debug, info, warn, error
MESHSTREAM_VERBOSE_LOGGING=true # Set to false in production
MESHSTREAM_CACHE_SIZE=1000 # Number of packets to cache for new subscribers

119
Dockerfile Normal file
View File

@@ -0,0 +1,119 @@
# Multi-stage build for Meshstream
###############################################################################
# Stage 1: Build the web application
###############################################################################
FROM node:20-alpine AS web-builder
WORKDIR /app/web
# Install pnpm globally
RUN npm install -g pnpm@latest
# Copy web app package files
COPY web/package.json web/pnpm-lock.yaml ./
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy the rest of the web app source
COPY web/ ./
# Build-time environment variables for web app
ARG MESHSTREAM_API_BASE_URL=""
ARG MESHSTREAM_APP_ENV="production"
ARG MESHSTREAM_SITE_TITLE
ARG MESHSTREAM_SITE_DESCRIPTION
ARG MESHSTREAM_GOOGLE_MAPS_ID
ARG MESHSTREAM_GOOGLE_MAPS_API_KEY
# Convert MESHSTREAM_ prefixed args to VITE_ environment variables for the web build
ENV VITE_API_BASE_URL=${MESHSTREAM_API_BASE_URL} \
VITE_APP_ENV=${MESHSTREAM_APP_ENV} \
VITE_SITE_TITLE=${MESHSTREAM_SITE_TITLE} \
VITE_SITE_DESCRIPTION=${MESHSTREAM_SITE_DESCRIPTION} \
VITE_GOOGLE_MAPS_ID=${MESHSTREAM_GOOGLE_MAPS_ID} \
VITE_GOOGLE_MAPS_API_KEY=${MESHSTREAM_GOOGLE_MAPS_API_KEY}
# Build the web app
RUN pnpm build
###############################################################################
# Stage 2: Build the Go server
###############################################################################
FROM golang:1.24-alpine AS go-builder
WORKDIR /app
# Install required system dependencies
RUN apk add --no-cache git protobuf make
# Cache Go modules
COPY go.mod go.sum ./
RUN go mod download
# Copy proto files (needed for any proto generation)
COPY proto/ ./proto/
# Copy web build from previous stage
COPY --from=web-builder /app/web/dist ./dist/static
# Copy the rest of the app code
COPY . .
# Generate protocol buffer code in case it's needed
RUN make gen-proto
# Build the Go application
RUN CGO_ENABLED=0 GOOS=linux go build -o /meshstream
###############################################################################
# Stage 3: Final lightweight runtime image
###############################################################################
FROM alpine:3.19
WORKDIR /app
# Add basic runtime dependencies
RUN apk add --no-cache ca-certificates tzdata
# Create a non-root user to run the app
RUN addgroup -S meshstream && adduser -S meshstream -G meshstream
# Copy the binary from the build stage
COPY --from=go-builder /meshstream /app/meshstream
# Copy the static files
COPY --from=go-builder /app/dist/static /app/static
# Set ownership to the non-root user
RUN chown -R meshstream:meshstream /app
# Switch to the non-root user
USER meshstream
# Expose the application port
EXPOSE 8080
# Server configuration
ENV MESHSTREAM_SERVER_HOST=0.0.0.0
ENV MESHSTREAM_SERVER_PORT=8080
ENV MESHSTREAM_STATIC_DIR=/app/static
# Reporting configuration
ENV MESHSTREAM_STATS_INTERVAL=30s
ENV MESHSTREAM_CACHE_SIZE=1000
ENV MESHSTREAM_VERBOSE_LOGGING=false
# MQTT configuration
ENV MESHSTREAM_MQTT_BROKER=mqtt.bayme.sh
ENV MESHSTREAM_MQTT_USERNAME=meshdev
ENV MESHSTREAM_MQTT_PASSWORD=large4cats
ENV MESHSTREAM_MQTT_TOPIC_PREFIX=msh/US/bayarea
ENV MESHSTREAM_MQTT_CLIENT_ID=meshstream
# Note: Web app configuration is set at build time
# and baked into the static files
# Run the application
ENTRYPOINT ["/app/meshstream"]

View File

@@ -1,4 +1,4 @@
.PHONY: build run gen-proto clean tools web-run web-build web-test web-lint
.PHONY: build run gen-proto clean tools web-run web-build web-test web-lint docker-build docker-run
ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
@@ -67,4 +67,36 @@ web-test:
# Run linting for the web application
web-lint:
cd $(WEB_DIR) && pnpm lint
cd $(WEB_DIR) && pnpm lint
# Docker commands
# Build a Docker image
docker-build:
docker build \
--build-arg MESHSTREAM_API_BASE_URL=$${MESHSTREAM_API_BASE_URL:-} \
--build-arg MESHSTREAM_APP_ENV=$${MESHSTREAM_APP_ENV:-production} \
--build-arg MESHSTREAM_SITE_TITLE=$${MESHSTREAM_SITE_TITLE:-Meshstream} \
--build-arg MESHSTREAM_SITE_DESCRIPTION=$${MESHSTREAM_SITE_DESCRIPTION:-"Meshtastic activity monitoring"} \
--build-arg MESHSTREAM_GOOGLE_MAPS_ID=$${MESHSTREAM_GOOGLE_MAPS_ID:-4f089fb2d9fbb3db} \
--build-arg MESHSTREAM_GOOGLE_MAPS_API_KEY=$${MESHSTREAM_GOOGLE_MAPS_API_KEY} \
-t meshstream .
# Run Docker container with environment variables
docker-run: docker-build
docker run -p 8080:8080 \
-e MESHSTREAM_MQTT_BROKER=$${MESHSTREAM_MQTT_BROKER:-mqtt.bayme.sh} \
-e MESHSTREAM_MQTT_USERNAME=$${MESHSTREAM_MQTT_USERNAME:-meshdev} \
-e MESHSTREAM_MQTT_PASSWORD=$${MESHSTREAM_MQTT_PASSWORD:-large4cats} \
-e MESHSTREAM_MQTT_TOPIC_PREFIX=$${MESHSTREAM_MQTT_TOPIC_PREFIX:-msh/US/bayarea} \
-e MESHSTREAM_SERVER_HOST=0.0.0.0 \
-e MESHSTREAM_SERVER_PORT=$${MESHSTREAM_SERVER_PORT:-8080} \
-e MESHSTREAM_STATIC_DIR=/app/static \
-e MESHSTREAM_LOG_LEVEL=$${MESHSTREAM_LOG_LEVEL:-info} \
-e MESHSTREAM_VERBOSE_LOGGING=$${MESHSTREAM_VERBOSE_LOGGING:-false} \
-e MESHSTREAM_CACHE_SIZE=$${MESHSTREAM_CACHE_SIZE:-50} \
meshstream
# Docker compose build and run with .env support
docker-compose-up:
docker-compose up --build

View File

@@ -13,10 +13,85 @@ go mod tidy
## Running
### Using Go directly
```
go run main.go
```
### Using Make
```
make run
```
### Using Docker
The application can be built and run in Docker using the provided Dockerfile:
```
# Build the Docker image
make docker-build
# Run the Docker container
make docker-run
```
Or using Docker Compose:
```
docker-compose up
```
#### Docker Environment Variables and Secrets
The application supports two types of environment variables:
1. **Build-time variables** - Used during the web application build (via Docker build args)
2. **Runtime variables** - Used when the application is running
You can set these variables in two ways:
1. **Using a `.env` file** (recommended for development and secrets):
```
# Copy the sample file
cp .env.example .env
# Edit with your values, especially for secrets like Google Maps API key
nano .env
# Build and run with variables from .env
docker-compose up --build
```
2. **Passing variables directly** (useful for CI/CD or one-off runs):
```
# Example with custom settings
docker run -p 8080:8080 \
-e MESHSTREAM_MQTT_BROKER=your-mqtt-broker.com \
-e MESHSTREAM_MQTT_TOPIC_PREFIX=msh/YOUR/REGION \
-e MESHSTREAM_SERVER_HOST=0.0.0.0 \
-e MESHSTREAM_STATIC_DIR=/app/static \
meshstream
```
For build-time variables (like the Google Maps API key), use Docker build arguments with the MESHSTREAM_ prefix:
```
docker build \
--build-arg MESHSTREAM_GOOGLE_MAPS_API_KEY=your_api_key_here \
--build-arg MESHSTREAM_GOOGLE_MAPS_ID=your_maps_id_here \
-t meshstream .
```
**Important Notes:**
- All environment variables use the `MESHSTREAM_` prefix.
- The Dockerfile internally transforms build-time variables like `MESHSTREAM_GOOGLE_MAPS_API_KEY` to `VITE_GOOGLE_MAPS_API_KEY` for the web application build process.
- Web application configuration (site title, Google Maps API key, etc.) must be set at build time. These values are compiled into the static files and cannot be changed at runtime.
- To update web application configuration, you must rebuild the Docker image.
## Decoding Meshtastic Packets
This project includes the Meshtastic protocol buffer definitions in the `proto/` directory and a decoder for parsing MQTT packets. The application will automatically decode JSON messages and extract key information from binary messages.

43
docker-compose.yml Normal file
View File

@@ -0,0 +1,43 @@
# Create a .env file for overrides. At the very least, set the following:
#
# VITE_GOOGLE_MAPS_API_KEY=your_api_key_here
# VITE_GOOGLE_MAPS_ID=your_maps_id_here
services:
meshstream:
build:
context: .
dockerfile: Dockerfile
args:
# Web application build-time variables
- MESHSTREAM_API_BASE_URL=${MESHSTREAM_API_BASE_URL:-}
- MESHSTREAM_APP_ENV=${MESHSTREAM_APP_ENV:-production}
- MESHSTREAM_SITE_TITLE=${MESHSTREAM_SITE_TITLE:-Bay Area Mesh}
- MESHSTREAM_SITE_DESCRIPTION=${MESHSTREAM_SITE_DESCRIPTION:-Meshtastic activity in the Bay Area region, CA.}
- MESHSTREAM_GOOGLE_MAPS_ID=${MESHSTREAM_GOOGLE_MAPS_ID}
- MESHSTREAM_GOOGLE_MAPS_API_KEY=${MESHSTREAM_GOOGLE_MAPS_API_KEY}
ports:
- "8080:8080"
environment:
# Runtime configuration with defaults from .env file or inline defaults
# MQTT connection settings
- MESHSTREAM_MQTT_BROKER=${MESHSTREAM_MQTT_BROKER:-mqtt.bayme.sh}
- MESHSTREAM_MQTT_USERNAME=${MESHSTREAM_MQTT_USERNAME:-meshdev}
- MESHSTREAM_MQTT_PASSWORD=${MESHSTREAM_MQTT_PASSWORD:-large4cats}
- MESHSTREAM_MQTT_TOPIC_PREFIX=${MESHSTREAM_MQTT_TOPIC_PREFIX:-msh/US/bayarea}
# Server configuration
- MESHSTREAM_SERVER_HOST=${MESHSTREAM_SERVER_HOST:-0.0.0.0}
- MESHSTREAM_SERVER_PORT=${MESHSTREAM_SERVER_PORT:-8080}
- MESHSTREAM_STATIC_DIR=${MESHSTREAM_STATIC_DIR:-/app/static}
# Logging and debugging
- MESHSTREAM_LOG_LEVEL=${MESHSTREAM_LOG_LEVEL:-info}
- MESHSTREAM_VERBOSE_LOGGING=${MESHSTREAM_VERBOSE_LOGGING:-false}
- MESHSTREAM_CACHE_SIZE=${MESHSTREAM_CACHE_SIZE:-1000}
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/api/status"]
interval: 30s
timeout: 5s
retries: 3

File diff suppressed because it is too large Load Diff

26
main.go
View File

@@ -28,10 +28,7 @@ type Config struct {
// Web server configuration
ServerHost string
ServerPort string
// Logging configuration
LogLevel string
LogFormat string
StaticDir string
// Channel keys configuration (name:key pairs)
ChannelKeys []string
@@ -75,10 +72,7 @@ func parseConfig() *Config {
// Web server configuration
flag.StringVar(&config.ServerHost, "server-host", getEnv("SERVER_HOST", "localhost"), "Web server host")
flag.StringVar(&config.ServerPort, "server-port", getEnv("SERVER_PORT", "8080"), "Web server port")
// Logging configuration
flag.StringVar(&config.LogLevel, "log-level", getEnv("LOG_LEVEL", "info"), "Log level (debug, info, warn, error)")
flag.StringVar(&config.LogFormat, "log-format", getEnv("LOG_FORMAT", "json"), "Log format (json, console)")
flag.StringVar(&config.StaticDir, "static-dir", getEnv("STATIC_DIR", "./server/static"), "Directory containing static web files")
// Channel key configuration (comma separated list of name:key pairs)
channelKeysDefault := getEnv("CHANNEL_KEYS", "LongFast:"+decoder.DefaultPrivateKey+",ERSN:VIuMtC5uDDJtC/ojdH314HLkDIHanX4LdbK5yViV9jA=")
@@ -146,21 +140,8 @@ func boolFromEnv(key string, defaultValue bool) bool {
}
func main() {
// Parse configuration from flags and environment variables
config := parseConfig()
// Set up logging
var logger logging.Logger
// Use the production logger (JSON format)
logger = logging.NewProdLogger()
// Add main component name
logger = logger.Named("main")
// Log our configuration
logger.Infof("Logger initialized with level: %s, format: %s",
config.LogLevel, config.LogFormat)
logger := logging.NewProdLogger().Named("main")
// Initialize channel keys
for _, channelKeyPair := range config.ChannelKeys {
@@ -232,6 +213,7 @@ func main() {
Logger: logger,
MQTTServer: config.MQTTBroker,
MQTTTopicPath: config.MQTTTopicPrefix + "/#",
StaticDir: config.StaticDir,
})
// Start the server in a goroutine

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@ type Config struct {
Broker *mqtt.Broker // The MQTT message broker
MQTTServer string // MQTT server hostname
MQTTTopicPath string // MQTT topic path being subscribed to
StaticDir string // Directory containing static web files
}
// Create connection info JSON to send to the client
@@ -75,7 +76,7 @@ func (s *Server) Start() error {
prefab.WithPort(port),
prefab.WithHTTPHandlerFunc("/api/status", s.handleStatus),
prefab.WithHTTPHandlerFunc("/api/stream", s.handleStream),
prefab.WithStaticFiles("/", "./server/static"),
prefab.WithStaticFiles("/", s.config.StaticDir),
)
// Start the server

View File

@@ -27,19 +27,6 @@ const calculateZoomFromPrecisionBits = (precisionBits?: number): number => {
return Math.min(18, baseZoom + (additionalZoom / 2)); // Cap at zoom 18
};
// Function to calculate accuracy in meters from precision bits
const calculateAccuracyFromPrecisionBits = (precisionBits?: number): number => {
if (!precisionBits) return 300; // Default accuracy of 300m
// Each precision bit halves the accuracy radius
// Starting with Earth's circumference (~40075km), calculate the precision
const earthCircumference = 40075000; // in meters
const accuracy = earthCircumference / (2 ** precisionBits) / 2;
// Limit to reasonable values
return Math.max(1, Math.min(accuracy, 10000));
};
export const Map: React.FC<MapProps> = ({
latitude,
longitude,
@@ -55,11 +42,6 @@ export const Map: React.FC<MapProps> = ({
// Calculate zoom level based on precision bits if zoom is not provided
const effectiveZoom = zoom || calculateZoomFromPrecisionBits(precisionBits);
// Calculate accuracy in meters if we have precision bits
const accuracyMeters = precisionBits !== undefined
? calculateAccuracyFromPrecisionBits(precisionBits)
: undefined;
const mapUrl = getStaticMapUrl(
latitude,
longitude,
@@ -67,8 +49,7 @@ export const Map: React.FC<MapProps> = ({
width,
height,
nightMode,
precisionBits,
accuracyMeters
precisionBits
);
const googleMapsUrl = getGoogleMapsUrl(latitude, longitude);

View File

@@ -1,45 +0,0 @@
import React from "react";
import { Packet } from "../lib/types";
interface MessageDisplayProps {
message: Packet;
}
export const MessageDisplay: React.FC<MessageDisplayProps> = ({ message }) => {
const { data } = message;
const getMessageContent = () => {
if (data.text_message) {
return data.text_message;
} else if (data.position) {
return `Position: ${data.position.latitude}, ${data.position.longitude}`;
} else if (data.node_info) {
return `Node Info: ${data.node_info.longName || data.node_info.shortName}`;
} else if (data.telemetry) {
return "Telemetry data";
} else if (data.decode_error) {
return `Error: ${data.decode_error}`;
}
return "Unknown message type";
};
return (
<div className="p-4 border border-neutral-700 rounded bg-neutral-800 shadow-inner">
<div className="flex justify-between mb-2">
<span className="font-medium text-neutral-200">
From: {data.from || "Unknown"}
</span>
<span className="text-neutral-400 text-sm">
ID: {data.id || "No ID"}
</span>
</div>
<div className="mb-2 text-neutral-300">{getMessageContent()}</div>
<div className="mt-3 flex justify-between items-center">
<span className="text-xs text-neutral-500">
Channel: {message.info.channel}
</span>
<span className="text-xs text-neutral-500">Type: {data.port_num}</span>
</div>
</div>
);
};

View File

@@ -144,7 +144,7 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ
}
// Update markers and fit the map
updateNodeMarkers(nodesWithPosition, navigate);
updateNodeMarkers(nodesWithPosition);
return true;
} catch (error) {
console.error("Error initializing map:", error);
@@ -153,7 +153,7 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ
}
console.warn("Cannot initialize map - prerequisites not met");
return false;
}, [nodesWithPosition, navigate, updateNodeMarkers, initializeMap]);
}, [nodesWithPosition, updateNodeMarkers, initializeMap]);
// Check for Google Maps API loading - make sure all required objects are available
useEffect(() => {
@@ -274,7 +274,7 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ
}
// Helper function to update node markers on the map
function updateNodeMarkers(nodes: MapNode[], navigate: ReturnType<typeof useNavigate>): void {
function updateNodeMarkers(nodes: MapNode[]): void {
if (!mapInstanceRef.current) return;
// Clear the bounds for recalculation
@@ -306,7 +306,7 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ
// Create or update marker
if (!markersRef.current[key]) {
createMarker(node, position, nodeName, navigate);
createMarker(node, position, nodeName);
} else {
updateMarker(node, position);
}
@@ -330,8 +330,7 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ
function createMarker(
node: MapNode,
position: google.maps.LatLngLiteral,
nodeName: string,
navigate: ReturnType<typeof useNavigate>
nodeName: string
): void {
if (!mapInstanceRef.current || !infoWindowRef.current) return;
@@ -369,7 +368,7 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ
// Add click listener to show info window
marker.addListener('gmp-click', () => {
showInfoWindow(node, marker, navigate);
showInfoWindow(node, marker);
});
markersRef.current[key] = marker;
@@ -412,8 +411,7 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ
// Show info window for a node
function showInfoWindow(
node: MapNode,
marker: google.maps.marker.AdvancedMarkerElement,
navigate: ReturnType<typeof useNavigate>
marker: google.maps.marker.AdvancedMarkerElement
): void {
if (!infoWindowRef.current || !mapInstanceRef.current) return;
@@ -445,8 +443,7 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ
<div style="font-size: 12px; margin-bottom: 8px; color: #333;">
Packets: ${node.messageCount || 0} · Text: ${node.textMessageCount || 0}
</div>
<a href="javascript:void(0);"
id="view-node-${node.id}"
<a href="/node/${node.id.toString(16)}"
style="font-size: 13px; color: #3b82f6; text-decoration: none; font-weight: 500; display: inline-block; padding: 4px 8px; background-color: #f1f5f9; border-radius: 4px;">
View details →
</a>
@@ -455,16 +452,6 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ
infoWindowRef.current.setContent(infoContent);
infoWindowRef.current.open(mapInstanceRef.current, marker);
// Add listener for the "View details" link with a delay to allow DOM to update
setTimeout(() => {
const link = document.getElementById(`view-node-${node.id}`);
if (link) {
link.addEventListener('gmp-click', () => {
navigate({ to: `/node/$nodeId`, params: { nodeId: node.id.toString(16) } });
});
}
}, 100);
}
// Prepare the styling for the map container

View File

@@ -27,7 +27,6 @@ import { Separator } from "../Separator";
import { KeyValuePair } from "../ui/KeyValuePair";
import { Section } from "../ui/Section";
import { BatteryLevel } from "./BatteryLevel";
import { NetworkStrength } from "./NetworkStrength";
import { GoogleMap } from "./GoogleMap";
import { NodePositionData } from "./NodePositionData";
import { EnvironmentMetrics } from "./EnvironmentMetrics";

View File

@@ -1,5 +1,4 @@
export * from './PacketList';
export * from './MessageDisplay';
export * from './PacketDetails';
export * from './Filter';
export * from './InfoMessage';

View File

@@ -43,7 +43,6 @@ export const DeviceMetricsPacket: React.FC<DeviceMetricsPacketProps> = ({
icon={<Gauge />}
iconBgColor="bg-amber-500"
label="Device Telemetry"
backgroundColor="bg-amber-950/5"
>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">

View File

@@ -121,7 +121,6 @@ export const EnvironmentMetricsPacket: React.FC<
icon={<Thermometer />}
iconBgColor="bg-emerald-700"
label="Environment Telemetry"
backgroundColor="bg-green-950/5"
>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">

View File

@@ -20,7 +20,6 @@ export const ErrorPacket: React.FC<ErrorPacketProps> = ({ packet }) => {
icon={<AlertTriangle />}
iconBgColor="bg-red-500"
label="Error"
backgroundColor="bg-red-950/5"
>
<div className="max-w-md">
<div className="text-red-400 mb-2 font-medium">

View File

@@ -46,7 +46,6 @@ export const GenericPacket: React.FC<GenericPacketProps> = ({ packet }) => {
icon={<Package />}
iconBgColor="bg-slate-500"
label={portName.replace("_APP", "")}
backgroundColor="bg-slate-950/5"
>
<div className="max-w-md">
<KeyValueGrid>

View File

@@ -21,7 +21,6 @@ export const NodeInfoPacket: React.FC<NodeInfoPacketProps> = ({ packet }) => {
icon={<User />}
iconBgColor="bg-purple-500"
label="Node Info"
backgroundColor="bg-purple-950/5"
>
<div className="flex flex-col gap-1.5">
{/* First row: Long name and short name */}

View File

@@ -35,7 +35,7 @@ export const PacketCard: React.FC<PacketCardProps> = ({
>
{React.cloneElement(icon as React.ReactElement, {
className: "h-3.5 w-3.5 text-white",
})}
} as React.HTMLAttributes<HTMLElement>)}
</div>
{data.from ? (
<Link

View File

@@ -57,7 +57,6 @@ export const TelemetryPacket: React.FC<TelemetryPacketProps> = ({ packet }) => {
icon={<BarChart />}
iconBgColor="bg-neutral-500"
label="Unknown Telemetry"
backgroundColor="bg-neutral-950/5"
>
<div className="text-neutral-400 text-sm">
Unknown telemetry data received at{' '}

View File

@@ -16,7 +16,6 @@ export const TextMessagePacket: React.FC<TextMessagePacketProps> = ({ packet })
icon={<MessageSquareText />}
iconBgColor="bg-blue-500"
label="Text Message"
backgroundColor="bg-blue-950/5"
>
<div className="max-w-lg bg-neutral-800/30 p-3 rounded-md tracking-tight break-words">
{data.textMessage || "Empty message"}

View File

@@ -40,7 +40,6 @@ export const WaypointPacket: React.FC<WaypointPacketProps> = ({ packet }) => {
icon={<MapPin />}
iconBgColor="bg-violet-500"
label="Waypoint"
backgroundColor="bg-violet-950/5"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">

View File

@@ -1,7 +1,7 @@
/**
* API client functions for interacting with the Meshstream server
*/
import { API_ENDPOINTS } from "./config";
import { getStreamEndpoint } from "./config";
import {
Packet,
StreamEvent,
@@ -204,8 +204,8 @@ export function streamPackets(
}
try {
// Create a new EventSource connection
source = new EventSource(API_ENDPOINTS.STREAM);
// Create a new EventSource connection using dynamic endpoint
source = new EventSource(getStreamEndpoint());
// Log connection attempt
if (reconnectAttempt === 0) {

View File

@@ -12,24 +12,18 @@ export const SITE_TITLE = import.meta.env.VITE_SITE_TITLE || "My Mesh";
export const SITE_DESCRIPTION =
import.meta.env.VITE_SITE_DESCRIPTION ||
"Realtime Meshtastic activity via MQTT.";
// API URL configuration
const getApiBaseUrl = (): string => {
// In production, use the same domain (empty string base URL)
if (IS_PROD) {
return import.meta.env.VITE_API_BASE_URL || "";
}
// In development, use the configured base URL with fallback
return import.meta.env.VITE_API_BASE_URL || "http://localhost:8080";
};
export const API_BASE_URL = getApiBaseUrl();
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "";
export const GOOGLE_MAPS_ID = import.meta.env.VITE_GOOGLE_MAPS_ID || "demo-map-id";
export const GOOGLE_MAPS_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_API_KEY || "";
// API endpoints
export const API_ENDPOINTS = {
STREAM: `${API_BASE_URL}/api/stream`,
};
// Google Maps configuration
export const GOOGLE_MAPS_ID = import.meta.env.VITE_GOOGLE_MAPS_ID || "demo-map-id";
/**
* Get the API endpoint for the stream
*/
export function getStreamEndpoint(): string {
return API_ENDPOINTS.STREAM;
}

View File

@@ -48,7 +48,7 @@ declare namespace google {
class InfoWindow {
constructor(opts?: InfoWindowOptions);
setContent(content: string): void;
open(map?: Map, anchor?: Marker): void;
open(map?: Map, anchor?: any): void;
close(): void;
}
@@ -101,6 +101,7 @@ declare namespace google {
// Event-related functionality
const event: {
addListener(instance: object, event: string, listener: (Event) => void): MapsEventListener;
/**
* Removes the given listener, which should have been returned by
* google.maps.event.addListener.