Add shutdown machine feature with configurable command

Backend:
- New migration: add shutdown_command TEXT column to machines table
- Machine model updated with ShutdownCommand field (Create/GetAll/GetByID/Update)
- MachineRequest/MachineResponse DTOs updated with shutdown_command field
- New ShutdownResponse DTO
- New POST /api/machines/{id}/shutdown handler via SSH
- Refactor sshmanager/testconn.go: extract dialSSH() helper shared with shutdown.go
- New sshmanager/shutdown.go: RunRemoteCommand with 15s timeout

Frontend:
- New shutdownMachine() API helper and ShutdownResponse type in client.ts
- New shutdown_command field in MachineForm
- Power button (amber) in machines table actions
- Shutdown confirmation modal with WoL warning notice
- shutdown_command input field in machine edit/create form
- Machine interface updated with shutdown_command and last_seen_at fields
This commit is contained in:
2026-07-13 10:02:18 -04:00
parent bf9bcccde2
commit bc3bc44c4a
11 changed files with 282 additions and 32 deletions
+11 -8
View File
@@ -22,6 +22,7 @@ type Machine struct {
Status string `db:"status" json:"status"`
LastSeenAt *time.Time `db:"last_seen_at" json:"last_seen_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
ShutdownCommand string `db:"shutdown_command" json:"shutdown_command"`
}
type MachineRepository struct {
@@ -36,11 +37,12 @@ func (r *MachineRepository) Create(m *Machine) (int64, error) {
res, err := r.db.Exec(`
INSERT INTO machines (name, host, port, ssh_user, ssh_key_id, mac_address,
wol_enabled, broadcast_addr, wake_timeout_seconds, wake_check_interval_seconds,
fingerprint_confirmed, host_key_fingerprint, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
fingerprint_confirmed, host_key_fingerprint, status, shutdown_command)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
m.Name, m.Host, m.Port, m.SSHUser, m.SSHKeyID, m.MACAddress,
boolToInt(m.WoLEnabled), m.BroadcastAddr, m.WakeTimeoutSeconds,
m.WakeCheckIntervalSeconds, boolToInt(m.FingerprintConfirmed), m.HostKeyFingerprint, m.Status,
m.ShutdownCommand,
)
if err != nil {
return 0, err
@@ -52,7 +54,7 @@ func (r *MachineRepository) GetAll() ([]Machine, error) {
rows, err := r.db.Query(`
SELECT id, name, host, port, ssh_user, ssh_key_id, mac_address,
wol_enabled, broadcast_addr, wake_timeout_seconds, wake_check_interval_seconds,
fingerprint_confirmed, host_key_fingerprint, status, last_seen_at, created_at
fingerprint_confirmed, host_key_fingerprint, status, last_seen_at, created_at, shutdown_command
FROM machines ORDER BY name`)
if err != nil {
return nil, err
@@ -69,7 +71,7 @@ func (r *MachineRepository) GetAll() ([]Machine, error) {
err := rows.Scan(&m.ID, &m.Name, &m.Host, &m.Port, &m.SSHUser, &keyID,
&mac, &m.WoLEnabled, &bcast, &m.WakeTimeoutSeconds,
&m.WakeCheckIntervalSeconds, &m.FingerprintConfirmed,
&hostKeyFP, &m.Status, &lastSeen, &m.CreatedAt)
&hostKeyFP, &m.Status, &lastSeen, &m.CreatedAt, &m.ShutdownCommand)
if err != nil {
return nil, err
}
@@ -103,12 +105,12 @@ func (r *MachineRepository) GetByID(id int64) (*Machine, error) {
err := r.db.QueryRow(`
SELECT id, name, host, port, ssh_user, ssh_key_id, mac_address,
wol_enabled, broadcast_addr, wake_timeout_seconds, wake_check_interval_seconds,
fingerprint_confirmed, host_key_fingerprint, status, last_seen_at, created_at
fingerprint_confirmed, host_key_fingerprint, status, last_seen_at, created_at, shutdown_command
FROM machines WHERE id = ?`, id).Scan(
&m.ID, &m.Name, &m.Host, &m.Port, &m.SSHUser, &keyID,
&mac, &m.WoLEnabled, &bcast, &m.WakeTimeoutSeconds,
&m.WakeCheckIntervalSeconds, &m.FingerprintConfirmed,
&hostKeyFP, &m.Status, &lastSeen, &m.CreatedAt)
&hostKeyFP, &m.Status, &lastSeen, &m.CreatedAt, &m.ShutdownCommand)
if err != nil {
return nil, err
}
@@ -135,12 +137,13 @@ func (r *MachineRepository) Update(m *Machine) error {
_, err := r.db.Exec(`
UPDATE machines SET name=?, host=?, port=?, ssh_user=?, ssh_key_id=?,
mac_address=?, wol_enabled=?, broadcast_addr=?, wake_timeout_seconds=?,
wake_check_interval_seconds=?, fingerprint_confirmed=?, host_key_fingerprint=?, status=?, last_seen_at=?
wake_check_interval_seconds=?, fingerprint_confirmed=?, host_key_fingerprint=?, status=?, last_seen_at=?,
shutdown_command=?
WHERE id=?`,
m.Name, m.Host, m.Port, m.SSHUser, m.SSHKeyID, m.MACAddress,
boolToInt(m.WoLEnabled), m.BroadcastAddr, m.WakeTimeoutSeconds,
m.WakeCheckIntervalSeconds, boolToInt(m.FingerprintConfirmed), m.HostKeyFingerprint,
m.Status, m.LastSeenAt, m.ID,
m.Status, m.LastSeenAt, m.ShutdownCommand, m.ID,
)
return err
}