Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b5299409c |
11 changed files with 2512 additions and 534 deletions
5
.env.sample
Normal file
5
.env.sample
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
SERVER_NAME=pippo
|
||||
SERVER_USER=root
|
||||
MAC_ADDRESS=aa:aa:aa:aa:aa:aa
|
||||
PORT=8080
|
||||
SHUTDOWN_PASSWORD="password"
|
||||
178
.github/workflows/build.yml
vendored
178
.github/workflows/build.yml
vendored
|
|
@ -43,63 +43,152 @@ jobs:
|
|||
|
||||
- name: Create deployment script
|
||||
run: |
|
||||
cat > deploy.sh << 'EOL'
|
||||
cat > install.sh << 'EOL'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Detect Raspberry Pi model and use the appropriate binary
|
||||
MODEL=$(cat /proc/cpuinfo | grep "Model" | sed 's/Model\s*:\s*//g')
|
||||
echo "Detected model: $MODEL"
|
||||
# Installation directory
|
||||
INSTALL_DIR=~/wol-server
|
||||
|
||||
# Select the right binary based on processor architecture
|
||||
ARCH=$(uname -m)
|
||||
echo "Architecture: $ARCH"
|
||||
echo "Creating installation directory..."
|
||||
mkdir -p $INSTALL_DIR
|
||||
mkdir -p $INSTALL_DIR/templates
|
||||
|
||||
if [[ "$ARCH" == "aarch64" ]]; then
|
||||
echo "Using ARM64 binary"
|
||||
cp wol-server-arm64 wol-server
|
||||
elif [[ "$ARCH" == "armv7l" ]]; then
|
||||
echo "Using ARMv7 binary"
|
||||
cp wol-server-arm7 wol-server
|
||||
else
|
||||
echo "Using ARMv6 binary"
|
||||
cp wol-server-arm6 wol-server
|
||||
fi
|
||||
echo "Installing application..."
|
||||
cp wol-server-arm6 $INSTALL_DIR/wol-server
|
||||
chmod +x $INSTALL_DIR/wol-server
|
||||
|
||||
echo "Creating directory..."
|
||||
mkdir -p ~/wol-server
|
||||
echo "Installing template files..."
|
||||
cp -r templates/* $INSTALL_DIR/templates/
|
||||
|
||||
echo "Copying application..."
|
||||
cp wol-server ~/wol-server/
|
||||
chmod +x ~/wol-server/wol-server
|
||||
|
||||
echo "Installing service..."
|
||||
echo "Installing system service..."
|
||||
sudo cp wol-server.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable wol-server
|
||||
|
||||
# Install required dependencies
|
||||
echo "Installing dependencies..."
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y wakeonlan sshpass
|
||||
|
||||
# Start the service
|
||||
echo "Starting service..."
|
||||
sudo systemctl restart wol-server
|
||||
|
||||
echo "Deployment complete!"
|
||||
echo "Service status:"
|
||||
sudo systemctl status wol-server
|
||||
echo "==========================================="
|
||||
echo "Installation complete!"
|
||||
echo "The WOL server is now running at http://$(hostname -I | awk '{print $1}'):8080"
|
||||
echo "==========================================="
|
||||
EOL
|
||||
|
||||
chmod +x deploy.sh
|
||||
chmod +x install.sh
|
||||
|
||||
- name: Create archive for each platform
|
||||
- name: Create README with installation instructions
|
||||
run: |
|
||||
# ARMv6 package
|
||||
mkdir -p armv6-package
|
||||
cp wol-server-arm6 armv6-package/wol-server-arm6
|
||||
cp wol-server.service armv6-package/
|
||||
cp deploy.sh armv6-package/
|
||||
tar -czf wol-server-armv6.tar.gz -C armv6-package .
|
||||
cat > INSTALL.md << 'EOL'
|
||||
# WOL Server Installation Guide
|
||||
|
||||
# All-in-one package
|
||||
mkdir -p all-package
|
||||
cp wol-server-arm6 all-package/
|
||||
cp wol-server.service all-package/
|
||||
cp deploy.sh all-package/
|
||||
tar -czf wol-server-all.tar.gz -C all-package .
|
||||
This guide will help you install the Wake-on-LAN server on your Raspberry Pi.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Raspberry Pi running Raspberry Pi OS (Raspbian)
|
||||
- SSH access to your Pi
|
||||
- SCP or SFTP capability to transfer files
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Transfer Files to Raspberry Pi
|
||||
|
||||
**Option 1: Using SCP from your computer**
|
||||
|
||||
```bash
|
||||
# Replace with your Pi's IP address
|
||||
PI_IP=192.168.1.100
|
||||
|
||||
# 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
|
||||
|
|
@ -111,10 +200,7 @@ jobs:
|
|||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
wol-server-arm6
|
||||
wol-server.service
|
||||
deploy.sh
|
||||
wol-server-armv6.tar.gz
|
||||
wol-server-all.tar.gz
|
||||
wol-server.tar.gz
|
||||
INSTALL.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.env
|
||||
367
README.md
367
README.md
|
|
@ -1,147 +1,286 @@
|
|||

|
||||
# WOL Server - Wake-on-LAN Control Panel for Raspberry Pi
|
||||
|
||||
A lightweight web-based Wake-on-LAN control panel designed for Raspberry Pi that lets you remotely power on and shut down your network devices.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Status Monitoring**: Real-time status checks to determine if your server is online or offline
|
||||
- **Wake-on-LAN**: Boot your server remotely with the click of a button
|
||||
- **Remote Shutdown**: Safely shut down your server when it's not needed
|
||||
- **Responsive UI**: Simple, mobile-friendly interface with color-coded status indicators
|
||||
- **Lightweight**: Built with Go for minimal resource usage, perfect for Raspberry Pi Zero
|
||||
|
||||
## Requirements
|
||||
|
||||
- Raspberry Pi (Zero, 2, 3, 4, etc.)
|
||||
- Go (version 1.16 or higher)
|
||||
- wakeonlan utility
|
||||
- SSH access to the target server (for shutdown functionality)
|
||||
- Target server configured for Wake-on-LAN
|
||||
- **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
|
||||
- **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
|
||||
- **Easy Setup**: Simple installation process with clear instructions
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install Dependencies
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install golang-go wakeonlan
|
||||
```
|
||||
- Raspberry Pi (any model) running Raspberry Pi OS
|
||||
- Network connection
|
||||
- Basic knowledge of SSH/terminal
|
||||
|
||||
### 2. Clone the Repository
|
||||
### Option 1: One-Command Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/wol-server.git
|
||||
cd wol-server
|
||||
```
|
||||
1. **Download the latest release** on your local machine from the [Releases page](https://github.com/thisloke/wol-server/releases)
|
||||
|
||||
### 3. Configure the Application
|
||||
|
||||
Edit the constants in `main.go` to match your server:
|
||||
|
||||
```go
|
||||
const (
|
||||
serverName = "yourserver" // Hostname or IP address of your server
|
||||
macAddress = "xx:xx:xx:xx:xx:xx" // MAC address of your server's network interface
|
||||
port = "8080" // Port to run the web application on
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Build the Application
|
||||
|
||||
```bash
|
||||
go build -o wol-server
|
||||
```
|
||||
|
||||
### 5. Set Up as a System Service
|
||||
|
||||
Create a systemd service file:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/wol-server.service
|
||||
```
|
||||
|
||||
Add the following content (adjust paths if needed):
|
||||
|
||||
```
|
||||
[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
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Enable and start the service:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable wol-server
|
||||
sudo systemctl start wol-server
|
||||
```
|
||||
|
||||
## SSH Configuration for Remote Shutdown
|
||||
|
||||
For the shutdown functionality to work, you need to set up password-less SSH:
|
||||
|
||||
1. Generate an SSH key on your Raspberry Pi:
|
||||
2. **Transfer the package to your Raspberry Pi** using SCP:
|
||||
```bash
|
||||
ssh-keygen -t rsa
|
||||
scp wol-server.tar.gz pi@your-pi-ip:~/
|
||||
```
|
||||
|
||||
2. Copy the key to your target server:
|
||||
3. **SSH into your Raspberry Pi**:
|
||||
```bash
|
||||
ssh-copy-id user@yourserver
|
||||
ssh pi@your-pi-ip
|
||||
```
|
||||
|
||||
3. Configure sudo on the target server to allow password-less shutdown:
|
||||
4. **Install with a single command**:
|
||||
```bash
|
||||
# On the target server, run:
|
||||
sudo visudo
|
||||
|
||||
# Add this line (replacing 'user' with your username):
|
||||
user ALL=(ALL) NOPASSWD: /sbin/shutdown
|
||||
tar -xzf wol-server.tar.gz && ./install.sh
|
||||
```
|
||||
|
||||
5. **Access the web interface** at:
|
||||
```
|
||||
http://your-pi-ip:8080
|
||||
```
|
||||
|
||||
### Option 2: Manual Installation
|
||||
|
||||
If you prefer a manual approach or encounter issues with the automated install:
|
||||
|
||||
1. **Create installation directory**:
|
||||
```bash
|
||||
mkdir -p ~/wol-server/templates
|
||||
```
|
||||
|
||||
2. **Transfer and install program files**:
|
||||
```bash
|
||||
# Copy the executable
|
||||
cp wol-server-arm6 ~/wol-server/wol-server
|
||||
chmod +x ~/wol-server/wol-server
|
||||
|
||||
# Copy template files
|
||||
cp templates/* ~/wol-server/templates/
|
||||
|
||||
# Create .env file
|
||||
cat > ~/wol-server/.env << EOL
|
||||
SERVER_NAME=pippo
|
||||
SERVER_USER=root
|
||||
MAC_ADDRESS=aa:bb:cc:dd:ee:ff
|
||||
PORT=8080
|
||||
EOL
|
||||
```
|
||||
|
||||
3. **Install service**:
|
||||
```bash
|
||||
sudo cp wol-server.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable wol-server
|
||||
sudo systemctl start wol-server
|
||||
```
|
||||
|
||||
4. **Install required dependencies**:
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y wakeonlan sshpass
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The application can be configured by editing the `.env` file in the installation directory:
|
||||
|
||||
```bash
|
||||
nano ~/wol-server/.env
|
||||
```
|
||||
|
||||
### Available Configuration Options
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| `SERVER_NAME` | Hostname/IP of target server | pippo |
|
||||
| `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
|
||||
sudo systemctl restart wol-server
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Access the web interface by navigating to `http://raspberry-pi-ip:8080` in your browser.
|
||||
### Accessing the Interface
|
||||
|
||||
The interface provides:
|
||||
Open a web browser and navigate to:
|
||||
```
|
||||
http://your-pi-ip:8080
|
||||
```
|
||||
|
||||
- Current server status (Online/Offline)
|
||||
- Boot button - sends a Wake-on-LAN magic packet to your server
|
||||
- Shutdown button - safely shuts down your server via SSH
|
||||
- Refresh button - manually updates the status display
|
||||
### Features
|
||||
|
||||
- **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
|
||||
|
||||
### Checking Service Status
|
||||
|
||||
```bash
|
||||
sudo systemctl status wol-server
|
||||
```
|
||||
|
||||
### Viewing Logs
|
||||
|
||||
```bash
|
||||
sudo journalctl -u wol-server -f
|
||||
```
|
||||
|
||||
### Updating
|
||||
|
||||
To update to a newer version:
|
||||
|
||||
1. Download and transfer the latest release
|
||||
2. Stop the service:
|
||||
```bash
|
||||
sudo systemctl stop wol-server
|
||||
```
|
||||
3. Extract the new files:
|
||||
```bash
|
||||
tar -xzf wol-server.tar.gz
|
||||
```
|
||||
4. Run the install script:
|
||||
```bash
|
||||
./install.sh
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Server won't boot**:
|
||||
- Verify that Wake-on-LAN is enabled in your server's BIOS/UEFI
|
||||
- Confirm the MAC address is correct
|
||||
- Check if your router blocks Wake-on-LAN packets
|
||||
### Service Won't Start
|
||||
|
||||
- **Shutdown doesn't work**:
|
||||
- Verify SSH key setup
|
||||
- Check sudo configuration on target server
|
||||
- Test manual SSH command: `ssh user@server sudo shutdown -h now`
|
||||
Check for template errors:
|
||||
```bash
|
||||
ls -la ~/wol-server/templates/
|
||||
```
|
||||
|
||||
- **Web interface not accessible**:
|
||||
- Ensure the service is running: `sudo systemctl status wol-server`
|
||||
- Check for firewall rules blocking port 8080
|
||||
- Verify the Raspberry Pi is connected to the network
|
||||
Verify the .env file exists:
|
||||
```bash
|
||||
cat ~/wol-server/.env
|
||||
```
|
||||
|
||||
## Contributing
|
||||
### Boot Command Not Working
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
1. Ensure `wakeonlan` is installed:
|
||||
```bash
|
||||
which wakeonlan || sudo apt-get install wakeonlan
|
||||
```
|
||||
2. Verify the MAC address is correct in your .env file
|
||||
3. Make sure the target device is properly configured for Wake-on-LAN
|
||||
|
||||
## License
|
||||
### Shutdown Not Working
|
||||
|
||||
1. Verify `sshpass` is installed:
|
||||
```bash
|
||||
which sshpass || sudo apt-get install sshpass
|
||||
```
|
||||
2. Check that the SERVER_USER setting in .env is correct
|
||||
3. Ensure SSH access is working between your Pi and the target server
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Running on a Different Port
|
||||
|
||||
Edit the `.env` file:
|
||||
```bash
|
||||
echo "PORT=8181" >> ~/wol-server/.env
|
||||
```
|
||||
|
||||
### Multiple Target Machines
|
||||
|
||||
To control multiple devices, you can install multiple instances:
|
||||
```bash
|
||||
# Create a second instance
|
||||
mkdir -p ~/wol-server2/templates
|
||||
cp -r ~/wol-server/templates/* ~/wol-server2/templates/
|
||||
cp ~/wol-server/wol-server ~/wol-server2/
|
||||
|
||||
# Different config
|
||||
cat > ~/wol-server2/.env << EOL
|
||||
SERVER_NAME=server2
|
||||
SERVER_USER=admin
|
||||
MAC_ADDRESS=aa:bb:cc:dd:ee:ff
|
||||
PORT=8081
|
||||
EOL
|
||||
|
||||
# Create a new service
|
||||
sudo cp /etc/systemd/system/wol-server.service /etc/systemd/system/wol-server2.service
|
||||
sudo sed -i 's|/home/pi/wol-server|/home/pi/wol-server2|g' /etc/systemd/system/wol-server2.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable wol-server2
|
||||
sudo systemctl start wol-server2
|
||||
```
|
||||
|
||||
## Project Information
|
||||
|
||||
Designed for use with Raspberry Pi to provide a simple way to manage servers and devices on your local network. The web interface makes it easy to power on and off machines without having to remember MAC addresses or commands.
|
||||
|
||||
### Contributing
|
||||
|
||||
Contributions are welcome! Feel free to submit pull requests or open issues to help improve this project.
|
||||
|
||||
### License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Inspired by the need for a simple, lightweight server power management tool
|
||||
- Thanks to the Go community for the excellent standard library that makes web development straightforward
|
||||
|
|
|
|||
5
go.mod
Normal file
5
go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module github.com/thisloke/wol-server
|
||||
|
||||
go 1.20
|
||||
|
||||
require github.com/joho/godotenv v1.5.1 // indirect
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
252
handlers.go
Normal file
252
handlers.go
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Handle the root route - show status
|
||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
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
|
||||
if !online {
|
||||
status = "Offline"
|
||||
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,
|
||||
}
|
||||
|
||||
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 boot request
|
||||
func bootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !isServerOnline() {
|
||||
// Boot the server using wakeonlan
|
||||
err := sendWakeOnLAN()
|
||||
if err != nil {
|
||||
log.Printf("Error booting server: %v", err)
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
} 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,
|
||||
}
|
||||
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 request
|
||||
func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||
online := isServerOnline()
|
||||
|
||||
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,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: "Online",
|
||||
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"),
|
||||
}
|
||||
|
||||
// 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 actual shutdown request
|
||||
func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only process POST requests for security
|
||||
if r.Method != "POST" {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Use the password from environment variable
|
||||
if shutdownPassword == "" {
|
||||
log.Printf("SHUTDOWN_PASSWORD not set in environment, cannot perform shutdown")
|
||||
// Show error message
|
||||
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,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if isServerOnline() {
|
||||
// Shutdown the server using the password from .env file
|
||||
err := shutdownServer(shutdownPassword)
|
||||
if err != nil {
|
||||
log.Printf("Error shutting down server: %v", err)
|
||||
|
||||
// Show error message
|
||||
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,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
} 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,
|
||||
}
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
793
main.go
793
main.go
|
|
@ -1,417 +1,462 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
const (
|
||||
serverName = "delemaco" // Server to ping
|
||||
macAddress = "b8:cb:29:a1:f3:88" // MAC address of the server
|
||||
port = "8080" // Port to listen on
|
||||
// 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
|
||||
)
|
||||
|
||||
// StatusData holds data for the HTML template
|
||||
type StatusData struct {
|
||||
Server string
|
||||
Status string
|
||||
Color string
|
||||
IsTestMode bool
|
||||
}
|
||||
|
||||
// Check if server is online
|
||||
func isServerOnline() bool {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// macOS and Linux have slightly different ping commands
|
||||
if runtime.GOOS == "darwin" {
|
||||
cmd = exec.Command("ping", "-c", "1", "-W", "1000", serverName)
|
||||
} else {
|
||||
cmd = exec.Command("ping", "-c", "1", "-W", "1", serverName)
|
||||
func loadEnvVariables() {
|
||||
// Load .env file if it exists
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Println("No .env file found, using default values")
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Send WOL packet
|
||||
func sendWakeOnLAN() error {
|
||||
log.Printf("Sending WOL packet to %s (%s)", serverName, macAddress)
|
||||
cmd := exec.Command("wakeonlan", macAddress)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// Shutdown server
|
||||
func shutdownServer() error {
|
||||
|
||||
// Real shutdown command for Linux
|
||||
log.Printf("Sending shutdown command to %s", serverName)
|
||||
cmd := exec.Command("ssh", serverName, "sudo", "shutdown", "-h", "now")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// Handle the root route - show status
|
||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
// Override defaults with environment variables if they exist
|
||||
if envServerName := os.Getenv("SERVER_NAME"); envServerName != "" {
|
||||
serverName = envServerName
|
||||
}
|
||||
|
||||
online := isServerOnline()
|
||||
status := "Online"
|
||||
color := "#4caf50" // Material green
|
||||
if !online {
|
||||
status = "Offline"
|
||||
color = "#d32f2f" // Material red
|
||||
if envServerUser := os.Getenv("SERVER_USER"); envServerUser != "" {
|
||||
serverUser = envServerUser
|
||||
}
|
||||
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: status,
|
||||
Color: color,
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
if envMacAddress := os.Getenv("MAC_ADDRESS"); envMacAddress != "" {
|
||||
macAddress = envMacAddress
|
||||
}
|
||||
|
||||
renderTemplate(w, data)
|
||||
}
|
||||
if envPort := os.Getenv("PORT"); envPort != "" {
|
||||
port = envPort
|
||||
}
|
||||
|
||||
// Handle boot request
|
||||
func bootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !isServerOnline() {
|
||||
// Boot the server using wakeonlan
|
||||
err := sendWakeOnLAN()
|
||||
if err != nil {
|
||||
log.Printf("Error booting server: %v", err)
|
||||
// Load refresh interval if set
|
||||
if envRefresh := os.Getenv("REFRESH_INTERVAL"); envRefresh != "" {
|
||||
if val, err := strconv.Atoi(envRefresh); err == nil && val > 0 {
|
||||
refreshInterval = val
|
||||
}
|
||||
|
||||
// Display booting status
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: "Booting",
|
||||
Color: "#607d8b", // Material blue-gray
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
}
|
||||
renderTemplate(w, data)
|
||||
} else {
|
||||
// Server is already online
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: "Online",
|
||||
Color: "#4caf50", // Material green
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
}
|
||||
renderTemplate(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle shutdown request
|
||||
func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if isServerOnline() {
|
||||
// Shutdown the server
|
||||
err := shutdownServer()
|
||||
if err != nil {
|
||||
log.Printf("Error shutting down server: %v", err)
|
||||
}
|
||||
|
||||
// Display shutting down status
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: "Shutting down",
|
||||
Color: "#5d4037", // Material brown
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
}
|
||||
renderTemplate(w, data)
|
||||
} else {
|
||||
// Server is already offline
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: "Offline",
|
||||
Color: "#d32f2f", // Material red
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
}
|
||||
renderTemplate(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Render the HTML template
|
||||
func renderTemplate(w http.ResponseWriter, data StatusData) {
|
||||
tmpl, err := template.New("status").Parse(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Server Status: {{.Server}}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: {{.Color}};
|
||||
--text-color: white;
|
||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||
--hover-color: rgba(255, 255, 255, 0.1);
|
||||
--card-bg: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--primary-color);
|
||||
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
padding: 20px;
|
||||
transition: background-color 0.5s ease;
|
||||
background-image: radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.05) 0%, transparent 20%),
|
||||
radial-gradient(circle at 90% 80%, rgba(255, 255, 255, 0.05) 0%, transparent 20%);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(5px);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 15px 25px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
background-color: var(--shadow-color);
|
||||
transition: transform 0.2s ease, background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--hover-color);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.button::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.button.refresh::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-160q-133 0-226.5-93.5T160-480q0-133 93.5-226.5T480-800q85 0 149 34.5T740-671v-99q0-13 8.5-21.5T770-800q13 0 21.5 8.5T800-770v194q0 13-8.5 21.5T770-546H576q-13 0-21.5-8.5T546-576q0-13 8.5-21.5T576-606h138q-38-60-97-97t-137-37q-109 0-184.5 75.5T220-480q0 109 75.5 184.5T480-220q59 0 111-25t89-69q8-9 20.5-10t21.5 7q9 8 10 20t-7 22q-45 53-112 86.5T480-160Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.boot::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-151 0-255.5-104.5T120-480q0-138 89-239t219-120q20-3 33.5 9.5T480-797q4 20-9 35.5T437-748q-103 12-170 87t-67 181q0 124 88 212t212 88q124 0 212-88t88-212q0-109-69.5-184.5T564-748q-21-3-31.5-19T525-798q3-20 19-30.5t35-6.5q136 19 228.5 122.5T900-480q0 150-104.5 255T480-120Zm0-170q-20 0-33.5-14T433-340v-286q0-21 14-34.5t33-13.5q20 0 33.5 13.5T527-626v286q0 22-14 36t-33 14Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.shutdown::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-151 0-255.5-104.5T120-480q0-138 89-239t219-120q20-3 33.5 9.5T480-797q4 20-9 35.5T437-748q-103 12-170 87t-67 181q0 124 88 212t212 88q124 0 212-88t88-212q0-109-69.5-184.5T564-748q-21-3-31.5-19T525-798q3-20 19-30.5t35-6.5q136 19 228.5 122.5T900-480q0 150-104.5 255T480-120Zm0-360Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.test-panel {
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 15px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.test-note {
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 600px) {
|
||||
.card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Status-specific icons */
|
||||
{{if eq .Status "Online"}}
|
||||
.status-icon::before {
|
||||
content: "✓";
|
||||
color: #4caf50;
|
||||
}
|
||||
{{else if eq .Status "Offline"}}
|
||||
.status-icon::before {
|
||||
content: "✗";
|
||||
color: #f44336;
|
||||
}
|
||||
{{else if eq .Status "Booting"}}
|
||||
.status-icon::before {
|
||||
content: "⟳";
|
||||
color: #ffeb3b;
|
||||
display: inline-block;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
{{else if eq .Status "Shutting down"}}
|
||||
.status-icon::before {
|
||||
content: "⏻";
|
||||
color: #ff9800;
|
||||
}
|
||||
{{end}}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="status-icon"></div>
|
||||
<h1 class="status-text">{{.Status}}</h1>
|
||||
<div class="server-name">Server: <strong>{{.Server}}</strong></div>
|
||||
|
||||
<div class="controls">
|
||||
<a href="/" class="button refresh">Refresh</a>
|
||||
<a href="/boot" class="button boot">Boot</a>
|
||||
<a href="/shutdown" class="button shutdown">Shutdown</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .IsTestMode}}
|
||||
<div class="test-panel">
|
||||
<div class="test-note">
|
||||
Running on macOS in test mode. Wake-on-LAN packets are sent, but remote shutdown is simulated.
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="footer">
|
||||
Wake-on-LAN Server Control Panel
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to load template", http.StatusInternalServerError)
|
||||
log.Printf("Template error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s, REFRESH=%d",
|
||||
serverName, serverUser, macAddress, port, refreshInterval)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Load environment variables
|
||||
loadEnvVariables()
|
||||
|
||||
// Setup template
|
||||
if err := setupTemplate(); err != nil {
|
||||
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("/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)
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
log.Println("Running on macOS in test mode - remote shutdown commands will be simulated")
|
||||
log.Println("Running on macOS - commands will be executed using the provided password")
|
||||
}
|
||||
|
||||
if err := http.ListenAndServe(listenAddr, nil); err != nil {
|
||||
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 based on the current time
|
||||
now := time.Now()
|
||||
currentTimeStr := now.Format("15:04")
|
||||
|
||||
// Check if we're within the schedule window (between start and end time)
|
||||
if currentTimeStr == scheduleConfig.StartTime {
|
||||
log.Printf("STARTUP MATCH: Current time %s matches start time, attempting boot", currentTimeStr)
|
||||
if !isServerOnline() {
|
||||
sendWakeOnLAN()
|
||||
}
|
||||
} else if currentTimeStr > scheduleConfig.StartTime && currentTimeStr < scheduleConfig.EndTime {
|
||||
log.Printf("STARTUP CHECK: Current time %s is within schedule window, checking server", currentTimeStr)
|
||||
if !isServerOnline() {
|
||||
log.Printf("Server should be online based on schedule - attempting boot")
|
||||
sendWakeOnLAN()
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
// Check if server should be on according to schedule
|
||||
shouldBeOn := CheckSchedule()
|
||||
serverIsOn := isServerOnline()
|
||||
|
||||
// Log schedule status (debug level)
|
||||
if scheduleConfig.Enabled {
|
||||
log.Printf("Schedule check: Window active=%v, Server online=%v", shouldBeOn, serverIsOn)
|
||||
|
||||
// Force check the current time against the schedule time
|
||||
now := time.Now()
|
||||
currentTimeStr := now.Format("15:04")
|
||||
if currentTimeStr == scheduleConfig.StartTime {
|
||||
log.Printf("EXACT TIME MATCH! Current time %s equals start time", currentTimeStr)
|
||||
shouldBeOn = true
|
||||
} else if currentTimeStr == scheduleConfig.EndTime {
|
||||
log.Printf("EXACT END TIME MATCH! Current time %s equals end time", currentTimeStr)
|
||||
shouldBeOn = false
|
||||
}
|
||||
}
|
||||
|
||||
if shouldBeOn && !serverIsOn {
|
||||
log.Println("Schedule: Server should be on but is offline. BOOT ATTEMPT INITIATED...")
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
} else if !shouldBeOn && serverIsOn {
|
||||
// Check if auto-shutdown is enabled
|
||||
if scheduleConfig.AutoShutdown && shutdownPassword != "" && scheduleConfig.StartedBySchedule {
|
||||
log.Println("Schedule: Server is on outside of scheduled window - 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 if !scheduleConfig.StartedBySchedule {
|
||||
// Server wasn't started by scheduler
|
||||
log.Println("Schedule: Server is on outside of scheduled window but was not started by scheduler")
|
||||
} else {
|
||||
// Just log the situation when auto-shutdown is not enabled
|
||||
log.Println("Schedule: Server is on outside of scheduled window")
|
||||
}
|
||||
}
|
||||
|
||||
// Update last run timestamp if we're exiting the window
|
||||
// This helps track when the schedule was last active
|
||||
currentConfig := GetScheduleConfig()
|
||||
if currentConfig.Enabled && !shouldBeOn && 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
schedule.json
Normal file
9
schedule.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"enabled": false,
|
||||
"startTime": "",
|
||||
"endTime": "",
|
||||
"frequency": "daily",
|
||||
"lastRun": "",
|
||||
"autoShutdown": false,
|
||||
"startedBySchedule": false
|
||||
}
|
||||
773
templates/status.html
Normal file
773
templates/status.html
Normal file
|
|
@ -0,0 +1,773 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
http-equiv="refresh"
|
||||
content="{{if .RefreshInterval}}{{.RefreshInterval}}{{else}}60{{end}}"
|
||||
/>
|
||||
<title>Server Status: {{.Server}}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: {{.Color}};
|
||||
--text-color: white;
|
||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||
--hover-color: rgba(255, 255, 255, 0.1);
|
||||
--card-bg: rgba(0, 0, 0, 0.15);
|
||||
--danger-color: #f44336;
|
||||
--success-color: #4caf50;
|
||||
--modal-bg: rgba(0, 0, 0, 0.85);
|
||||
--error-color: #ff6b6b;
|
||||
--info-color: #2196f3;
|
||||
--warning-color: #ff9800;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--primary-color);
|
||||
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
padding: 20px;
|
||||
transition: background-color 0.5s ease;
|
||||
background-image: radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.05) 0%, transparent 20%),
|
||||
radial-gradient(circle at 90% 80%, rgba(255, 255, 255, 0.05) 0%, transparent 20%);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(5px);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.schedule-card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(5px);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.schedule-header {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.schedule-status {
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.schedule-status.active {
|
||||
background-color: rgba(76, 175, 80, 0.2);
|
||||
border: 1px solid rgba(76, 175, 80, 0.5);
|
||||
}
|
||||
|
||||
.schedule-status.inactive {
|
||||
background-color: rgba(244, 67, 54, 0.2);
|
||||
border: 1px solid rgba(244, 67, 54, 0.5);
|
||||
}
|
||||
|
||||
.schedule-info {
|
||||
margin-bottom: 25px;
|
||||
padding: 15px;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.schedule-info p {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.schedule-info p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 15px 25px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
background-color: var(--shadow-color);
|
||||
transition: transform 0.2s ease, background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--hover-color);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.button.danger {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.button.success {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.button.submit {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.button::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.button.refresh::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-160q-133 0-226.5-93.5T160-480q0-133 93.5-226.5T480-800q85 0 149 34.5T740-671v-99q0-13 8.5-21.5T770-800q13 0 21.5 8.5T800-770v194q0 13-8.5 21.5T770-546H576q-13 0-21.5-8.5T546-576q0-13 8.5-21.5T576-606h138q-38-60-97-97t-137-37q-109 0-184.5 75.5T220-480q0 109 75.5 184.5T480-220q59 0 111-25t89-69q8-9 20.5-10t21.5 7q9 8 10 20t-7 22q-45 53-112 86.5T480-160Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.boot::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-151 0-255.5-104.5T120-480q0-138 89-239t219-120q20-3 33.5 9.5T480-797q4 20-9 35.5T437-748q-103 12-170 87t-67 181q0 124 88 212t212 88q124 0 212-88t88-212q0-109-69.5-184.5T564-748q-21-3-31.5-19T525-798q3-20 19-30.5t35-6.5q136 19 228.5 122.5T900-480q0 150-104.5 255T480-120Zm0-170q-20 0-33.5-14T433-340v-286q0-21 14-34.5t33-13.5q20 0 33.5 13.5T527-626v286q0 22-14 36t-33 14Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.shutdown::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-151 0-255.5-104.5T120-480q0-138 89-239t219-120q20-3 33.5 9.5T480-797q4 20-9 35.5T437-748q-103 12-170 87t-67 181q0 124 88 212t212 88q124 0 212-88t88-212q0-109-69.5-184.5T564-748q-21-3-31.5-19T525-798q3-20 19-30.5t35-6.5q136 19 228.5 122.5T900-480q0 150-104.5 255T480-120Zm0-360Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.schedule::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T120-480q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-120Zm0-82q117 0 198.5-81.5T760-482q0-117-81.5-198.5T480-762q-117 0-198.5 81.5T200-482q0 117 81.5 198.5T480-202Zm0-280Zm-40 202-170-170 56-56 114 113 226-226 56 56-282 283Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.disable::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Zm-40 120-56-56 40-40-40-40 56-56 40 40 40-40 56 56-40 40 40 40-56 56-40-40-40 40Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.submit::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.test-panel {
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 15px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.test-note {
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--modal-bg);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #1e1e1e;
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
input[type="datetime-local"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
accent-color: var(--success-color);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
margin-top: 5px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.shutdown-warning {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(255, 152, 0, 0.2);
|
||||
border: 1px solid rgba(255, 152, 0, 0.5);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--error-color);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
border-radius: 50px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background-color: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge.inactive {
|
||||
background-color: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 600px) {
|
||||
.card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-actions .button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Status-specific icons */
|
||||
{{if eq .Status "Online"}}
|
||||
.status-icon::before {
|
||||
content: "✓";
|
||||
color: #4caf50;
|
||||
}
|
||||
{{else if eq .Status "Offline"}}
|
||||
.status-icon::before {
|
||||
content: "✗";
|
||||
color: #f44336;
|
||||
}
|
||||
{{else if eq .Status "Booting"}}
|
||||
.status-icon::before {
|
||||
content: "⟳";
|
||||
color: #ffeb3b;
|
||||
display: inline-block;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
{{else if eq .Status "Shutting down"}}
|
||||
.status-icon::before {
|
||||
content: "⏻";
|
||||
color: #ff9800;
|
||||
}
|
||||
{{end}}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="status-icon"></div>
|
||||
<h1 class="status-text">{{.Status}}</h1>
|
||||
<div class="server-name">Server: <strong>{{.Server}}</strong></div>
|
||||
|
||||
<div class="controls">
|
||||
<a href="/" class="button refresh">Refresh</a>
|
||||
<a href="/boot" class="button boot">Boot</a>
|
||||
<a href="/confirm-shutdown" class="button shutdown">Shutdown</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduled Backup Window -->
|
||||
<div class="schedule-card">
|
||||
<h2 class="schedule-header">Scheduled Backup Window</h2>
|
||||
|
||||
{{if .Schedule.Enabled}}
|
||||
<div class="schedule-status active">
|
||||
Automatic scheduled backup is active
|
||||
</div>
|
||||
<div class="schedule-info">
|
||||
<p>
|
||||
<span>Start Time:</span>
|
||||
<span>{{.Schedule.StartTime}}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>End Time:</span>
|
||||
<span>{{.Schedule.EndTime}}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>Frequency:</span>
|
||||
<span>{{.Schedule.Frequency}}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>Auto Shutdown:</span>
|
||||
<span
|
||||
>{{if .Schedule.AutoShutdown}}
|
||||
<span class="badge active">Enabled</span>
|
||||
{{else}}
|
||||
<span class="badge inactive">Disabled</span>
|
||||
{{end}}</span
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<span>Last Update:</span>
|
||||
<span>{{.LastUpdated}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button id="editSchedule" class="button schedule">
|
||||
Edit Schedule
|
||||
</button>
|
||||
<button id="disableSchedule" class="button disable">
|
||||
Disable Schedule
|
||||
</button>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="schedule-status inactive">No active backup schedule</div>
|
||||
<div class="controls">
|
||||
<button id="enableSchedule" class="button schedule">
|
||||
Configure Schedule
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .IsTestMode}}
|
||||
<div class="test-panel">
|
||||
<div class="test-note">
|
||||
Running on macOS. Commands will be executed using the provided
|
||||
password.
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="footer">Wake-on-LAN Server Control Panel</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Configuration Modal -->
|
||||
<div id="scheduleModal" class="modal-overlay" style="display: none">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Configure Backup Schedule</div>
|
||||
<div class="modal-body">
|
||||
<form id="scheduleForm">
|
||||
<div class="form-group">
|
||||
<label for="startTime" class="form-label">Start Time:</label>
|
||||
<input
|
||||
type="time"
|
||||
id="startTime"
|
||||
name="startTime"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="endTime" class="form-label">End Time:</label>
|
||||
<input
|
||||
type="time"
|
||||
id="endTime"
|
||||
name="endTime"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="frequency" class="form-label">Frequency:</label>
|
||||
<select id="frequency" name="frequency" class="form-input">
|
||||
<option value="daily">Every day</option>
|
||||
<option value="every2days">Every 2 days</option>
|
||||
<option value="weekly">Once a week</option>
|
||||
<option value="monthly">Once a month</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox-wrapper">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="autoShutdown"
|
||||
name="autoShutdown"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
<label for="autoShutdown"
|
||||
>Enable automatic shutdown at end time</label
|
||||
>
|
||||
</div>
|
||||
<div class="form-help">
|
||||
When enabled, the server will automatically shut down at the
|
||||
specified end time. Make sure SSH access is properly configured.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-help shutdown-warning">
|
||||
<strong>Note:</strong> To use automatic shutdown, add
|
||||
SHUTDOWN_PASSWORD to your .env file. The server will only be shut
|
||||
down automatically if it was started by the scheduler.
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="scheduleError"
|
||||
class="error-message"
|
||||
style="display: none"
|
||||
></div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="cancelSchedule" class="button">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="button submit">Save Schedule</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .ConfirmShutdown}}
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Confirm Shutdown</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to shut down <strong>{{.Server}}</strong>?<br />
|
||||
This will immediately power off the server.
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<a href="/" class="button">Cancel</a>
|
||||
<form action="/shutdown" method="POST" style="display: inline">
|
||||
<button type="submit" class="button danger">Yes, Shutdown</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}} {{if .ErrorMessage}}
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Error</div>
|
||||
<div class="modal-body">
|
||||
<div class="error-message">{{.ErrorMessage}}</div>
|
||||
|
||||
<div class="shutdown-warning" style="margin-top: 20px">
|
||||
<p>
|
||||
To use the shutdown feature, add SHUTDOWN_PASSWORD to your .env
|
||||
file.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<a href="/" class="button">Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<script>
|
||||
// Schedule modal handling
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const scheduleModal = document.getElementById("scheduleModal");
|
||||
const enableScheduleBtn = document.getElementById("enableSchedule");
|
||||
const disableScheduleBtn = document.getElementById("disableSchedule");
|
||||
const cancelScheduleBtn = document.getElementById("cancelSchedule");
|
||||
const scheduleForm = document.getElementById("scheduleForm");
|
||||
const scheduleError = document.getElementById("scheduleError");
|
||||
|
||||
// Show schedule modal
|
||||
if (enableScheduleBtn) {
|
||||
enableScheduleBtn.addEventListener("click", function () {
|
||||
scheduleModal.style.display = "flex";
|
||||
});
|
||||
}
|
||||
|
||||
// Handle auto shutdown checkbox
|
||||
const autoShutdownCheckbox = document.getElementById("autoShutdown");
|
||||
|
||||
// Edit schedule button
|
||||
const editScheduleBtn = document.getElementById("editSchedule");
|
||||
if (editScheduleBtn) {
|
||||
editScheduleBtn.addEventListener("click", function() {
|
||||
// Pre-fill form with current values
|
||||
document.getElementById("startTime").value = "{{.Schedule.StartTime}}";
|
||||
document.getElementById("endTime").value = "{{.Schedule.EndTime}}";
|
||||
document.getElementById("frequency").value = "{{.Schedule.Frequency}}";
|
||||
document.getElementById("autoShutdown").checked = {{.Schedule.AutoShutdown}};
|
||||
|
||||
scheduleModal.style.display = "flex";
|
||||
});
|
||||
}
|
||||
|
||||
// Hide schedule modal
|
||||
if (cancelScheduleBtn) {
|
||||
cancelScheduleBtn.addEventListener("click", function () {
|
||||
scheduleModal.style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
// Handle disable schedule
|
||||
if (disableScheduleBtn) {
|
||||
disableScheduleBtn.addEventListener("click", function () {
|
||||
// Disable schedule via API
|
||||
fetch("/api/schedule", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enabled: false,
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
frequency: "daily",
|
||||
autoShutdown: false,
|
||||
shutdownPassword: "",
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
// Refresh page to show updated status
|
||||
window.location.reload();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
alert("Failed to disable schedule. Please try again.");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle schedule form submission
|
||||
if (scheduleForm) {
|
||||
scheduleForm.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// We no longer use these variables - validation happens with startTime/endTime below
|
||||
|
||||
// Get form values
|
||||
const startTime = document.getElementById("startTime").value;
|
||||
const endTime = document.getElementById("endTime").value;
|
||||
const frequency = document.getElementById("frequency").value;
|
||||
|
||||
// Simple validation for times
|
||||
if (!startTime || !endTime) {
|
||||
scheduleError.textContent =
|
||||
"Please enter both start and end times";
|
||||
scheduleError.style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
// Get auto shutdown settings
|
||||
const autoShutdown =
|
||||
document.getElementById("autoShutdown").checked;
|
||||
|
||||
// Submit to API
|
||||
fetch("/api/schedule", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
frequency: frequency,
|
||||
autoShutdown: autoShutdown,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
// Refresh page to show updated status
|
||||
window.location.reload();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
scheduleError.textContent =
|
||||
"Failed to save schedule. Please try again.";
|
||||
scheduleError.style.display = "block";
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
661
utils.go
Normal file
661
utils.go
Normal file
|
|
@ -0,0 +1,661 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScheduleConfig holds the backup window schedule settings
|
||||
type ScheduleConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
StartTime string `json:"startTime"` // Format: "HH:MM" (24-hour)
|
||||
EndTime string `json:"endTime"` // Format: "HH:MM" (24-hour)
|
||||
Frequency string `json:"frequency"` // "daily", "every2days", "weekly", "monthly"
|
||||
LastRun string `json:"lastRun"` // ISO8601 format - when the schedule last ran
|
||||
AutoShutdown bool `json:"autoShutdown"` // Whether to automatically shut down at end time
|
||||
StartedBySchedule bool `json:"startedBySchedule"` // Whether server was started by scheduler
|
||||
}
|
||||
|
||||
// StatusData holds data for the HTML template
|
||||
type StatusData struct {
|
||||
Server string
|
||||
Status string
|
||||
Color string
|
||||
IsTestMode bool
|
||||
ConfirmShutdown bool
|
||||
AskPassword bool
|
||||
ErrorMessage string
|
||||
Schedule ScheduleConfig
|
||||
LastUpdated string
|
||||
RefreshInterval int
|
||||
}
|
||||
|
||||
var tmpl *template.Template
|
||||
var scheduleConfig ScheduleConfig
|
||||
var scheduleConfigPath = "schedule.json"
|
||||
var shutdownPassword string // Will be loaded from environment
|
||||
|
||||
// Setup the HTML template
|
||||
func setupTemplate() error {
|
||||
// Check if templates directory exists, create if not
|
||||
if _, err := os.Stat("templates"); os.IsNotExist(err) {
|
||||
if err := os.Mkdir("templates", 0755); err != nil {
|
||||
return fmt.Errorf("failed to create templates directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Path to the template file
|
||||
templatePath := filepath.Join("templates", "status.html")
|
||||
|
||||
// Check if the template file exists
|
||||
if _, err := os.Stat(templatePath); os.IsNotExist(err) {
|
||||
log.Printf("Template file not found at %s. Please create it.", templatePath)
|
||||
return fmt.Errorf("template file not found: %s", templatePath)
|
||||
}
|
||||
|
||||
// Parse the template from the file
|
||||
var err error
|
||||
tmpl, err = template.ParseFiles(templatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse template: %v", err)
|
||||
}
|
||||
|
||||
// Load schedule config
|
||||
err = loadScheduleConfig()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to load schedule config: %v", err)
|
||||
// Continue with default (empty) schedule config
|
||||
}
|
||||
|
||||
// Check for required system tools
|
||||
checkRequiredTools()
|
||||
|
||||
// Load shutdown password from environment
|
||||
shutdownPassword = os.Getenv("SHUTDOWN_PASSWORD")
|
||||
if shutdownPassword == "" {
|
||||
log.Println("SHUTDOWN_PASSWORD not set in environment. Automatic shutdown will be disabled.")
|
||||
} else {
|
||||
log.Println("SHUTDOWN_PASSWORD loaded from environment")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if required system tools are available
|
||||
func checkRequiredTools() {
|
||||
// Check for wakeonlan command
|
||||
if _, err := exec.LookPath("wakeonlan"); err != nil {
|
||||
if _, err := exec.LookPath("etherwake"); err != nil {
|
||||
if _, err := exec.LookPath("wol"); err != nil {
|
||||
log.Printf("WARNING: No Wake-on-LAN tools found. Please install wakeonlan, etherwake, or wol package.")
|
||||
log.Printf("Installation instructions:")
|
||||
log.Printf(" - For Debian/Ubuntu: sudo apt-get install wakeonlan")
|
||||
log.Printf(" - For macOS: brew install wakeonlan")
|
||||
log.Printf(" - For Windows: Download from https://www.depicus.com/wake-on-lan/wake-on-lan-cmd")
|
||||
} else {
|
||||
log.Printf("Using 'wol' for Wake-on-LAN functionality")
|
||||
}
|
||||
} else {
|
||||
log.Printf("Using 'etherwake' for Wake-on-LAN functionality")
|
||||
}
|
||||
} else {
|
||||
log.Printf("Found 'wakeonlan' command for Wake-on-LAN functionality")
|
||||
}
|
||||
|
||||
// Check for ping command (needed for server status checks)
|
||||
if _, err := exec.LookPath("ping"); err != nil {
|
||||
log.Printf("WARNING: 'ping' command not found. Server status checks may fail.")
|
||||
}
|
||||
}
|
||||
|
||||
// Load schedule configuration from file
|
||||
func loadScheduleConfig() error {
|
||||
// Check if config file exists
|
||||
if _, err := os.Stat(scheduleConfigPath); os.IsNotExist(err) {
|
||||
// Create default config
|
||||
scheduleConfig = ScheduleConfig{
|
||||
Enabled: false,
|
||||
StartTime: "",
|
||||
EndTime: "",
|
||||
Frequency: "daily",
|
||||
LastRun: "",
|
||||
AutoShutdown: false,
|
||||
StartedBySchedule: false,
|
||||
}
|
||||
// Save default config
|
||||
return saveScheduleConfig()
|
||||
}
|
||||
|
||||
// Read the file
|
||||
data, err := os.ReadFile(scheduleConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read schedule config file: %v", err)
|
||||
}
|
||||
|
||||
// Unmarshal JSON data
|
||||
err = json.Unmarshal(data, &scheduleConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse schedule config: %v", err)
|
||||
}
|
||||
|
||||
// Log loaded configuration for debugging
|
||||
log.Printf("Loaded schedule config: Enabled=%v, StartTime=%s, EndTime=%s, Frequency=%s",
|
||||
scheduleConfig.Enabled, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save schedule configuration to file
|
||||
func saveScheduleConfig() error {
|
||||
data, err := json.MarshalIndent(scheduleConfig, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal schedule config: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(scheduleConfigPath, data, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save schedule config: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetScheduleConfig returns the current schedule config
|
||||
func GetScheduleConfig() ScheduleConfig {
|
||||
return scheduleConfig
|
||||
}
|
||||
|
||||
// UpdateScheduleConfig updates the schedule configuration
|
||||
func UpdateScheduleConfig(newConfig ScheduleConfig) error {
|
||||
scheduleConfig = newConfig
|
||||
return saveScheduleConfig()
|
||||
}
|
||||
|
||||
// CheckSchedule checks if server should be on/off based on schedule
|
||||
func CheckSchedule() (shouldBeOn bool) {
|
||||
// If schedule is not enabled, do nothing
|
||||
if !scheduleConfig.Enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
// If start time or end time is empty, the schedule is not properly configured
|
||||
if scheduleConfig.StartTime == "" || scheduleConfig.EndTime == "" {
|
||||
log.Printf("Schedule configuration incomplete: StartTime=%s, EndTime=%s",
|
||||
scheduleConfig.StartTime, scheduleConfig.EndTime)
|
||||
return false
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
today := now.Format("2006-01-02")
|
||||
|
||||
// Get current time as just hours and minutes for direct string comparison first
|
||||
currentTimeStr := now.Format("15:04")
|
||||
|
||||
// Log the exact time comparison we're doing
|
||||
log.Printf("Schedule debug: Current=%s, Start=%s, End=%s, LastRun=%s",
|
||||
currentTimeStr, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.LastRun)
|
||||
|
||||
// Parse start time with proper error handling
|
||||
startTime, err := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.StartTime))
|
||||
if err != nil {
|
||||
log.Printf("Error parsing start time '%s': %v", scheduleConfig.StartTime, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse end time with proper error handling
|
||||
endTime, err := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.EndTime))
|
||||
if err != nil {
|
||||
log.Printf("Error parsing end time '%s': %v", scheduleConfig.EndTime, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// If end time is before start time, it means the window spans to the next day
|
||||
if endTime.Before(startTime) {
|
||||
endTime = endTime.AddDate(0, 0, 1)
|
||||
|
||||
// Special case: if we're after midnight but before the end time
|
||||
// we need to adjust the start time to be from yesterday
|
||||
if now.Before(endTime) && now.After(time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())) {
|
||||
startTime = startTime.AddDate(0, 0, -1)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the schedule should run today based on frequency
|
||||
// Check if we're in the schedule window
|
||||
if !shouldRunToday(now) {
|
||||
log.Printf("Schedule is active but not set to run today based on frequency: %s", scheduleConfig.Frequency)
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for auto shutdown at end time
|
||||
if currentTimeStr == scheduleConfig.EndTime {
|
||||
if scheduleConfig.AutoShutdown && shutdownPassword != "" && isServerOnline() {
|
||||
// Only shut down if the server was started by the scheduler
|
||||
if scheduleConfig.StartedBySchedule {
|
||||
log.Printf("Auto shutdown triggered at schedule end time %s", scheduleConfig.EndTime)
|
||||
|
||||
// Try up to 3 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 {
|
||||
log.Printf("Server was not started by scheduler, skipping auto shutdown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if current time is within the schedule window
|
||||
// Check if we're between start and end times
|
||||
if currentTimeStr == scheduleConfig.StartTime {
|
||||
log.Printf("Schedule match: Current time exactly matches start time")
|
||||
shouldBeOn = true
|
||||
} else if currentTimeStr == scheduleConfig.EndTime {
|
||||
log.Printf("Schedule end: Current time exactly matches end time")
|
||||
shouldBeOn = false
|
||||
|
||||
// Check if auto shutdown is enabled
|
||||
if scheduleConfig.AutoShutdown && shutdownPassword != "" && isServerOnline() {
|
||||
// Only shut down if the server was started by the scheduler
|
||||
if scheduleConfig.StartedBySchedule {
|
||||
log.Printf("Auto shutdown is enabled - attempting to shut down server")
|
||||
|
||||
// Try up to 3 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 {
|
||||
log.Printf("Server was not started by scheduler, skipping auto shutdown")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise check if we're within the window with a small buffer (30 seconds)
|
||||
shouldBeOn = (now.After(startTime.Add(-30*time.Second)) && now.Before(endTime))
|
||||
}
|
||||
|
||||
log.Printf("Schedule window check: Current=%v, Start=%v, End=%v, ShouldBeOn=%v",
|
||||
now.Format("15:04:05"), startTime.Format("15:04:05"), endTime.Format("15:04:05"), shouldBeOn)
|
||||
|
||||
// Explicitly check end time for better debugging
|
||||
if scheduleConfig.EndTime != "" && currentTimeStr == scheduleConfig.EndTime {
|
||||
log.Printf("EXACT END TIME MATCH! Current time %s equals end time - schedule window should close", currentTimeStr)
|
||||
}
|
||||
|
||||
// If we're in the schedule window, update the LastRun and attempt to boot the server
|
||||
if shouldBeOn {
|
||||
// Only update LastRun if it hasn't been set yet for this window
|
||||
if scheduleConfig.LastRun == "" {
|
||||
log.Printf("Entering scheduled backup window, updating LastRun timestamp")
|
||||
scheduleConfig.LastRun = now.Format(time.RFC3339)
|
||||
if err := saveScheduleConfig(); err != nil {
|
||||
log.Printf("Warning: Failed to save schedule config: %v", err)
|
||||
}
|
||||
|
||||
// Force a server boot attempt on window start
|
||||
if !isServerOnline() {
|
||||
log.Printf("Schedule window started - attempting to boot server")
|
||||
if err := sendWakeOnLAN(); err != nil {
|
||||
log.Printf("Schedule boot failed: %v", err)
|
||||
} else {
|
||||
// Mark that server was started by scheduler
|
||||
scheduleConfig.StartedBySchedule = true
|
||||
if err := saveScheduleConfig(); err != nil {
|
||||
log.Printf("Failed to save schedule config: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return shouldBeOn
|
||||
}
|
||||
|
||||
// shouldRunToday checks if the schedule should run today based on frequency
|
||||
func shouldRunToday(now time.Time) bool {
|
||||
// Special case: if we're within the current window (between start and end time),
|
||||
// we should consider the schedule to be active regardless of LastRun
|
||||
currentTimeStr := now.Format("15:04")
|
||||
today := now.Format("2006-01-02")
|
||||
|
||||
startTime, startErr := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.StartTime))
|
||||
endTime, endErr := time.Parse("2006-01-02 15:04", fmt.Sprintf("%s %s", today, scheduleConfig.EndTime))
|
||||
|
||||
if startErr == nil && endErr == nil {
|
||||
// If end time is before start time, it means the window spans to the next day
|
||||
if endTime.Before(startTime) {
|
||||
endTime = endTime.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
// If we're currently in the window, allow it to run
|
||||
if now.After(startTime) && now.Before(endTime) {
|
||||
log.Println("Currently within today's schedule window - schedule should be active")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If no previous run, allow it to run
|
||||
if scheduleConfig.LastRun == "" {
|
||||
log.Println("No previous run recorded, schedule can run today")
|
||||
return true
|
||||
}
|
||||
|
||||
lastRun, err := time.Parse(time.RFC3339, scheduleConfig.LastRun)
|
||||
if err != nil {
|
||||
log.Printf("Error parsing last run date '%s': %v", scheduleConfig.LastRun, err)
|
||||
// If we can't parse the date, better to let it run than to block it
|
||||
return true
|
||||
}
|
||||
|
||||
// Don't allow running multiple times on the same day unless
|
||||
// it's been reset explicitly (LastRun set to empty)
|
||||
if lastRun.Year() == now.Year() && lastRun.YearDay() == now.YearDay() {
|
||||
// Check if we've passed the end time today - if so, we can reset for tomorrow
|
||||
if scheduleConfig.EndTime != "" && currentTimeStr > scheduleConfig.EndTime {
|
||||
log.Println("Current time is after end time - resetting for next run")
|
||||
scheduleConfig.LastRun = ""
|
||||
saveScheduleConfig()
|
||||
return false
|
||||
}
|
||||
|
||||
log.Println("Schedule already ran today, skipping")
|
||||
return false
|
||||
}
|
||||
|
||||
switch scheduleConfig.Frequency {
|
||||
case "daily":
|
||||
// Run every day
|
||||
log.Println("Daily schedule: allowed to run today")
|
||||
return true
|
||||
case "every2days":
|
||||
// Check if at least 2 days have passed
|
||||
elapsed := now.Sub(lastRun)
|
||||
eligible := elapsed >= 48*time.Hour
|
||||
log.Printf("Every 2 days schedule: %v hours elapsed, eligible=%v", elapsed.Hours(), eligible)
|
||||
return eligible
|
||||
case "weekly":
|
||||
// Check if at least 7 days have passed
|
||||
elapsed := now.Sub(lastRun)
|
||||
eligible := elapsed >= 7*24*time.Hour
|
||||
log.Printf("Weekly schedule: %v days elapsed, eligible=%v", elapsed.Hours()/24, eligible)
|
||||
return eligible
|
||||
case "monthly":
|
||||
// Check if last run was in a different month
|
||||
sameMonth := lastRun.Month() == now.Month() && lastRun.Year() == now.Year()
|
||||
log.Printf("Monthly schedule: eligible=%v", !sameMonth)
|
||||
return !sameMonth
|
||||
default:
|
||||
log.Printf("Unknown frequency '%s', defaulting to daily", scheduleConfig.Frequency)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if server is online
|
||||
func isServerOnline() bool {
|
||||
var cmd *exec.Cmd
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
// macOS and Linux have slightly different ping commands
|
||||
if runtime.GOOS == "darwin" {
|
||||
cmd = exec.Command("ping", "-c", "1", "-W", "1000", serverName)
|
||||
} else {
|
||||
cmd = exec.Command("ping", "-c", "1", "-W", "1", serverName)
|
||||
}
|
||||
|
||||
// Capture output for debugging
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
log.Printf("Checking if server %s is online...", serverName)
|
||||
err := cmd.Run()
|
||||
|
||||
if err != nil {
|
||||
// Only log the full error in debug mode to avoid spamming the logs
|
||||
if stderr.String() != "" {
|
||||
log.Printf("Server %s is offline: %v - %s", serverName, err, stderr.String())
|
||||
} else {
|
||||
log.Printf("Server %s is offline", serverName)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
log.Printf("Server %s is online", serverName)
|
||||
return true
|
||||
}
|
||||
|
||||
// Send WOL packet
|
||||
func sendWakeOnLAN() error {
|
||||
log.Printf("Sending WOL packet to %s (%s)", serverName, macAddress)
|
||||
|
||||
// Check if wakeonlan command exists
|
||||
if _, err := exec.LookPath("wakeonlan"); err == nil {
|
||||
// Create the command
|
||||
cmd := exec.Command("wakeonlan", macAddress)
|
||||
|
||||
// Capture both stdout and stderr
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// Execute the command
|
||||
err := cmd.Run()
|
||||
|
||||
// Log the result
|
||||
if err != nil {
|
||||
log.Printf("WOL command failed: %v - stderr: %s", err, stderr.String())
|
||||
return fmt.Errorf("WOL command failed: %v - %s", err, stderr.String())
|
||||
}
|
||||
|
||||
output := stdout.String()
|
||||
if output != "" {
|
||||
log.Printf("WOL command output: %s", output)
|
||||
}
|
||||
|
||||
log.Printf("WOL packet sent successfully to %s", macAddress)
|
||||
return nil
|
||||
} else {
|
||||
// wakeonlan command not found, try etherwake
|
||||
if _, err := exec.LookPath("etherwake"); err == nil {
|
||||
log.Printf("Using etherwake as wakeonlan alternative")
|
||||
cmd := exec.Command("etherwake", macAddress)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Printf("etherwake command failed: %v - stderr: %s", err, stderr.String())
|
||||
return fmt.Errorf("etherwake command failed: %v - %s", err, stderr.String())
|
||||
}
|
||||
|
||||
log.Printf("WOL packet sent successfully via etherwake to %s", macAddress)
|
||||
return nil
|
||||
} else {
|
||||
// Try wol command as last resort
|
||||
if _, err := exec.LookPath("wol"); err == nil {
|
||||
log.Printf("Using wol as wakeonlan alternative")
|
||||
cmd := exec.Command("wol", macAddress)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Printf("wol command failed: %v - stderr: %s", err, stderr.String())
|
||||
return fmt.Errorf("wol command failed: %v - %s", err, stderr.String())
|
||||
}
|
||||
|
||||
log.Printf("WOL packet sent successfully via wol to %s", macAddress)
|
||||
return nil
|
||||
} else {
|
||||
// Implement a fallback pure Go WOL solution
|
||||
log.Printf("No WOL tools found. Please install wakeonlan, etherwake, or wol package.")
|
||||
log.Printf("Installation instructions:")
|
||||
log.Printf(" - For Debian/Ubuntu: sudo apt-get install wakeonlan")
|
||||
log.Printf(" - For macOS: brew install wakeonlan")
|
||||
log.Printf(" - For Windows: Download from https://www.depicus.com/wake-on-lan/wake-on-lan-cmd")
|
||||
|
||||
return fmt.Errorf("wakeonlan command not found in PATH. Please install wakeonlan tool")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown server with password
|
||||
func shutdownServer(password string) error {
|
||||
log.Printf("Sending shutdown command to %s", serverName)
|
||||
|
||||
var err error
|
||||
var stderr bytes.Buffer
|
||||
|
||||
// First try using sshpass with password
|
||||
if _, err := exec.LookPath("sshpass"); err == nil {
|
||||
log.Println("Using sshpass for authentication")
|
||||
log.Printf("Password being used: %s", password)
|
||||
|
||||
// Create the command
|
||||
cmdArgs := []string{
|
||||
"sshpass", "-p", password, "ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-o", "ConnectTimeout=10",
|
||||
fmt.Sprintf("%s@%s", serverUser, serverName),
|
||||
"sudo", "-S", "shutdown", "-h", "now",
|
||||
}
|
||||
log.Printf("Full command: %s", strings.Join(cmdArgs, " "))
|
||||
|
||||
// Add more SSH options to handle potential issues
|
||||
cmd := exec.Command("sshpass", "-p", password, "ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-o", "ConnectTimeout=10",
|
||||
fmt.Sprintf("%s@%s", serverUser, serverName),
|
||||
"sudo", "-S", "shutdown", "-h", "now")
|
||||
|
||||
// Capture stderr to log any error messages
|
||||
stderr.Reset()
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdin = bytes.NewBufferString(password + "\n")
|
||||
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
log.Println("SSH command executed successfully using sshpass")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("sshpass method failed: %v - %s", err, stderr.String())
|
||||
}
|
||||
|
||||
// Try direct SSH with password via stdin
|
||||
log.Println("Trying direct SSH with password via stdin")
|
||||
log.Printf("Password being used: %s", password)
|
||||
|
||||
// Create command args
|
||||
cmdArgs := []string{
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-o", "ConnectTimeout=10",
|
||||
fmt.Sprintf("%s@%s", serverUser, serverName),
|
||||
"sudo", "-S", "shutdown", "-h", "now",
|
||||
}
|
||||
log.Printf("Full command: %s", strings.Join(cmdArgs, " "))
|
||||
|
||||
cmd := exec.Command("ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-o", "ConnectTimeout=10",
|
||||
fmt.Sprintf("%s@%s", serverUser, serverName),
|
||||
"sudo", "-S", "shutdown", "-h", "now")
|
||||
|
||||
stderr.Reset()
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdin = bytes.NewBufferString(password + "\n")
|
||||
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
log.Println("SSH command executed successfully using direct SSH")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("SSH Error details: %s", stderr.String())
|
||||
|
||||
// Try a simpler shutdown command as a fallback
|
||||
log.Println("Trying simpler shutdown command as fallback")
|
||||
log.Printf("Password being used: %s", password)
|
||||
|
||||
// Create command args
|
||||
cmdArgs = []string{
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
fmt.Sprintf("%s@%s", serverUser, serverName),
|
||||
"sudo", "shutdown", "now",
|
||||
}
|
||||
log.Printf("Full command: %s", strings.Join(cmdArgs, " "))
|
||||
|
||||
cmd = exec.Command("ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
fmt.Sprintf("%s@%s", serverUser, serverName),
|
||||
"sudo", "shutdown", "now")
|
||||
|
||||
stderr.Reset()
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdin = bytes.NewBufferString(password + "\n")
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
log.Printf("All shutdown attempts failed: %v - %s", err, stderr.String())
|
||||
return fmt.Errorf("SSH command failed: %v - %s", err, stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue