diff --git a/Dockerfile.build b/Dockerfile.build index bb1ac92..b22e7ba 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -9,6 +9,7 @@ RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ libsqlite3-dev \ make \ nodejs \ + iproute2 \ && rm -rf /var/lib/apt/lists/* # Verify installations diff --git a/cmd/cloud/main.go b/cmd/cloud/main.go index 2dd887e..447e049 100644 --- a/cmd/cloud/main.go +++ b/cmd/cloud/main.go @@ -41,7 +41,6 @@ func run() error { apiServer := api.NewAPIServer( api.WithMetricsManager(server.GetMetricsManager()), api.WithSNMPManager(server.GetSNMPManager()), - api.WithAPIKey(cfg.APIKey), // Pass the API key from config ) server.SetAPIServer(apiServer) diff --git a/packaging/cloud/config/nginx.conf b/packaging/cloud/config/nginx.conf new file mode 100644 index 0000000..e338657 --- /dev/null +++ b/packaging/cloud/config/nginx.conf @@ -0,0 +1,33 @@ +server { + listen 80 default_server; + server_name _; + + access_log /var/log/nginx/serviceradar.access.log; + error_log /var/log/nginx/serviceradar.error.log; + + # API endpoints + location /api/ { + proxy_pass http://localhost:8090; + 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; + } + + # WebSocket support (for Next.js if needed) + location /_next/webpack-hmr { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Main UI + location / { + proxy_pass http://localhost:3000; + 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; + } +} \ No newline at end of file diff --git a/pkg/cloud/api/server.go b/pkg/cloud/api/server.go index ec1baa0..dfd6365 100644 --- a/pkg/cloud/api/server.go +++ b/pkg/cloud/api/server.go @@ -6,10 +6,10 @@ import ( "encoding/json" "log" "net/http" + "os" "time" "github.com/carverauto/serviceradar/pkg/checker/snmp" - "github.com/carverauto/serviceradar/pkg/db" srHttp "github.com/carverauto/serviceradar/pkg/http" "github.com/carverauto/serviceradar/pkg/metrics" "github.com/gorilla/mux" @@ -19,14 +19,13 @@ func NewAPIServer(options ...func(server *APIServer)) *APIServer { s := &APIServer{ nodes: make(map[string]*NodeStatus), router: mux.NewRouter(), - apiKey: "", // Default empty API key } for _, o := range options { o(s) } - s.setupRoutes(s.apiKey) + s.setupRoutes() return s } @@ -43,23 +42,11 @@ func WithSNMPManager(m snmp.SNMPManager) func(server *APIServer) { } } -func WithAPIKey(apiKey string) func(server *APIServer) { - return func(server *APIServer) { - server.apiKey = apiKey - } -} - -func WithDB(db db.Service) func(server *APIServer) { - return func(server *APIServer) { - server.db = db - } -} - -func (s *APIServer) setupRoutes(apiKey string) { +func (s *APIServer) setupRoutes() { // Create a middleware chain middlewareChain := func(next http.Handler) http.Handler { // Order matters: first API key check, then CORS headers - return srHttp.CommonMiddleware(srHttp.APIKeyMiddleware(apiKey)(next)) + return srHttp.CommonMiddleware(srHttp.APIKeyMiddleware(os.Getenv("API_KEY"))(next)) } // Add middleware to router @@ -173,8 +160,6 @@ func (s *APIServer) getNodeHistory(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) nodeID := vars["id"] - log.Printf("Getting node history for: %s", nodeID) - if s.nodeHistoryHandler == nil { http.Error(w, "History handler not configured", http.StatusInternalServerError) return @@ -188,8 +173,6 @@ func (s *APIServer) getNodeHistory(w http.ResponseWriter, r *http.Request) { return } - log.Printf("Fetched %d history points for node: %s", len(points), nodeID) - if err := s.encodeJSONResponse(w, points); err != nil { log.Printf("Error encoding history response: %v", err) http.Error(w, "Error encoding response", http.StatusInternalServerError) @@ -291,7 +274,6 @@ func (s *APIServer) getNode(w http.ResponseWriter, r *http.Request) { node, exists := s.getNodeByID(nodeID) if !exists { - log.Printf("Node %s not found", nodeID) http.Error(w, "Node not found", http.StatusNotFound) return diff --git a/pkg/cloud/api/types.go b/pkg/cloud/api/types.go index 50965fc..a0a5736 100644 --- a/pkg/cloud/api/types.go +++ b/pkg/cloud/api/types.go @@ -6,7 +6,6 @@ import ( "time" "github.com/carverauto/serviceradar/pkg/checker/snmp" - "github.com/carverauto/serviceradar/pkg/db" "github.com/carverauto/serviceradar/pkg/metrics" "github.com/carverauto/serviceradar/pkg/models" "github.com/gorilla/mux" @@ -55,7 +54,5 @@ type APIServer struct { nodeHistoryHandler func(nodeID string) ([]NodeHistoryPoint, error) metricsManager metrics.MetricCollector snmpManager snmp.SNMPManager - db db.Service knownPollers []string - apiKey string } diff --git a/pkg/cloud/server.go b/pkg/cloud/server.go index d40e93f..508e126 100644 --- a/pkg/cloud/server.go +++ b/pkg/cloud/server.go @@ -44,9 +44,6 @@ func NewServer(_ context.Context, config *Config) (*Server, error) { config.Metrics.MaxNodes = 10000 } - // log the config.Metrics - log.Printf("Metrics config: %+v", config.Metrics) - metricsManager := metrics.NewManager(models.MetricsConfig{ Enabled: config.Metrics.Enabled, Retention: config.Metrics.Retention, @@ -336,13 +333,6 @@ func (s *Server) SetAPIServer(apiServer api.Service) { return nil, fmt.Errorf("failed to get node history: %w", err) } - // debug points - log.Printf("Fetched %d history points for node: %s", len(points), nodeID) - // log first 20 points - for i := 0; i < 20 && i < len(points); i++ { - log.Printf("Point %d: %v", i, points[i]) - } - apiPoints := make([]api.NodeHistoryPoint, len(points)) for i, p := range points { apiPoints[i] = api.NodeHistoryPoint{ @@ -356,8 +346,6 @@ func (s *Server) SetAPIServer(apiServer api.Service) { } func (s *Server) checkInitialStates() { - log.Printf("Checking initial states of all nodes") - likeConditions := make([]string, 0, len(s.pollerPatterns)) args := make([]interface{}, 0, len(s.pollerPatterns)) @@ -562,8 +550,6 @@ func (s *Server) processSNMPMetrics(nodeID string, details json.RawMessage, time Metadata: metadata, } - log.Printf("Storing SNMP metric %s for node %s, value: %s", oidName, nodeID, valueStr) - // Store in database if err := s.db.StoreMetric(nodeID, metric); err != nil { log.Printf("Error storing SNMP metric %s for node %s: %v", oidName, nodeID, err) @@ -593,8 +579,6 @@ func (*Server) processSweepData(svc *api.ServiceStatus, now time.Time) error { return fmt.Errorf("%w: %w", errInvalidSweepData, err) } - log.Printf("Received sweep data with timestamp: %v", time.Unix(sweepData.LastSweep, 0).Format(time.RFC3339)) - // If LastSweep is not set or is invalid (0 or negative), use current time if sweepData.LastSweep > now.Add(oneDay).Unix() { log.Printf("Invalid or missing LastSweep timestamp (%d), using current time", sweepData.LastSweep) diff --git a/pkg/cloud/types.go b/pkg/cloud/types.go index 591230d..d18f5c4 100644 --- a/pkg/cloud/types.go +++ b/pkg/cloud/types.go @@ -31,7 +31,6 @@ type Config struct { Metrics Metrics `json:"metrics"` SNMP snmp.Config `json:"snmp"` Security *models.SecurityConfig `json:"security"` - APIKey string `json:"api_key,omitempty"` } type Server struct { diff --git a/scripts/build-web.sh b/scripts/build-web.sh deleted file mode 100755 index 5520319..0000000 --- a/scripts/build-web.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -set -e - -echo "Building web interface..." - -# Build web interface -cd ./web -npm install -npm run build - -echo "Web interface build complete." \ No newline at end of file diff --git a/scripts/buildAll.sh b/scripts/buildAll.sh index 48c3931..14b6b4b 100755 --- a/scripts/buildAll.sh +++ b/scripts/buildAll.sh @@ -3,9 +3,10 @@ VERSION=${VERSION:-1.0.19} +./scripts/setup-deb-agent.sh ./scripts/setup-deb-poller.sh +./scripts/setup-deb-web.sh ./scripts/setup-deb-dusk-checker.sh -./scripts/setup-deb-agent.sh ./scripts/setup-deb-snmp-checker.sh scp ./release-artifacts/serviceradar-poller_${VERSION}.deb duskadmin@192.168.2.22:~/ diff --git a/scripts/buildServiceRadar.sh b/scripts/buildServiceRadar.sh new file mode 100755 index 0000000..aa311d9 --- /dev/null +++ b/scripts/buildServiceRadar.sh @@ -0,0 +1,216 @@ +#!/bin/bash +# buildServiceradar.sh - Build and optionally install ServiceRadar components +set -e # Exit on any error + +# Default settings +VERSION=${VERSION:-1.0.20} +BUILD_TAGS=${BUILD_TAGS:-""} +BUILD_ALL=false +INSTALL=false +TARGET_HOST="" +COMPONENTS=() + +# Display usage information +usage() { + echo "Usage: $0 [options] [components]" + echo + echo "Options:" + echo " -h, --help Show this help message" + echo " -v, --version VERSION Set version number (default: $VERSION)" + echo " -t, --tags TAGS Set build tags" + echo " -a, --all Build all components" + echo " -i, --install Install packages after building" + echo " --host HOST Install to remote host (requires SSH access)" + echo + echo "Components:" + echo " cloud Build cloud API service" + echo " web Build web UI" + echo " poller Build poller service" + echo " agent Build agent service" + echo " dusk-checker Build dusk checker" + echo " snmp-checker Build SNMP checker" + echo + echo "Examples:" + echo " $0 --all Build all components" + echo " $0 cloud web Build cloud and web components" + echo " $0 --all --install Build and install all components locally" + echo " $0 cloud web --install --host user@server Build and install on remote host" + echo + exit 1 +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + usage + ;; + -v|--version) + VERSION="$2" + shift 2 + ;; + -t|--tags) + BUILD_TAGS="$2" + shift 2 + ;; + -a|--all) + BUILD_ALL=true + shift + ;; + -i|--install) + INSTALL=true + shift + ;; + --host) + TARGET_HOST="$2" + shift 2 + ;; + cloud|web|poller|agent|dusk-checker|snmp-checker) + COMPONENTS+=("$1") + shift + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +# Check if we should build all components +if [ "$BUILD_ALL" = true ]; then + COMPONENTS=("cloud" "web" "poller" "agent" "dusk-checker" "snmp-checker") +fi + +# If no components specified, show usage +if [ ${#COMPONENTS[@]} -eq 0 ]; then + echo "Error: No components specified for building" + usage +fi + +# Export variables for sub-scripts +export VERSION +export BUILD_TAGS + +# Function to build a component +build_component() { + local component=$1 + echo "=========================================" + echo "Building $component component (version $VERSION)" + echo "=========================================" + + case $component in + cloud) + ./scripts/setup-deb-cloud.sh + ;; + web) + ./scripts/setup-deb-web.sh + ;; + poller) + ./scripts/setup-deb-poller.sh + ;; + agent) + ./scripts/setup-deb-agent.sh + ;; + dusk-checker) + ./scripts/setup-deb-dusk-checker.sh + ;; + snmp-checker) + ./scripts/setup-deb-snmp-checker.sh + ;; + *) + echo "Unknown component: $component" + return 1 + ;; + esac + + echo "Build of $component completed successfully" + return 0 +} + +# Function to install packages +install_packages() { + local install_cmd="sudo dpkg -i" + local prefix="./release-artifacts/" + local packages=() + + # Build list of package files + for component in "${COMPONENTS[@]}"; do + local package_name + + case $component in + cloud) + package_name="serviceradar-cloud_${VERSION}.deb" + ;; + web) + package_name="serviceradar-web_${VERSION}.deb" + ;; + poller) + package_name="serviceradar-poller_${VERSION}.deb" + ;; + agent) + package_name="serviceradar-agent_${VERSION}.deb" + ;; + dusk-checker) + package_name="serviceradar-dusk-checker_${VERSION}.deb" + ;; + snmp-checker) + package_name="serviceradar-snmp-checker_${VERSION}.deb" + ;; + *) + echo "Unknown component for installation: $component" + continue + ;; + esac + + # Add package to the list if it exists + if [ -f "${prefix}${package_name}" ]; then + packages+=("${prefix}${package_name}") + else + echo "Warning: Package file not found: ${prefix}${package_name}" + fi + done + + # If no packages to install, return + if [ ${#packages[@]} -eq 0 ]; then + echo "No packages found for installation" + return 1 + fi + + # Install locally or remotely + if [ -z "$TARGET_HOST" ]; then + echo "Installing packages locally..." + $install_cmd "${packages[@]}" + else + echo "Installing packages on $TARGET_HOST..." + + # Create temp directory on remote host + ssh "$TARGET_HOST" "mkdir -p ~/serviceradar-tmp" + + # Copy packages to remote host + for package in "${packages[@]}"; do + echo "Copying $package to $TARGET_HOST..." + scp "$package" "$TARGET_HOST:~/serviceradar-tmp/" + done + + # Install packages on remote host + ssh "$TARGET_HOST" "sudo dpkg -i ~/serviceradar-tmp/*.deb && rm -rf ~/serviceradar-tmp" + fi + + echo "Installation completed successfully" + return 0 +} + +# Create release-artifacts directory if it doesn't exist +mkdir -p ./release-artifacts + +# Build each component +for component in "${COMPONENTS[@]}"; do + build_component "$component" || exit 1 +done + +# Install packages if requested +if [ "$INSTALL" = true ]; then + install_packages || exit 1 +fi + +echo "All operations completed successfully!" diff --git a/scripts/setup-deb-cloud.sh b/scripts/setup-deb-cloud.sh index bfcbfa5..8d188ba 100755 --- a/scripts/setup-deb-cloud.sh +++ b/scripts/setup-deb-cloud.sh @@ -1,5 +1,5 @@ #!/bin/bash -# setup-deb-cloud.sh +# setup-deb-cloud.sh - UPDATED set -e # Exit on any error echo "Setting up package structure..." @@ -12,29 +12,8 @@ PKG_ROOT="serviceradar-cloud_${VERSION}" mkdir -p "${PKG_ROOT}/DEBIAN" mkdir -p "${PKG_ROOT}/usr/local/bin" mkdir -p "${PKG_ROOT}/etc/serviceradar" +mkdir -p "${PKG_ROOT}/etc/nginx/conf.d" mkdir -p "${PKG_ROOT}/lib/systemd/system" -#mkdir -p "${PKG_ROOT}/usr/local/share/serviceradar-cloud/web" - -#echo "Building web interface..." - -# Build web interface if not already built -#if [ ! -d "web/dist" ]; then -# cd ./web -# npm install -# npm run build -# cd .. -#fi - -# Create a directory for the embedded content -#mkdir -p pkg/cloud/api/web -#cp -r web/dist pkg/cloud/api/web/ - -# Only copy web assets to package directory for container builds -# For non-container builds, they're embedded in the binary -#if [[ "$BUILD_TAGS" == *"containers"* ]]; then -# cp -r web/dist "${PKG_ROOT}/usr/local/share/serviceradar-cloud/web/" -# echo "Copied web assets for container build" -#fi echo "Building Go binary..." @@ -59,10 +38,12 @@ Version: ${VERSION} Section: utils Priority: optional Architecture: amd64 -Depends: systemd +Depends: systemd, nginx +Recommends: serviceradar-web Maintainer: Michael Freeman -Description: ServiceRadar cloud service with web interface - Provides centralized monitoring and web dashboard for ServiceRadar. +Description: ServiceRadar cloud API service + Provides centralized monitoring and API server for ServiceRadar monitoring system. + Includes Nginx configuration for API access. Config: /etc/serviceradar/cloud.json EOF @@ -74,12 +55,13 @@ EOF # Create systemd service file cat > "${PKG_ROOT}/lib/systemd/system/serviceradar-cloud.service" << EOF [Unit] -Description=ServiceRadar Cloud Service +Description=ServiceRadar Cloud API Service After=network.target [Service] Type=simple User=serviceradar +EnvironmentFile=/etc/serviceradar/api.env ExecStart=/usr/local/bin/serviceradar-cloud -config /etc/serviceradar/cloud.json Restart=always RestartSec=10 @@ -91,10 +73,8 @@ KillSignal=SIGTERM WantedBy=multi-user.target EOF -# Create default config only if we're creating a fresh package -if [ ! -f "/etc/serviceradar/cloud.json" ]; then - # Create default config file - cat > "${PKG_ROOT}/etc/serviceradar/cloud.json" << EOF +# Create default config file +cat > "${PKG_ROOT}/etc/serviceradar/cloud.json" << EOF { "listen_addr": ":8090", "grpc_addr": ":50052", @@ -131,13 +111,18 @@ if [ ! -f "/etc/serviceradar/cloud.json" ]; then ] } EOF -fi # Create postinst script cat > "${PKG_ROOT}/DEBIAN/postinst" << EOF #!/bin/bash set -e +# Check for Nginx +if ! command -v nginx >/dev/null 2>&1; then + echo "ERROR: Nginx is required but not installed. Please install nginx and try again." + exit 1 +fi + # Create serviceradar user if it doesn't exist if ! id -u serviceradar >/dev/null 2>&1; then useradd --system --no-create-home --shell /usr/sbin/nologin serviceradar @@ -152,17 +137,26 @@ mkdir -p /var/lib/serviceradar chown -R serviceradar:serviceradar /var/lib/serviceradar chmod 755 /var/lib/serviceradar -# Set permissions for web assets -#if [ -d "/usr/local/share/serviceradar-cloud/web" ]; then -# chown -R serviceradar:serviceradar /usr/local/share/serviceradar-cloud -# chmod -R 755 /usr/local/share/serviceradar-cloud -#fi +# Generate API key if it doesn't exist +if [ ! -f "/etc/serviceradar/api.env" ]; then + echo "Generating API key..." + API_KEY=\$(openssl rand -hex 32) + echo "API_KEY=\$API_KEY" > /etc/serviceradar/api.env + chmod 600 /etc/serviceradar/api.env + chown serviceradar:serviceradar /etc/serviceradar/api.env + echo "API key generated and stored in /etc/serviceradar/api.env" +fi # Enable and start service systemctl daemon-reload systemctl enable serviceradar-cloud systemctl start serviceradar-cloud || echo "Failed to start service, please check the logs" +echo "ServiceRadar Cloud API service installed successfully!" +echo "API is running on port 8090" +echo "Accessible via Nginx at http://localhost/api/" +echo "For a complete UI experience, install the serviceradar-web package." + exit 0 EOF @@ -177,7 +171,6 @@ set -e systemctl stop serviceradar-cloud || true systemctl disable serviceradar-cloud || true -exit 0 EOF chmod 755 "${PKG_ROOT}/DEBIAN/prerm" @@ -187,8 +180,8 @@ echo "Building Debian package..." # Create release-artifacts directory if it doesn't exist mkdir -p ./release-artifacts -# Build the package -dpkg-deb --build "${PKG_ROOT}" +# Build the package with root-owner-group to avoid ownership warnings +dpkg-deb --root-owner-group --build "${PKG_ROOT}" # Move the deb file to the release-artifacts directory mv "${PKG_ROOT}.deb" "./release-artifacts/" diff --git a/scripts/setup-deb-web.sh b/scripts/setup-deb-web.sh new file mode 100755 index 0000000..a9199a8 --- /dev/null +++ b/scripts/setup-deb-web.sh @@ -0,0 +1,248 @@ +#!/bin/bash +# setup-deb-web.sh - UPDATED +set -e # Exit on any error + +echo "Setting up package structure for Next.js web interface..." + +VERSION=${VERSION:-1.0.20} + +# Create package directory structure +PKG_ROOT="serviceradar-web_${VERSION}" +mkdir -p "${PKG_ROOT}/DEBIAN" +mkdir -p "${PKG_ROOT}/usr/local/share/serviceradar-web" +mkdir -p "${PKG_ROOT}/lib/systemd/system" +mkdir -p "${PKG_ROOT}/etc/serviceradar" +mkdir -p "${PKG_ROOT}/etc/nginx/conf.d" + +echo "Building Next.js application..." + +# Build Next.js application +cd ./web + +# Ensure package.json contains the right scripts and dependencies +if ! grep -q '"next": ' package.json; then + echo "ERROR: This doesn't appear to be a Next.js app. Check your web directory." + exit 1 +fi + +# Install dependencies with npm +npm install + +# Build the Next.js application +echo "Building Next.js application with standalone output..." +npm run build + +# Copy the Next.js standalone build +echo "Copying Next.js standalone build to package..." +cp -r .next/standalone/* "../${PKG_ROOT}/usr/local/share/serviceradar-web/" +cp -r .next/standalone/.next "../${PKG_ROOT}/usr/local/share/serviceradar-web/" + +# Make sure static files are copied +mkdir -p "../${PKG_ROOT}/usr/local/share/serviceradar-web/.next/static" +cp -r .next/static "../${PKG_ROOT}/usr/local/share/serviceradar-web/.next/" + +# Copy public files if they exist +if [ -d "public" ]; then + cp -r public "../${PKG_ROOT}/usr/local/share/serviceradar-web/" +fi + +cd .. + +echo "Creating package files..." + +# Create default config file +cat > "${PKG_ROOT}/etc/serviceradar/web.json" << EOF +{ + "port": 3000, + "host": "0.0.0.0", + "api_url": "http://localhost:8090" +} +EOF + +# Create Nginx configuration +cat > "${PKG_ROOT}/etc/nginx/conf.d/serviceradar-web.conf" << EOF +# ServiceRadar Web Interface - Nginx Configuration +server { + listen 80; + server_name _; # Catch-all server name (use your domain if you have one) + + access_log /var/log/nginx/serviceradar-web.access.log; + error_log /var/log/nginx/serviceradar-web.error.log; + + # API proxy (assumes serviceradar-cloud package is installed) + location /api/ { + proxy_pass http://localhost:8090; + 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; + } + + # Support for Next.js WebSockets (if used) + location /_next/webpack-hmr { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Main app - proxy all requests to Next.js + location / { + proxy_pass http://127.0.0.1:3000; + 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; + } +} +EOF + +# Create control file +cat > "${PKG_ROOT}/DEBIAN/control" << EOF +Package: serviceradar-web +Version: ${VERSION} +Section: utils +Priority: optional +Architecture: amd64 +Depends: systemd, nodejs (>= 16.0.0), nginx +Recommends: serviceradar-cloud +Maintainer: Michael Freeman +Description: ServiceRadar web interface + Next.js web interface for the ServiceRadar monitoring system. + Includes Nginx configuration for integrated API and UI access. +Config: /etc/serviceradar/web.json +EOF + +# Create conffiles to mark configuration files +cat > "${PKG_ROOT}/DEBIAN/conffiles" << EOF +/etc/serviceradar/web.json +/etc/nginx/conf.d/serviceradar-web.conf +EOF + +# Create systemd service file +cat > "${PKG_ROOT}/lib/systemd/system/serviceradar-web.service" << EOF +[Unit] +Description=ServiceRadar Web Interface +After=network.target + +[Service] +Type=simple +User=serviceradar +WorkingDirectory=/usr/local/share/serviceradar-web +Environment=NODE_ENV=production +Environment=PORT=3000 +EnvironmentFile=/etc/serviceradar/api.env +ExecStart=/usr/bin/node server.js +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + +# Create postinst script +cat > "${PKG_ROOT}/DEBIAN/postinst" << EOF +#!/bin/bash +set -e + +# Check for Nginx +if ! command -v nginx >/dev/null 2>&1; then + echo "ERROR: Nginx is required but not installed. Please install nginx and try again." + exit 1 +fi + +# Create serviceradar user if it doesn't exist +if ! id -u serviceradar >/dev/null 2>&1; then + useradd --system --no-create-home --shell /usr/sbin/nologin serviceradar +fi + +# Install Node.js if not already installed +if ! command -v node >/dev/null 2>&1; then + echo "Installing Node.js..." + curl -fsSL https://deb.nodesource.com/setup_18.x | bash - + apt-get install -y nodejs +fi + +# Set permissions +chown -R serviceradar:serviceradar /usr/local/share/serviceradar-web +chown -R serviceradar:serviceradar /etc/serviceradar/web.json +chmod 755 /usr/local/share/serviceradar-web +chmod 644 /etc/serviceradar/web.json + +# Check for API key from cloud package +if [ ! -f "/etc/serviceradar/api.env" ]; then + echo "WARNING: API key file not found. The serviceradar-cloud package should be installed first." + echo "Creating a temporary API key file..." + API_KEY=\$(openssl rand -hex 32) + echo "API_KEY=\$API_KEY" > /etc/serviceradar/api.env + chmod 600 /etc/serviceradar/api.env + chown serviceradar:serviceradar /etc/serviceradar/api.env + echo "For proper functionality, please reinstall serviceradar-cloud package." +fi + +# Configure Nginx +if [ -f /etc/nginx/sites-enabled/default ]; then + echo "Disabling default Nginx site..." + rm -f /etc/nginx/sites-enabled/default +fi + +# Create symbolic link if Nginx uses sites-enabled pattern +if [ -d /etc/nginx/sites-enabled ]; then + ln -sf /etc/nginx/conf.d/serviceradar-web.conf /etc/nginx/sites-enabled/ +fi + +# Test and reload Nginx +echo "Testing Nginx configuration..." +nginx -t || { echo "Warning: Nginx configuration test failed. Please check your configuration."; } +systemctl reload nginx || systemctl restart nginx || echo "Warning: Failed to reload/restart Nginx." + +# Enable and start service +systemctl daemon-reload +systemctl enable serviceradar-web +systemctl start serviceradar-web || echo "Failed to start service, please check the logs" + +echo "ServiceRadar Web Interface installed successfully!" +echo "Web UI is running on port 3000" +echo "Nginx configured as reverse proxy - you can access the UI at http://localhost/" + +exit 0 +EOF + +chmod 755 "${PKG_ROOT}/DEBIAN/postinst" + +# Create prerm script +cat > "${PKG_ROOT}/DEBIAN/prerm" << EOF +#!/bin/bash +set -e + +# Stop and disable service +systemctl stop serviceradar-web || true +systemctl disable serviceradar-web || true + +# Remove Nginx symlink if exists +if [ -f /etc/nginx/sites-enabled/serviceradar-web.conf ]; then + rm -f /etc/nginx/sites-enabled/serviceradar-web.conf +fi + +# Reload Nginx if running +if systemctl is-active --quiet nginx; then + systemctl reload nginx || true +fi + +exit 0 +EOF + +chmod 755 "${PKG_ROOT}/DEBIAN/prerm" + +echo "Building Debian package..." + +# Create release-artifacts directory if it doesn't exist +mkdir -p ./release-artifacts + +# Build the package with root-owner-group to avoid ownership warnings +dpkg-deb --root-owner-group --build "${PKG_ROOT}" + +# Move the deb file to the release-artifacts directory +mv "${PKG_ROOT}.deb" "./release-artifacts/" + +echo "Package built: release-artifacts/${PKG_ROOT}.deb" \ No newline at end of file diff --git a/web/next.config.ts b/web/next.config.ts index cbd7263..bae678c 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -2,9 +2,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { reactStrictMode: true, + output: "standalone", async rewrites() { - // const backendUrl = process.env.BACKEND_URL || 'http://localhost:8090'; - const backendUrl = process.env.BACKEND_URL || 'http://172.233.208.210:8090'; + const backendUrl = process.env.BACKEND_URL || 'http://localhost:8090'; return [ { source: '/api/:path*', @@ -16,6 +16,12 @@ const nextConfig: NextConfig = { NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8090', NEXT_PUBLIC_BACKEND_URL: process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8090/', }, + serverRuntimeConfig: { + // Will only be available on the server side + apiKey: process.env.API_KEY || '', + } }; +console.log('Next.js configuration loaded with API_KEY length:', process.env.API_KEY) + export default nextConfig; \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index 7e7ddfd..69c2a29 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,9 +11,11 @@ "lodash": "^4.17.21", "lucide-react": "^0.476.0", "next": "15.1.7", + "next-runtime-env": "^3.2.2", "react": "^19.0.0", "react-dom": "^19.0.0", "recharts": "^2.15.1", + "styled-jsx": "^5.1.6", "tailwindcss-animate": "^1.0.7", "xlsx": "^0.18.5" }, @@ -811,6 +813,22 @@ "node": ">= 10" } }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.24.tgz", + "integrity": "sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-win32-x64-msvc": { "version": "15.1.7", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.7.tgz", @@ -3276,7 +3294,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -4255,6 +4272,301 @@ } } }, + "node_modules/next-runtime-env": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/next-runtime-env/-/next-runtime-env-3.2.2.tgz", + "integrity": "sha512-S5S6NxIf3XeaVc9fLBN2L5Jzu+6dLYCXeOaPQa1RzKRYlG2BBayxXOj6A4VsciocyNkJMazW1VAibtbb1/ZjAw==", + "license": "MIT", + "dependencies": { + "next": "^14", + "react": "^18" + }, + "peerDependencies": { + "next": "^14", + "react": "^18" + } + }, + "node_modules/next-runtime-env/node_modules/@next/env": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.24.tgz", + "integrity": "sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==", + "license": "MIT" + }, + "node_modules/next-runtime-env/node_modules/@next/swc-darwin-arm64": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.24.tgz", + "integrity": "sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/next-runtime-env/node_modules/@next/swc-darwin-x64": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.24.tgz", + "integrity": "sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/next-runtime-env/node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.24.tgz", + "integrity": "sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/next-runtime-env/node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.24.tgz", + "integrity": "sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/next-runtime-env/node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.24.tgz", + "integrity": "sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/next-runtime-env/node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.24.tgz", + "integrity": "sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/next-runtime-env/node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.24.tgz", + "integrity": "sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/next-runtime-env/node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.24.tgz", + "integrity": "sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/next-runtime-env/node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/next-runtime-env/node_modules/next": { + "version": "14.2.24", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.24.tgz", + "integrity": "sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.24", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.24", + "@next/swc-darwin-x64": "14.2.24", + "@next/swc-linux-arm64-gnu": "14.2.24", + "@next/swc-linux-arm64-musl": "14.2.24", + "@next/swc-linux-x64-gnu": "14.2.24", + "@next/swc-linux-x64-musl": "14.2.24", + "@next/swc-win32-arm64-msvc": "14.2.24", + "@next/swc-win32-ia32-msvc": "14.2.24", + "@next/swc-win32-x64-msvc": "14.2.24" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-runtime-env/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/next-runtime-env/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/next-runtime-env/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/next-runtime-env/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/next-runtime-env/node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/web/package.json b/web/package.json index a4885cc..71bf7fb 100644 --- a/web/package.json +++ b/web/package.json @@ -12,9 +12,11 @@ "lodash": "^4.17.21", "lucide-react": "^0.476.0", "next": "15.1.7", + "next-runtime-env": "^3.2.2", "react": "^19.0.0", "react-dom": "^19.0.0", "recharts": "^2.15.1", + "styled-jsx": "^5.1.6", "tailwindcss-animate": "^1.0.7", "xlsx": "^0.18.5" }, diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 729f09a..7d1968b 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,7 +1,9 @@ +// app/layout.tsx import './globals.css'; import { Inter } from 'next/font/google'; import { Providers } from './providers'; import { ReactNode } from 'react'; +import { PublicEnvScript } from 'next-runtime-env'; const inter = Inter({ subsets: ['latin'] }); @@ -10,15 +12,15 @@ export const metadata = { description: 'Monitor your network services', }; -// Define the props type for RootLayout interface RootLayoutProps { - children: ReactNode; // Explicitly type children + children: ReactNode; } export default function RootLayout({ children }: RootLayoutProps) { return ( + diff --git a/web/src/app/nodes/page.js b/web/src/app/nodes/page.js index 64e270a..604a7ba 100644 --- a/web/src/app/nodes/page.js +++ b/web/src/app/nodes/page.js @@ -1,46 +1,89 @@ -// src/app/nodes/page.js -import { Suspense } from 'react'; -import NodeList from '../../components/NodeList'; +// Server component that fetches data +import {Suspense} from "react"; +import NodeList from "../../components/NodeList"; -// Async function to fetch data on the server with API key authentication -async function fetchNodes() { +// Disable static generation, always fetch latest data +export const revalidate = 0; + +// Server component that fetches all data needed +async function fetchNodesWithMetrics() { try { - // When running on the server, use the full backend URL - const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL; - const apiKey = process.env.API_KEY; - - const response = await fetch(`${backendUrl}/api/nodes`, { - headers: { - 'X-API-Key': apiKey - }, - cache: 'no-store' // Don't cache this request + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8090'; + const apiKey = process.env.API_KEY || ''; + + // Fetch all nodes first + const nodesResponse = await fetch(`${backendUrl}/api/nodes`, { + headers: { 'X-API-Key': apiKey }, + cache: 'no-store', // Prevent caching }); - if (!response.ok) { - console.error('Nodes API fetch failed:', { - status: response.status, - statusText: response.statusText - }); + if (!nodesResponse.ok) { + throw new Error(`Nodes API request failed: ${nodesResponse.status}`); + } + + const nodes = await nodesResponse.json(); + console.log(`Fetched ${nodes.length} nodes`); + + // Create metrics lookup object + const serviceMetrics = {}; + + // Fetch metrics for each node with ICMP services + for (const node of nodes) { + const icmpServices = node.services?.filter(s => s.type === 'icmp') || []; - throw new Error(`Nodes API request failed: ${response.status}`); + if (icmpServices.length > 0) { + console.log(`Node ${node.node_id} has ${icmpServices.length} ICMP services`); + + // Fetch all metrics for this node (one fetch per node is more efficient) + try { + const metricsResponse = await fetch(`${backendUrl}/api/nodes/${node.node_id}/metrics`, { + headers: { 'X-API-Key': apiKey }, + cache: 'no-store', + }); + + if (!metricsResponse.ok) { + console.error(`Metrics API failed for ${node.node_id}: ${metricsResponse.status}`); + continue; + } + + const allNodeMetrics = await metricsResponse.json(); + console.log(`Received ${allNodeMetrics.length} metrics for ${node.node_id}`); + + // Filter and organize metrics for each ICMP service + for (const service of icmpServices) { + const serviceMetricsData = allNodeMetrics.filter(m => m.service_name === service.name); + const key = `${node.node_id}-${service.name}`; + serviceMetrics[key] = serviceMetricsData; + console.log(`${key}: Filtered ${serviceMetricsData.length} metrics`); + } + } catch (error) { + console.error(`Error fetching metrics for ${node.node_id}:`, error); + } + } } - return await response.json(); + return { nodes, serviceMetrics }; } catch (error) { - console.error('Error fetching nodes:', error); - return []; + console.error('Error fetching nodes data:', error); + return { nodes: [], serviceMetrics: {} }; } } export default async function NodesPage() { - const initialNodes = await fetchNodes(); + // Fetch all required data from the server + const { nodes, serviceMetrics } = await fetchNodesWithMetrics(); + + // Log the metrics data for debugging + console.log(`Fetched ${Object.keys(serviceMetrics).length} service metric sets`); return (
- -
Loading nodes...
-
}> - + +
Loading nodes...
+ + }> +
); diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 43d92b2..3015d3b 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,17 +1,19 @@ -// src/app/page.tsx +// src/app/page.tsx (Server Component) import { Suspense } from 'react'; import Dashboard from '../components/Dashboard'; +// This runs only on the server async function fetchStatus() { try { - // When running on the server, use the full backend URL - const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL; + // Direct server-to-server call with API key + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8090'; const apiKey = process.env.API_KEY || ''; const response = await fetch(`${backendUrl}/api/status`, { headers: { 'X-API-Key': apiKey - } + }, + cache: 'no-store' // For fresh data on each request }); if (!response.ok) { @@ -25,13 +27,16 @@ async function fetchStatus() { } } +// Server Component export default async function HomePage() { + // Data fetching happens server-side const initialData = await fetchStatus(); return (

Dashboard

Loading dashboard...
}> + {/* Pass pre-fetched data to client component */} diff --git a/web/src/app/service/[nodeid]/[servicename]/page.js b/web/src/app/service/[nodeid]/[servicename]/page.js index b1175eb..d9811e7 100644 --- a/web/src/app/service/[nodeid]/[servicename]/page.js +++ b/web/src/app/service/[nodeid]/[servicename]/page.js @@ -9,9 +9,10 @@ async function fetchServiceData(nodeId, serviceName) { const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8090'; const apiKey = process.env.API_KEY || ''; + // Fetch node info const nodesResponse = await fetch(`${backendUrl}/api/nodes`, { headers: { 'X-API-Key': apiKey }, - cache: 'no-store', // Prevent caching on the server + cache: 'no-store', }); if (!nodesResponse.ok) { @@ -19,42 +20,46 @@ async function fetchServiceData(nodeId, serviceName) { } const nodes = await nodesResponse.json(); - const node = nodes.find((n) => n.node_id === nodeId); + if (!node) return { error: 'Node not found' }; const service = node.services?.find((s) => s.name === serviceName); if (!service) return { error: 'Service not found' }; + // Fetch metrics let metrics = []; try { const metricsResponse = await fetch(`${backendUrl}/api/nodes/${nodeId}/metrics`, { headers: { 'X-API-Key': apiKey }, - next: { revalidate: 30 }, + cache: 'no-store', }); + if (!metricsResponse.ok) { - console.error(`Metrics API failed: ${metricsResponse.status} - ${await metricsResponse.text()}`); + console.error(`Metrics API failed: ${metricsResponse.status}`); } else { metrics = await metricsResponse.json(); } } catch (metricsError) { console.error('Error fetching metrics data:', metricsError); } + const serviceMetrics = metrics.filter((m) => m.service_name === serviceName); + // Fetch SNMP data if needed let snmpData = []; if (service.type === 'snmp') { try { const end = new Date(); const start = new Date(); - start.setHours(end.getHours() - 1); + start.setHours(end.getHours() - 24); // Get 24h of data for initial load const snmpUrl = `${backendUrl}/api/nodes/${nodeId}/snmp?start=${start.toISOString()}&end=${end.toISOString()}`; console.log("Fetching SNMP from:", snmpUrl); const snmpResponse = await fetch(snmpUrl, { headers: { 'X-API-Key': apiKey }, - next: { revalidate: 30 }, + cache: 'no-store', }); if (!snmpResponse.ok) { @@ -62,6 +67,7 @@ async function fetchServiceData(nodeId, serviceName) { console.error(`SNMP API failed: ${snmpResponse.status} - ${errorText}`); throw new Error(`SNMP API request failed: ${snmpResponse.status} - ${errorText}`); } + snmpData = await snmpResponse.json(); console.log("SNMP data fetched:", snmpData.length); } catch (snmpError) { diff --git a/web/src/components/Dashboard.jsx b/web/src/components/Dashboard.jsx index 22090e5..68b796c 100644 --- a/web/src/components/Dashboard.jsx +++ b/web/src/components/Dashboard.jsx @@ -1,31 +1,16 @@ -// src/components/Dashboard.jsx +// src/components/Dashboard.jsx - Client Component 'use client'; import React from 'react'; -import { useAPIData } from '@/lib/api'; function Dashboard({ initialData = null }) { - // Use improved API client with caching - refresh every 30 seconds instead of 10 - const { data: systemStatus, error, isLoading } = useAPIData('/api/status', initialData, 10000); + // No data fetching here - just use the data passed from server component - if (isLoading && !systemStatus) { - return ( -
- {[...Array(3)].map((_, i) => ( -
-
-
-
- ))} -
- ); - } - - if (error) { + if (!initialData) { return (

Error Loading Dashboard

-

{error}

+

Could not load dashboard data

); } @@ -38,7 +23,7 @@ function Dashboard({ initialData = null }) { Total Nodes

- {systemStatus?.total_nodes || 0} + {initialData?.total_nodes || 0}

@@ -48,7 +33,7 @@ function Dashboard({ initialData = null }) { Healthy Nodes

- {systemStatus?.healthy_nodes || 0} + {initialData?.healthy_nodes || 0}

@@ -58,8 +43,8 @@ function Dashboard({ initialData = null }) { Last Update

- {systemStatus?.last_update - ? new Date(systemStatus.last_update).toLocaleTimeString() + {initialData?.last_update + ? new Date(initialData.last_update).toLocaleTimeString() : 'N/A'}

diff --git a/web/src/components/NodeList.jsx b/web/src/components/NodeList.jsx index d5cf4e8..692737d 100644 --- a/web/src/components/NodeList.jsx +++ b/web/src/components/NodeList.jsx @@ -1,20 +1,32 @@ -// src/components/NodeList.jsx 'use client'; -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import ServiceSparkline from "./ServiceSparkline"; -import { useAPIData } from '@/lib/api'; -function NodeList({ initialNodes = [] }) { +function NodeList({ initialNodes = [], serviceMetrics = {} }) { const router = useRouter(); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [nodesPerPage] = useState(10); const [sortBy, setSortBy] = useState('name'); const [sortOrder, setSortOrder] = useState('asc'); + const [nodes, setNodes] = useState(initialNodes); - const { data: nodes, error, isLoading } = useAPIData('/api/nodes', initialNodes, 10000); + // Update nodes when initialNodes changes + useEffect(() => { + setNodes(initialNodes); + }, [initialNodes]); + + // Set up auto-refresh + useEffect(() => { + const refreshInterval = 10000; // 10 seconds (sync with ServiceSparkline) + const timer = setInterval(() => { + router.refresh(); // Trigger server-side re-fetch of nodes/page.js + }, refreshInterval); + + return () => clearInterval(timer); + }, [router]); const sortNodesByName = useCallback((a, b) => { const aMatch = a.node_id.match(/(\d+)$/); @@ -53,9 +65,7 @@ function NodeList({ initialNodes = [] }) { sortedResults.sort((a, b) => b.is_healthy === a.is_healthy ? sortNodesByName(a, b) - : b.is_healthy - ? 1 - : -1 + : b.is_healthy ? 1 : -1 ); break; case 'name': @@ -95,40 +105,6 @@ function NodeList({ initialNodes = [] }) { setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc')); }, []); - // Error State - if (error) { - return ( -
-

Error Loading Nodes

-

{error}

-
- ); - } - - // Loading State - if (isLoading && (!nodes || nodes.length === 0)) { - return ( -
-
-
-
-
-
- {[...Array(6)].map((_, i) => ( -
-
-
-
-
-
-
- ))} -
-
- ); - } - - // Regular Component Content return (
{/* Header row */} @@ -138,19 +114,14 @@ function NodeList({ initialNodes = [] }) { setSearchTerm(e.target.value)} /> @@ -168,7 +137,7 @@ function NodeList({ initialNodes = [] }) {
{/* Content placeholder when no nodes are found */} - {sortedNodes.length === 0 && !isLoading && ( + {sortedNodes.length === 0 && (

No nodes found

@@ -178,99 +147,34 @@ function NodeList({ initialNodes = [] }) { )} {/* Main content */} - {renderTableView()} - - {/* Pagination */} - {pageCount > 1 && ( -

- {[...Array(pageCount)].map((_, i) => ( - - ))} -
- )} -
- ); - - function renderTableView() { - return (
- - - - - + + + + + {currentNodes.map((node) => ( - + - @@ -300,8 +204,23 @@ function NodeList({ initialNodes = [] }) {
- Status - - Node - - Services - - ICMP Response Time - - Last Update - StatusNodeServicesICMP Response TimeLast Update
-
-
- {node.node_id} +
{node.node_id}
{node.services?.map((service, idx) => (
- handleServiceClick(node.node_id, service.name) - } + className="inline-flex items-center gap-1 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 p-1 rounded transition-colors" + onClick={() => handleServiceClick(node.node_id, service.name)} > - - - {service.name} - + + {service.name}
))}
@@ -278,21 +182,21 @@ function NodeList({ initialNodes = [] }) {
{node.services ?.filter((service) => service.type === 'icmp') - .map((service, idx) => ( -
- -
- ))} + .map((service, idx) => { + const metricKey = `${node.node_id}-${service.name}`; + const metricsForService = serviceMetrics[metricKey] || []; + return ( +
+ +
+ ); + })}
+ {new Date(node.last_update).toLocaleString()}
- ); - } + + {/* Pagination */} + {pageCount > 1 && ( +
+ {[...Array(pageCount)].map((_, i) => ( + + ))} +
+ )} + + ); } export default NodeList; \ No newline at end of file diff --git a/web/src/components/SNMPDashboard.jsx b/web/src/components/SNMPDashboard.jsx index d4275fa..b374727 100644 --- a/web/src/components/SNMPDashboard.jsx +++ b/web/src/components/SNMPDashboard.jsx @@ -1,20 +1,17 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react'; +'use client'; + +import React, {useCallback, useState, useEffect} from 'react'; import {CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import { useRouter } from 'next/navigation'; -const SNMPDashboard = ({ nodeId, serviceName }) => { - const [snmpData, setSNMPData] = useState([]); +const SNMPDashboard = ({ nodeId, serviceName, initialData = [] }) => { + const router = useRouter(); + const [snmpData, setSNMPData] = useState(initialData); const [processedData, setProcessedData] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); const [timeRange, setTimeRange] = useState('1h'); const [selectedMetric, setSelectedMetric] = useState(null); const [availableMetrics, setAvailableMetrics] = useState([]); - // Use refs to prevent state updates during render cycles - const dataRef = useRef(snmpData); - const fetchingRef = useRef(false); - const timerId = useRef(null); - // Process SNMP counter data to show rates instead of raw values const processCounterData = useCallback((data) => { if (!data || data.length < 2) return data || []; @@ -51,144 +48,79 @@ const SNMPDashboard = ({ nodeId, serviceName }) => { } }, []); - // The main data fetching function - separated to avoid re-creation on every render - const fetchSNMPData = useCallback(async () => { - // Prevent concurrent fetches - if (fetchingRef.current) return; - fetchingRef.current = true; - - try { - // Only show loading on initial fetch - if (!dataRef.current.length) { - setLoading(true); - } - - const end = new Date(); - const start = new Date(); - - switch (timeRange) { - case '1h': - start.setHours(end.getHours() - 1); - break; - case '6h': - start.setHours(end.getHours() - 6); - break; - case '24h': - start.setHours(end.getHours() - 24); - break; - default: - start.setHours(end.getHours() - 1); - } - - // Directly use fetch to avoid any potential issues with the cache - const response = await fetch( - `/api/nodes/${nodeId}/snmp?start=${start.toISOString()}&end=${end.toISOString()}`, - { - headers: { - 'X-API-Key': process.env.NEXT_PUBLIC_API_KEY || '', - 'Cache-Control': 'no-cache' - }, - cache: 'no-store' - } - ); - - if (!response.ok) { - throw new Error(`Server returned ${response.status} ${response.statusText}`); - } - - const data = await response.json(); + // Set up auto-refresh from server + useEffect(() => { + const refreshInterval = 30000; // 30 seconds + const timer = setInterval(() => { + router.refresh(); // Trigger a server-side refresh + }, refreshInterval); - // Handle empty or invalid data - if (!data || !Array.isArray(data)) { - console.warn("Received invalid SNMP data format"); - fetchingRef.current = false; - return; - } + return () => clearInterval(timer); + }, [router]); - // Don't update state if component is unmounting or not mounted - if (!dataRef.current) return; + // Initialize metrics and selection + useEffect(() => { + if (initialData.length > 0) { + setSNMPData(initialData); // Extract unique OID names - const metrics = [...new Set(data.map(item => item.oid_name))]; - - // Update our state safely - setSNMPData(data); - dataRef.current = data; + const metrics = [...new Set(initialData.map(item => item.oid_name))]; setAvailableMetrics(metrics); if (!selectedMetric && metrics.length > 0) { setSelectedMetric(metrics[0]); } - - setLoading(false); - setError(null); - } catch (err) { - console.error('Error fetching SNMP data:', err); - - // Only show error if we don't have any data yet - if (!dataRef.current.length) { - setError(err.message || "Failed to fetch SNMP data"); - setLoading(false); - } - } finally { - fetchingRef.current = false; } - }, [nodeId, timeRange, selectedMetric]); - - // Initial data load - useEffect(() => { - // Reset state when parameters change - setSNMPData([]); - dataRef.current = []; - setLoading(true); - setError(null); + }, [initialData, selectedMetric]); - fetchSNMPData().catch(err => console.error("Initial fetch error:", err)); - - return () => { - // Clear any pending timers on unmount - if (timerId.current) clearTimeout(timerId.current); - }; - }, [fetchSNMPData, nodeId, timeRange]); - - // Set up polling - in a separate effect to avoid interfering with data fetching + // Process metric data when selected metric changes useEffect(() => { - const pollInterval = 30000; // 30 seconds + if (snmpData.length > 0 && selectedMetric) { + try { + // Filter by time range + const end = new Date(); + const start = new Date(); + + switch (timeRange) { + case '1h': + start.setHours(end.getHours() - 1); + break; + case '6h': + start.setHours(end.getHours() - 6); + break; + case '24h': + start.setHours(end.getHours() - 24); + break; + default: + start.setHours(end.getHours() - 1); + } - // Set up polling with manual setTimeout instead of setInterval - const pollData = () => { - fetchSNMPData() - .catch(err => console.error("Poll error:", err)) - .finally(() => { - // Only schedule next poll if component is still mounted - if (dataRef.current !== null) { - timerId.current = setTimeout(pollData, pollInterval); - } + // Filter by time range + const timeFilteredData = snmpData.filter(item => { + const timestamp = new Date(item.timestamp); + return timestamp >= start && timestamp <= end; }); - }; - - // Start polling - timerId.current = setTimeout(pollData, pollInterval); - // Clean up on unmount - return () => { - dataRef.current = null; // Signal that we're unmounting - if (timerId.current) clearTimeout(timerId.current); - }; - }, [fetchSNMPData]); + // Filter by selected metric + const metricData = timeFilteredData.filter(item => item.oid_name === selectedMetric); - // Process metric data when selected metric changes - useEffect(() => { - if (snmpData.length > 0 && selectedMetric) { - try { - const metricData = snmpData.filter(item => item.oid_name === selectedMetric); + // Process the data const processed = processCounterData(metricData); setProcessedData(processed); } catch (err) { console.error('Error processing metric data:', err); } } - }, [selectedMetric, snmpData, processCounterData]); + }, [selectedMetric, snmpData, timeRange, processCounterData]); + + // When time range changes, refresh the page to get new data from server + const handleTimeRangeChange = (range) => { + setTimeRange(range); + // For significant time range changes, refresh data from server + if (range === '24h' || (timeRange === '24h' && range !== '24h')) { + router.refresh(); + } + }; const formatRate = (rate) => { if (rate === undefined || rate === null || isNaN(rate)) return "N/A"; @@ -205,46 +137,25 @@ const SNMPDashboard = ({ nodeId, serviceName }) => { } }; - // Error state - if (error) { - return ( -
-

Error Loading SNMP Data

-

{error}

- -
- ); - } - - // Loading state - if (loading) { + // Empty data state + if (!initialData.length) { return ( -
-
- Loading SNMP data... -
+
+

+ No SNMP Data Available +

+

+ No metrics found for this service. +

); } - // Empty data state - if (!processedData.length) { + if (!processedData.length && selectedMetric) { return (

- No SNMP Data Available + No Data Available

No metrics found for the selected time range and OID. @@ -257,7 +168,7 @@ const SNMPDashboard = ({ nodeId, serviceName }) => { {['1h', '6h', '24h'].map((range) => (