Initialize Go module

This commit is contained in:
Lorenzo Iovino 2025-04-21 22:21:02 +02:00
parent f9549a10bb
commit fa9fe6095b
11 changed files with 2466 additions and 534 deletions

773
templates/status.html Normal file
View 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>