diff --git a/.env b/.env new file mode 100644 index 0000000..6641729 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +SERVER_NAME=delemaco +SERVER_USER=root +MAC_ADDRESS=b8:cb:29:a1:f3:88 +PORT=8080 diff --git a/.env.sample b/.env.sample index 39aae63..ea2a15a 100644 --- a/.env.sample +++ b/.env.sample @@ -2,4 +2,3 @@ SERVER_NAME=pippo SERVER_USER=root MAC_ADDRESS=aa:aa:aa:aa:aa:aa PORT=8080 -SHUTDOWN_PASSWORD="password" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dd69d0..b4a8249 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,137 +2,205 @@ name: Build and Release on: push: - branches: [main] + branches: [ main ] pull_request: - branches: [main] + branches: [ main ] jobs: build: - name: Build multi-arch Raspberry Pi binaries + name: Cross-compile for Raspberry Pi runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: "1.22" + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' - - name: Build for ARMv6 (Pi Zero / Pi 1) - run: | - GOOS=linux GOARCH=arm GOARM=6 \ - go build -ldflags="-s -w" -o wol-server-armv6 + - name: Build for ARM (Pi Zero, Pi 1 - ARMv6) + run: | + GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w" -o wol-server-arm6 - - name: Build for ARM64 (Pi Zero 2 / Pi 3 / Pi 4 / Pi 5) - run: | - GOOS=linux GOARCH=arm64 \ - go build -ldflags="-s -w" -o wol-server-arm64 + - name: Create systemd service file + run: | + cat > wol-server.service << 'EOL' + [Unit] + Description=WOL Server Go Application + After=network.target - - name: Create systemd service file - run: | - cat > wol-server.service << 'EOF' - [Unit] - Description=WOL Server Go Application - After=network.target + [Service] + User=pi + WorkingDirectory=/home/pi/wol-server + ExecStart=/home/pi/wol-server/wol-server + Restart=always - [Service] - Type=simple - User=loke - WorkingDirectory=/home/loke/wol-server - ExecStart=/home/loke/wol-server/wol-server - Restart=always - RestartSec=3 + [Install] + WantedBy=multi-user.target + EOL - [Install] - WantedBy=multi-user.target - EOF + - name: Create deployment script + run: | + cat > install.sh << 'EOL' + #!/bin/bash + set -e - - name: Create install script - run: | - cat > install.sh << 'EOF' - #!/bin/bash - set -e + # Installation directory + INSTALL_DIR=~/wol-server - INSTALL_DIR="$HOME/wol-server" + echo "Creating installation directory..." + mkdir -p $INSTALL_DIR + mkdir -p $INSTALL_DIR/templates - echo "Creating installation directory..." - mkdir -p "$INSTALL_DIR/templates" + echo "Installing application..." + cp wol-server-arm6 $INSTALL_DIR/wol-server + chmod +x $INSTALL_DIR/wol-server - ARCH=$(uname -m) - case "$ARCH" in - armv6l|armv7l) - BIN="wol-server-armv6" - ;; - aarch64) - BIN="wol-server-arm64" - ;; - *) - echo "Unsupported architecture: $ARCH" - exit 1 - ;; - esac + echo "Installing template files..." + cp -r templates/* $INSTALL_DIR/templates/ - echo "Detected architecture: $ARCH" - echo "Using binary: $BIN" + echo "Installing system service..." + sudo cp wol-server.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable wol-server - cp "$BIN" "$INSTALL_DIR/wol-server" - chmod +x "$INSTALL_DIR/wol-server" + # Install required dependencies + echo "Installing dependencies..." + sudo apt-get update -qq + sudo apt-get install -y wakeonlan sshpass - echo "Installing templates..." - cp -r templates/* "$INSTALL_DIR/templates/" + # Start the service + echo "Starting service..." + sudo systemctl restart wol-server - echo "Installing systemd service..." - sudo cp wol-server.service /etc/systemd/system/ - sudo systemctl daemon-reload - sudo systemctl enable wol-server + echo "===========================================" + echo "Installation complete!" + echo "The WOL server is now running at http://$(hostname -I | awk '{print $1}'):8080" + echo "===========================================" + EOL - echo "Installing dependencies..." - sudo apt-get update -qq - sudo apt-get install -y wakeonlan sshpass + chmod +x install.sh - echo "Starting service..." - sudo systemctl restart wol-server + - name: Create README with installation instructions + run: | + cat > INSTALL.md << 'EOL' + # WOL Server Installation Guide - echo "===========================================" - echo "WOL Server installed successfully!" - echo "URL: http://$(hostname -I | awk '{print $1}'):8080" - echo "===========================================" - EOF + This guide will help you install the Wake-on-LAN server on your Raspberry Pi. - chmod +x install.sh + ## Prerequisites - - name: Create release package - run: | - mkdir -p package - cp wol-server-armv6 package/ - cp wol-server-arm64 package/ - cp wol-server.service package/ - cp install.sh package/ - cp -r templates package/ + - Raspberry Pi running Raspberry Pi OS (Raspbian) + - SSH access to your Pi + - SCP or SFTP capability to transfer files - tar -czf wol-server.tar.gz -C package . + ## Installation Steps - - name: Create Forgejo Release - if: github.ref == 'refs/heads/main' - run: | - TAG="v${{ github.run_number }}" - API_URL="${{ github.server_url }}/api/v1" - REPO="${{ github.repository }}" + ### 1. Transfer Files to Raspberry Pi - # Create release - RELEASE_ID=$(curl -s -X POST \ - -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ - -H "Content-Type: application/json" \ - "${API_URL}/repos/${REPO}/releases" \ - -d "{\"tag_name\": \"${TAG}\", \"name\": \"Release ${TAG}\", \"draft\": false, \"prerelease\": false}" \ - | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) + **Option 1: Using SCP from your computer** - # Upload asset - curl -s -X POST \ - -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ - "${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=wol-server.tar.gz" \ - -F "attachment=@wol-server.tar.gz" > /dev/null + ```bash + # Replace with your Pi's IP address + PI_IP=192.168.1.100 - echo "Release ${TAG} created with asset wol-server.tar.gz" + # Transfer the installation package + scp wol-server.tar.gz pi@$PI_IP:~/ + ``` + + **Option 2: Using SFTP client** + + Use a tool like FileZilla, WinSCP, or Cyberduck to transfer the `wol-server.tar.gz` file to your Raspberry Pi. + + ### 2. SSH into your Raspberry Pi + + ```bash + ssh pi@192.168.1.100 + ``` + + ### 3. Extract and Install + + ```bash + # Navigate to home directory + cd ~ + + # Extract the archive + tar -xzf wol-server.tar.gz + + # Run the installation script + ./install.sh + ``` + + ### 4. Test the Installation + + Open a web browser and navigate to: + ``` + http://[your-pi-ip]:8080 + ``` + + ## Troubleshooting + + If the service fails to start, check the logs: + ```bash + sudo systemctl status wol-server + ``` + + If template errors occur, ensure the template files were copied correctly: + ```bash + ls -la ~/wol-server/templates/ + ``` + + ## Manual Installation (if needed) + + If you encounter issues with the automated install: + + ```bash + # Create directories + mkdir -p ~/wol-server/templates + + # Copy files manually + cp wol-server-arm6 ~/wol-server/wol-server + chmod +x ~/wol-server/wol-server + cp templates/* ~/wol-server/templates/ + sudo cp wol-server.service /etc/systemd/system/ + + # Install dependencies + sudo apt-get update + sudo apt-get install -y wakeonlan sshpass + + # Enable and start service + sudo systemctl daemon-reload + sudo systemctl enable wol-server + sudo systemctl start wol-server + ``` + EOL + + - name: Create all-in-one package + run: | + # Create a single package with everything needed + mkdir -p package + cp wol-server-arm6 package/ + cp wol-server.service package/ + cp install.sh package/ + cp -r templates package/ + cp INSTALL.md package/ + + # Create the tarball + tar -czf wol-server.tar.gz -C package . + + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + if: github.ref == 'refs/heads/main' + with: + tag_name: v${{ github.run_number }} + name: Release v${{ github.run_number }} + draft: false + prerelease: false + files: | + wol-server.tar.gz + INSTALL.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 4c49bd7..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.env diff --git a/README.md b/README.md index 0e16060..9b602d8 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,7 @@ A lightweight web-based Wake-on-LAN control panel designed for Raspberry Pi that ## Features - **Simple Web Interface**: Boot and shut down your server with a clean, responsive UI -- **Status Monitoring**: Check if your target device is online with auto-refreshing UI -- **Scheduled Backup Window**: Configure automatic daily, bi-daily, weekly, or monthly server startup and shutdown for backup operations -- **Auto Shutdown**: Shut down the server automatically at the end of the backup window -- **Smart Shutdown Protection**: Only auto-shuts down servers that were started by the scheduler -- **Passwordless Operation**: Uses environment variable for all shutdown operations -- **Multiple Shutdown Methods**: Supports various SSH authentication methods for reliable automatic shutdown +- **Status Monitoring**: Check if your target device is online - **Raspberry Pi Optimized**: Built specifically for ARM processors found in all Raspberry Pi models - **Secure Shutdown**: Password-protected shutdown functionality - **Lightweight**: Minimal resource usage ideal for running on even the oldest Pi models @@ -107,10 +102,6 @@ nano ~/wol-server/.env | `SERVER_USER` | SSH username for shutdown | root | | `MAC_ADDRESS` | MAC address for Wake-on-LAN | aa:bb:cc:dd:ee:ff | | `PORT` | Web interface port | 8080 | -| `SHUTDOWN_PASSWORD` | Password for all shutdown operations | None | -| `REFRESH_INTERVAL` | UI refresh interval in seconds | 60 | - -The scheduled backup window configuration is stored in `schedule.json` in the installation directory. It includes the start time, end time, and frequency settings. After changing configuration, restart the service: ```bash @@ -131,49 +122,6 @@ http://your-pi-ip:8080 - **Status Checking**: The interface shows the current status (Online/Offline) - **Booting**: Click the "Boot" button to send a WOL magic packet - **Shutting Down**: Click "Shutdown" and enter your SSH password when prompted -- **Scheduled Backup Window**: Configure automatic server startup and shutdown on a regular schedule - -#### Using Scheduled Backup Window - -1. Click "Configure Schedule" in the Scheduled Backup Window section -2. Enter your desired start and end times (in 24-hour format) -3. Select a frequency (daily, every 2 days, weekly, or monthly) -4. Optionally, enable "Auto Shutdown" (requires SHUTDOWN_PASSWORD in .env file) -5. Click "Save Schedule" to activate -6. The server will automatically boot at the start time and: - - If auto shutdown is enabled: automatically shut down at the end time - - If auto shutdown is disabled: remain on until manually shut down -7. To modify an existing schedule, click "Edit Schedule" -8. To disable, click "Disable Schedule" from the main interface - -**Note:** All shutdown operations (manual and scheduled) use the SHUTDOWN_PASSWORD from your .env file. - -#### Auto Shutdown Feature - -The auto shutdown feature provides several advantages: -- Saves power by ensuring the server only runs during scheduled backup periods -- Prevents the server from accidentally remaining on after backups are complete -- Fully automates the backup window process -- Smart protection: only shuts down servers that were started by the scheduler - -**Requirements for Shutdown Operations:** -1. Set the `SHUTDOWN_PASSWORD` in your .env file -2. The SSH server must be properly configured on the target server -3. The user account specified in the configuration must have sudo privileges -4. The password must be correct for the specified user account -5. The server must allow password authentication via SSH - -**Troubleshooting Shutdown Operations:** -- If shutdown fails, check the logs for specific error messages -- Ensure `sshpass` is installed on your Raspberry Pi (`sudo apt-get install sshpass`) -- Verify you can manually SSH to the server with the provided credentials -- Confirm the user has sudo privileges to run the shutdown command -- Check if the server requires SSH key authentication instead of password -- Verify the SHUTDOWN_PASSWORD is correctly set in your .env file - -#### Auto-Refreshing UI - -The web interface automatically refreshes every minute (or according to the REFRESH_INTERVAL setting) to show the current server status. This ensures you always see up-to-date information without having to manually refresh the page. ## Maintenance diff --git a/handlers.go b/handlers.go index 684c558..0c4333e 100644 --- a/handlers.go +++ b/handlers.go @@ -4,7 +4,6 @@ import ( "log" "net/http" "runtime" - "time" ) // Handle the root route - show status @@ -14,32 +13,21 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { return } - // Add cache control headers to prevent caching - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Expires", "0") - online := isServerOnline() status := "Online" - color := "#4caf50" // Material green + color := "#4caf50" // Material green if !online { status = "Offline" - color = "#d32f2f" // Material red + color = "#d32f2f" // Material red } - // Get current schedule configuration - scheduleConfig := GetScheduleConfig() - data := StatusData{ - Server: serverName, - Status: status, - Color: color, - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - ErrorMessage: "", - Schedule: scheduleConfig, - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: status, + Color: color, + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, + ErrorMessage: "", } if err := tmpl.Execute(w, data); err != nil { @@ -59,14 +47,11 @@ func bootHandler(w http.ResponseWriter, r *http.Request) { // Display booting status data := StatusData{ - Server: serverName, - Status: "Booting", - Color: "#607d8b", // Material blue-gray - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Booting", + Color: "#607d8b", // Material blue-gray + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -75,14 +60,11 @@ func bootHandler(w http.ResponseWriter, r *http.Request) { } else { // Server is already online data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", // Material green - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Online", + Color: "#4caf50", // Material green + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -98,14 +80,11 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) { if !online { // Server is already offline data := StatusData{ - Server: serverName, - Status: "Offline", - Color: "#d32f2f", // Material red - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Offline", + Color: "#d32f2f", // Material red + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -114,53 +93,42 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) { return } - // Check if shutdown password is set - if shutdownPassword == "" { - // Show error about missing password - data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", // Material green - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - ConfirmShutdown: false, - ErrorMessage: "SHUTDOWN_PASSWORD not set in environment. Please set it in the .env file.", - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, - } - if err := tmpl.Execute(w, data); err != nil { - http.Error(w, "Failed to render template", http.StatusInternalServerError) - log.Printf("Template render error: %v", err) - } - return - } - - // Show confirmation dialog - we'll use the password from .env + // Show confirmation dialog data := StatusData{ Server: serverName, Status: "Online", - Color: "#4caf50", // Material green + Color: "#4caf50", // Material green IsTestMode: runtime.GOOS == "darwin", ConfirmShutdown: true, - AskPassword: false, // Make sure we don't ask for password - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), + AskPassword: false, } - - // Notify the user if password is not configured - if shutdownPassword == "" { - data.ErrorMessage = "SHUTDOWN_PASSWORD not set in environment. Shutdown may fail." - } - if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) log.Printf("Template render error: %v", err) } } -// Handle shutdown confirmation without password -// enterPasswordHandler function removed - we now use the password from .env directly +// Handle password entry for shutdown +func enterPasswordHandler(w http.ResponseWriter, r *http.Request) { + if !isServerOnline() { + // Server is already offline, redirect to home + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // Show password entry dialog + data := StatusData{ + Server: serverName, + Status: "Online", + Color: "#4caf50", // Material green + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } +} // Handle actual shutdown request func shutdownHandler(w http.ResponseWriter, r *http.Request) { @@ -170,20 +138,25 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { return } - // Use the password from environment variable - if shutdownPassword == "" { - log.Printf("SHUTDOWN_PASSWORD not set in environment, cannot perform shutdown") - // Show error message + // Parse form data to get password + if err := r.ParseForm(); err != nil { + log.Printf("Error parsing form: %v", err) + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // Get password from form + password := r.FormValue("password") + + if password == "" { + // Show password form again with error data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - ErrorMessage: "SHUTDOWN_PASSWORD not set in environment", - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Online", + Color: "#4caf50", + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + ErrorMessage: "Password cannot be empty", } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -193,22 +166,19 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { } if isServerOnline() { - // Shutdown the server using the password from .env file - err := shutdownServer(shutdownPassword) + // Shutdown the server + err := shutdownServer(password) if err != nil { log.Printf("Error shutting down server: %v", err) - // Show error message + // Show password form again with error data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, // No longer asking for password - ErrorMessage: "Failed to shutdown server. Please check the password in .env file.", - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Online", + Color: "#4caf50", + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + ErrorMessage: "Failed to shutdown server. Please check your password.", } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -219,14 +189,11 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { // Display shutting down status data := StatusData{ - Server: serverName, - Status: "Shutting down", - Color: "#5d4037", // Material brown - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Shutting down", + Color: "#5d4037", // Material brown + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -235,14 +202,11 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { } else { // Server is already offline data := StatusData{ - Server: serverName, - Status: "Offline", - Color: "#d32f2f", // Material red - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Offline", + Color: "#d32f2f", // Material red + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) diff --git a/main.go b/main.go index d4b78c6..7bb50e8 100644 --- a/main.go +++ b/main.go @@ -1,27 +1,21 @@ package main import ( - "bytes" - "encoding/json" "fmt" "log" "net/http" "os" - "os/exec" "runtime" - "strconv" - "time" "github.com/joho/godotenv" ) // Default values var ( - serverName = "server" // Server to ping - serverUser = "root" // SSH username - macAddress = "aa:aa:aa:aa:aa:aa" // MAC address of the server - port = "8080" // Port to listen on - refreshInterval = 60 // UI refresh interval in seconds + serverName = "server" // Server to ping + serverUser = "root" // SSH username + macAddress = "aa:aa:aa:aa:aa:aa" // MAC address of the server + port = "8080" // Port to listen on ) func loadEnvVariables() { @@ -47,15 +41,8 @@ func loadEnvVariables() { port = envPort } - // Load refresh interval if set - if envRefresh := os.Getenv("REFRESH_INTERVAL"); envRefresh != "" { - if val, err := strconv.Atoi(envRefresh); err == nil && val > 0 { - refreshInterval = val - } - } - - log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s, REFRESH=%d", - serverName, serverUser, macAddress, port, refreshInterval) + log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s", + serverName, serverUser, macAddress, port) } func main() { @@ -67,24 +54,13 @@ func main() { log.Fatalf("Failed to setup template: %v", err) } - // Verify schedule configuration and clean up stale schedule data if needed - verifyScheduleConfig() - - // Setup a ticker to check schedule and perform actions - go runScheduleChecker() - // Register route handlers http.HandleFunc("/", indexHandler) http.HandleFunc("/boot", bootHandler) http.HandleFunc("/confirm-shutdown", confirmShutdownHandler) - // Password is now taken directly from .env file + http.HandleFunc("/enter-password", enterPasswordHandler) http.HandleFunc("/shutdown", shutdownHandler) - // Schedule API endpoints - http.HandleFunc("/api/schedule", scheduleHandler) - // API shutdown endpoint - http.HandleFunc("/api/shutdown", apiShutdownHandler) - // Start the server listenAddr := fmt.Sprintf(":%s", port) log.Printf("Starting WOL Server on http://localhost%s", listenAddr) @@ -97,364 +73,3 @@ func main() { log.Fatalf("Server failed to start: %v", err) } } - -// API Shutdown handler - shuts down the server with password from environment -func apiShutdownHandler(w http.ResponseWriter, r *http.Request) { - // Add cache control headers to prevent caching - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Expires", "0") - // Set content type for JSON response - w.Header().Set("Content-Type", "application/json") - - // Only allow POST requests - if r.Method != "POST" { - w.WriteHeader(http.StatusMethodNotAllowed) - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": false, - "error": "Method not allowed. Use POST.", - }) - return - } - - // Check if shutdown password is available in environment - if shutdownPassword == "" { - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": false, - "error": "SHUTDOWN_PASSWORD not set in environment", - }) - return - } - - // Check if server is online before attempting shutdown - if !isServerOnline() { - // Server is already offline - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": false, - "error": "Server is already offline", - }) - return - } - - // Try to shut down the server using the password from environment - err := shutdownServer(shutdownPassword) - if err != nil { - // Shutdown command failed - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": false, - "error": "Failed to shutdown server: " + err.Error(), - }) - log.Printf("API shutdown failed: %v", err) - return - } - - // Shutdown initiated successfully - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": true, - "message": "Server shutdown initiated", - }) - log.Printf("API shutdown successful") -} - -// Handle schedule API requests -func scheduleHandler(w http.ResponseWriter, r *http.Request) { - // Set content type - w.Header().Set("Content-Type", "application/json") - - // Handle GET request - return current schedule - if r.Method == "GET" { - data, err := json.Marshal(GetScheduleConfig()) - if err != nil { - http.Error(w, fmt.Sprintf(`{"error": "Failed to marshal schedule data: %v"}`, err), http.StatusInternalServerError) - return - } - w.Write(data) - return - } - - // Handle POST request - update schedule - if r.Method == "POST" { - var newConfig ScheduleConfig - err := json.NewDecoder(r.Body).Decode(&newConfig) - if err != nil { - http.Error(w, fmt.Sprintf(`{"error": "Failed to parse request body: %v"}`, err), http.StatusBadRequest) - return - } - - // Validate the schedule data - if newConfig.Enabled { - // Validate time format (HH:MM) - _, err = time.Parse("15:04", newConfig.StartTime) - if err != nil { - http.Error(w, `{"error": "Invalid start time format. Use 24-hour format (HH:MM)"}`, http.StatusBadRequest) - return - } - - _, err = time.Parse("15:04", newConfig.EndTime) - if err != nil { - http.Error(w, `{"error": "Invalid end time format. Use 24-hour format (HH:MM)"}`, http.StatusBadRequest) - return - } - - // Validate frequency - validFrequencies := map[string]bool{ - "daily": true, - "every2days": true, - "weekly": true, - "monthly": true, - } - - if !validFrequencies[newConfig.Frequency] { - http.Error(w, `{"error": "Invalid frequency. Use 'daily', 'every2days', 'weekly', or 'monthly'"}`, http.StatusBadRequest) - return - } - - // Reset lastRun if it wasn't set - if newConfig.LastRun == "" { - newConfig.LastRun = "" - } - - // If auto shutdown is enabled, make sure we have a password in env - if newConfig.AutoShutdown && shutdownPassword == "" { - http.Error(w, `{"error": "SHUTDOWN_PASSWORD not set in environment. Please set it before enabling auto-shutdown"}`, http.StatusBadRequest) - return - } - - // Check if SSH connection can be established with the password - if newConfig.AutoShutdown && shutdownPassword != "" { - log.Printf("Testing SSH connection to %s with provided password", serverName) - - // We'll just check if the server is reachable first - if !isServerOnline() { - log.Printf("Server %s is not online, can't test SSH connection", serverName) - } else { - // Try to run a harmless command to test SSH connection - cmd := exec.Command("sshpass", "-p", shutdownPassword, "ssh", - "-o", "StrictHostKeyChecking=no", - "-o", "UserKnownHostsFile=/dev/null", - "-o", "LogLevel=ERROR", - "-o", "ConnectTimeout=5", - fmt.Sprintf("%s@%s", serverUser, serverName), - "echo", "SSH connection test successful") - - var stderr bytes.Buffer - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - log.Printf("SSH connection test failed: %v - %s", err, stderr.String()) - // We don't prevent saving the config even if test fails - // Just log a warning for now - log.Printf("WARNING: Auto shutdown may not work with the provided password") - } else { - log.Printf("SSH connection test successful") - } - } - } - } - - // Save the new configuration - err = UpdateScheduleConfig(newConfig) - if err != nil { - http.Error(w, fmt.Sprintf(`{"error": "Failed to save schedule config: %v"}`, err), http.StatusInternalServerError) - return - } - - // Return the updated config - data, err := json.Marshal(GetScheduleConfig()) - if err != nil { - http.Error(w, fmt.Sprintf(`{"error": "Failed to marshal schedule data: %v"}`, err), http.StatusInternalServerError) - return - } - w.Write(data) - return - } - - // Method not allowed - http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed) -} - -// Verify and clean up schedule configuration -func verifyScheduleConfig() { - // If schedule is enabled, validate all required fields - if scheduleConfig.Enabled { - log.Println("Verifying schedule configuration...") - log.Printf("Current config: StartTime=%s, EndTime=%s, Frequency=%s, AutoShutdown=%v", - scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency, scheduleConfig.AutoShutdown) - - // Check for valid time formats - _, startErr := time.Parse("15:04", scheduleConfig.StartTime) - _, endErr := time.Parse("15:04", scheduleConfig.EndTime) - - if startErr != nil || endErr != nil || scheduleConfig.StartTime == "" || scheduleConfig.EndTime == "" { - log.Println("Warning: Invalid time format in schedule configuration, disabling schedule") - scheduleConfig.Enabled = false - UpdateScheduleConfig(scheduleConfig) - return - } - - // Immediately check if we need to boot ONLY at exact start time - now := time.Now() - currentTimeStr := now.Format("15:04") - - // Check ONLY if current time EXACTLY matches start time - if currentTimeStr == scheduleConfig.StartTime && ShouldRunToday(now) { - log.Printf("STARTUP MATCH: Current time %s matches start time EXACTLY, attempting boot", currentTimeStr) - if !isServerOnline() { - sendWakeOnLAN() - // Mark that the server was started by the scheduler - scheduleConfig.StartedBySchedule = true - scheduleConfig.LastRun = now.Format(time.RFC3339) - UpdateScheduleConfig(scheduleConfig) - } - } - - // Check for valid frequency - validFrequencies := map[string]bool{ - "daily": true, - "every2days": true, - "weekly": true, - "monthly": true, - } - - if !validFrequencies[scheduleConfig.Frequency] { - log.Println("Warning: Invalid frequency in schedule configuration, setting to daily") - scheduleConfig.Frequency = "daily" - UpdateScheduleConfig(scheduleConfig) - } - - log.Printf("Schedule configuration verified: Start=%s, End=%s, Frequency=%s", - scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency) - } -} - -// Run a periodic check of schedule and take appropriate actions -func runScheduleChecker() { - // Define the checkScheduleOnce function - checkScheduleOnce := func() { - // Only check exact times for schedule actions, don't use window logic - now := time.Now() - currentTimeStr := now.Format("15:04") - serverIsOn := isServerOnline() - - // Log schedule status (debug level) - if scheduleConfig.Enabled { - log.Printf("Schedule check: Current=%s, Start=%s, End=%s, LastRun=%s", - currentTimeStr, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.LastRun) - - // Only act at exact start or end times - // EXACT START TIME MATCH - Try to boot server - if currentTimeStr == scheduleConfig.StartTime && !serverIsOn && ShouldRunToday(now) { - log.Println("EXACT START TIME: Initiating boot sequence...") - - // Try multiple times to boot with small delays between attempts - for attempt := 1; attempt <= 3; attempt++ { - log.Printf("Boot attempt %d/3", attempt) - err := sendWakeOnLAN() - if err != nil { - log.Printf("Error booting server from schedule: %v", err) - } else { - log.Println("Schedule: Boot command sent successfully") - // Mark that server was started by scheduler - scheduleConfig.StartedBySchedule = true - scheduleConfig.LastRun = now.Format(time.RFC3339) - UpdateScheduleConfig(scheduleConfig) - } - - // Check if server came online - time.Sleep(3 * time.Second) // Extended wait time for boot check - if isServerOnline() { - log.Println("Server successfully booted!") - break - } - - // Short delay before next attempt - if attempt < 3 { - time.Sleep(1 * time.Second) - } - } - // EXACT END TIME MATCH - Try to shutdown server - } else if currentTimeStr == scheduleConfig.EndTime && serverIsOn { - // Check if auto-shutdown is enabled - if scheduleConfig.AutoShutdown && shutdownPassword != "" && scheduleConfig.StartedBySchedule { - log.Println("EXACT END TIME: Attempting auto-shutdown") - - // Try multiple times to shut down the server - var shutdownSuccessful bool - for attempt := 1; attempt <= 3; attempt++ { - log.Printf("Auto shutdown attempt %d/3", attempt) - err := shutdownServer(shutdownPassword) - if err != nil { - log.Printf("Auto shutdown attempt %d failed: %v", attempt, err) - if attempt < 3 { - time.Sleep(3 * time.Second) - } - } else { - log.Printf("Auto shutdown initiated successfully on attempt %d", attempt) - shutdownSuccessful = true - break - } - } - - if !shutdownSuccessful { - log.Printf("All auto shutdown attempts failed") - } - } - } else { - // No action at non-exact times, just log status - if serverIsOn && scheduleConfig.StartedBySchedule && currentTimeStr > scheduleConfig.EndTime { - log.Printf("Server is still online after end time %s - waiting for next exact end time match", scheduleConfig.EndTime) - } - } - } - - // Update last run timestamp if we've passed the end time - // This helps track when the schedule was last active - currentConfig := GetScheduleConfig() - nowTime := time.Now() - currentTimeString := nowTime.Format("15:04") - if currentConfig.Enabled && currentTimeString > currentConfig.EndTime && currentConfig.LastRun != "" { - lastRun, err := time.Parse(time.RFC3339, currentConfig.LastRun) - if err == nil { - // If it's been more than a day since the last update, reset the timestamp - // This allows the schedule to run again based on frequency - if time.Since(lastRun) > 24*time.Hour { - log.Println("Schedule: Resetting last run timestamp for next scheduled run") - currentConfig.LastRun = "" - UpdateScheduleConfig(currentConfig) - } - } - } - } - - // Use a slightly shorter interval for more responsive scheduling - // First check immediately at startup - checkScheduleOnce() - - // Then set up regular checks - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - log.Println("Schedule checker started - checking every 5 seconds") - log.Printf("Current schedule: enabled=%v, startTime=%s, endTime=%s, frequency=%s, autoShutdown=%v", - scheduleConfig.Enabled, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency, scheduleConfig.AutoShutdown) - - for { - func() { - // Recover from any panics that might occur in the schedule checker - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in schedule checker: %v", r) - } - }() - - checkScheduleOnce() - }() - - // Wait for next tick - <-ticker.C - } -} diff --git a/schedule.json b/schedule.json deleted file mode 100644 index a07df7d..0000000 --- a/schedule.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "enabled": true, - "startTime": "13:46", - "endTime": "13:49", - "frequency": "daily", - "lastRun": "2025-09-05T13:46:41+02:00", - "autoShutdown": true, - "startedBySchedule": true -} \ No newline at end of file diff --git a/templates/status.html b/templates/status.html index 1aae2aa..2c00eaa 100644 --- a/templates/status.html +++ b/templates/status.html @@ -1,773 +1,402 @@ - + -
- - - + + +- Start Time: - {{.Schedule.StartTime}} -
-- End Time: - {{.Schedule.EndTime}} -
-- Frequency: - {{.Schedule.Frequency}} -
-- Auto Shutdown: - {{if .Schedule.AutoShutdown}} - Enabled - {{else}} - Disabled - {{end}} -
-- Last Update: - {{.LastUpdated}} -
-