mirror of
https://github.com/dpup/meshstream.git
synced 2026-03-28 17:42:37 +01:00
Docker set up and fixes for build
This commit is contained in:
25
.dockerignore
Normal file
25
.dockerignore
Normal 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
|
||||
53
.env.example
53
.env.example
@@ -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
119
Dockerfile
Normal 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"]
|
||||
34
Makefile
34
Makefile
@@ -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))))
|
||||
|
||||
@@ -68,3 +68,35 @@ web-test:
|
||||
# Run linting for the web application
|
||||
web-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
|
||||
75
README.md
75
README.md
@@ -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
43
docker-compose.yml
Normal 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
|
||||
5244
generated/google/protobuf/descriptor.pb.go
Normal file
5244
generated/google/protobuf/descriptor.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
26
main.go
26
main.go
@@ -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
|
||||
|
||||
1422
proto/google/protobuf/descriptor.proto
Normal file
1422
proto/google/protobuf/descriptor.proto
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './PacketList';
|
||||
export * from './MessageDisplay';
|
||||
export * from './PacketDetails';
|
||||
export * from './Filter';
|
||||
export * from './InfoMessage';
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{' '}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
3
web/src/types/google-maps.d.ts
vendored
3
web/src/types/google-maps.d.ts
vendored
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user