mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-03-31 06:24:06 -04:00
add shutdown via sshpass or net rpc, also improve wake and other things
This commit is contained in:
@@ -24,7 +24,7 @@ WORKDIR /app
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update &&\
|
||||
apt-get install -y --no-install-recommends default-mysql-client nodejs npm iputils-ping nmap &&\
|
||||
apt-get install -y --no-install-recommends default-mysql-client nodejs npm iputils-ping nmap samba-common sshpass &&\
|
||||
apt-get clean &&\
|
||||
rm -rf /var/lib/{apt,dpkg,cache,log}/
|
||||
COPY --from=python-build /opt/venv /opt/venv
|
||||
|
||||
@@ -45,6 +45,7 @@ INSTALLED_APPS = [
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
|
||||
@@ -17,7 +17,7 @@ if os.getenv("DJANGO_SUPERUSER_USER") and os.getenv("DJANGO_SUPERUSER_PASSWORD")
|
||||
[i.delete() for i in Websocket.objects.all()]
|
||||
Websocket.objects.create(visitors=0)
|
||||
|
||||
# notifications
|
||||
# ping interval
|
||||
if os.environ.get("PING_INTERVAL"):
|
||||
ping_interval = os.environ.get("PING_INTERVAL")
|
||||
else:
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import ipaddress
|
||||
import wakeonlan
|
||||
import subprocess
|
||||
|
||||
|
||||
def wake(mac, ip, netmask):
|
||||
subnet = ipaddress.ip_network(
|
||||
f"{ip}/{netmask}", strict=False).broadcast_address
|
||||
wakeonlan.send_magic_packet(mac, ip_address=str(subnet))
|
||||
|
||||
def shutdown(command):
|
||||
subprocess.run(command, shell=True)
|
||||
@@ -4,13 +4,11 @@ import subprocess
|
||||
|
||||
from channels.db import database_sync_to_async
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils.timezone import make_aware
|
||||
from django_celery_beat.models import (ClockedSchedule, CrontabSchedule,
|
||||
IntervalSchedule, PeriodicTask)
|
||||
from django_celery_beat.models import (CrontabSchedule, IntervalSchedule,
|
||||
PeriodicTask)
|
||||
|
||||
from wol.commands import shutdown, wake
|
||||
from wol.models import Device, Port, Settings, Websocket
|
||||
from wol.wake import wake
|
||||
|
||||
|
||||
class WSConsumer(AsyncWebsocketConsumer):
|
||||
@@ -56,7 +54,18 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
"wol", {
|
||||
"type": "send_group",
|
||||
"message": {
|
||||
"type": "wake",
|
||||
"type": "pending",
|
||||
"message": received["id"]
|
||||
}
|
||||
}
|
||||
)
|
||||
elif received["type"] == "shutdown":
|
||||
await self.shutdown_device(received["id"])
|
||||
await self.channel_layer.group_send(
|
||||
"wol", {
|
||||
"type": "send_group",
|
||||
"message": {
|
||||
"type": "pending",
|
||||
"message": received["id"]
|
||||
}
|
||||
}
|
||||
@@ -78,8 +87,6 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
await self.update_port(received["data"])
|
||||
elif received["type"] == "update_settings":
|
||||
await self.update_settings(received["data"])
|
||||
elif received["type"] == "celery":
|
||||
await self.celery_create_scheduled_wake(received["data"])
|
||||
elif received["type"] == "scan_network":
|
||||
await self.send(text_data=json.dumps({
|
||||
"type": "scan_network",
|
||||
@@ -115,6 +122,11 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
dev = Device.objects.filter(id=id).first()
|
||||
wake(dev.mac, dev.ip, dev.netmask)
|
||||
|
||||
@database_sync_to_async
|
||||
def shutdown_device(self, id):
|
||||
dev = Device.objects.filter(id=id).first()
|
||||
shutdown(dev.shutdown_cmd)
|
||||
|
||||
@database_sync_to_async
|
||||
def get_all_devices(self):
|
||||
devices = Device.objects.all()
|
||||
@@ -127,9 +139,14 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
"mac": dev.mac,
|
||||
"netmask": dev.netmask,
|
||||
"ports": [],
|
||||
"cron": {
|
||||
"wake": {
|
||||
"enabled": False,
|
||||
"value": ""
|
||||
"cron": ""
|
||||
},
|
||||
"shutdown": {
|
||||
"enabled": False,
|
||||
"cron": "",
|
||||
"command": dev.shutdown_cmd
|
||||
}
|
||||
}
|
||||
for p in Port.objects.all().order_by("number"):
|
||||
@@ -139,16 +156,18 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
"checked": False,
|
||||
"open": False
|
||||
})
|
||||
try:
|
||||
task = PeriodicTask.objects.filter(
|
||||
name=data["name"], task="wol.tasks.scheduled_wake", crontab_id__isnull=False).get()
|
||||
if task:
|
||||
cron = CrontabSchedule.objects.get(id=task.crontab_id)
|
||||
data["cron"]["enabled"] = task.enabled
|
||||
data["cron"]["value"] = " ".join(
|
||||
[cron.minute, cron.hour, cron.day_of_week, cron.day_of_month, cron.month_of_year])
|
||||
except PeriodicTask.DoesNotExist:
|
||||
pass
|
||||
for action in ["wake", "shutdown"]:
|
||||
try:
|
||||
task = PeriodicTask.objects.filter(
|
||||
name=f"{data['name']}-{action}",
|
||||
task=f"wol.tasks.scheduled_{action}", crontab_id__isnull=False).get()
|
||||
if task:
|
||||
wake = CrontabSchedule.objects.get(id=task.crontab_id)
|
||||
data[action]["enabled"] = task.enabled
|
||||
data[action]["cron"] = " ".join(
|
||||
[wake.minute, wake.hour, wake.day_of_week, wake.day_of_month, wake.month_of_year])
|
||||
except PeriodicTask.DoesNotExist:
|
||||
pass
|
||||
d.append(data)
|
||||
return d
|
||||
|
||||
@@ -164,7 +183,8 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
defaults={
|
||||
"name": data["name"],
|
||||
"ip": data["ip"],
|
||||
"netmask": data["netmask"]
|
||||
"netmask": data["netmask"],
|
||||
"shutdown_cmd": data["shutdown"]["command"]
|
||||
}
|
||||
)
|
||||
if data.get("ports"):
|
||||
@@ -174,36 +194,37 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
number=port["number"], name=port["name"])
|
||||
obj.port.add(p)
|
||||
else:
|
||||
p = Port.objects.filter(number=port["number"])
|
||||
if p.exists():
|
||||
p = Port.objects.filter(number=port["number"]).first()
|
||||
if p and p in obj.port.all():
|
||||
obj.port.remove(p)
|
||||
|
||||
if data.get("cron"):
|
||||
if data["cron"]["enabled"]:
|
||||
cron_value = data["cron"]["value"].strip().split(" ")
|
||||
if not len(cron_value) == 5:
|
||||
return
|
||||
minute, hour, dom, month, dow = cron_value
|
||||
schedule, _ = CrontabSchedule.objects.get_or_create(
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day_of_week=dow,
|
||||
day_of_month=dom,
|
||||
month_of_year=month
|
||||
)
|
||||
PeriodicTask.objects.update_or_create(
|
||||
name=data["name"],
|
||||
defaults={
|
||||
"crontab": schedule,
|
||||
"task": "wol.tasks.scheduled_wake",
|
||||
"args": json.dumps([data["id"]]),
|
||||
"enabled": True
|
||||
}
|
||||
)
|
||||
else:
|
||||
for task in PeriodicTask.objects.filter(name=data["name"], task="wol.tasks.scheduled_wake"):
|
||||
task.enabled = False
|
||||
task.save()
|
||||
for action in ["wake", "shutdown"]:
|
||||
if data.get(action):
|
||||
if data[action]["enabled"]:
|
||||
cron = data[action]["cron"].strip().split(" ")
|
||||
if not len(cron) == 5:
|
||||
return
|
||||
minute, hour, dom, month, dow = cron
|
||||
schedule, _ = CrontabSchedule.objects.get_or_create(
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day_of_week=dow,
|
||||
day_of_month=dom,
|
||||
month_of_year=month
|
||||
)
|
||||
PeriodicTask.objects.update_or_create(
|
||||
name=f"{data['name']}-{action}",
|
||||
defaults={
|
||||
"crontab": schedule,
|
||||
"task": f"wol.tasks.scheduled_{action}",
|
||||
"args": json.dumps([data["id"]]),
|
||||
"enabled": True
|
||||
}
|
||||
)
|
||||
else:
|
||||
for task in PeriodicTask.objects.filter(name=f"{data['name']}-{action}", task=f"wol.tasks.scheduled_{action}"):
|
||||
task.enabled = False
|
||||
task.save()
|
||||
|
||||
@database_sync_to_async
|
||||
def update_port(self, data):
|
||||
@@ -223,7 +244,8 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
data = {
|
||||
"discovery": conf.scan_address,
|
||||
"interval": conf.interval,
|
||||
"scan_network": []
|
||||
"scan_network": [],
|
||||
"notifications": conf.notifications
|
||||
}
|
||||
return data
|
||||
|
||||
@@ -235,7 +257,8 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
id=1,
|
||||
defaults={
|
||||
"scan_address": data["discovery"],
|
||||
"interval": data["interval"]
|
||||
"interval": data["interval"],
|
||||
"notifications": data["notifications"]
|
||||
}
|
||||
)
|
||||
schedule, _ = IntervalSchedule.objects.get_or_create(
|
||||
@@ -249,19 +272,6 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
}
|
||||
)
|
||||
|
||||
@database_sync_to_async
|
||||
def celery_create_scheduled_wake(self, data):
|
||||
aware_time = make_aware(parse_datetime(data["time"]))
|
||||
schedule, _ = ClockedSchedule.objects.get_or_create(
|
||||
clocked_time=aware_time
|
||||
)
|
||||
PeriodicTask.objects.get_or_create(
|
||||
clocked=schedule,
|
||||
name=data["name"],
|
||||
task="wol.tasks.scheduled_wake",
|
||||
args=json.dumps([data["id"]])
|
||||
)
|
||||
|
||||
@database_sync_to_async
|
||||
def scan_network(self):
|
||||
data = []
|
||||
|
||||
@@ -13,8 +13,8 @@ class Device(models.Model):
|
||||
ip = models.GenericIPAddressField()
|
||||
mac = models.CharField(max_length=17)
|
||||
netmask = models.CharField(max_length=15, default="255.255.255.0", blank=False, null=False)
|
||||
scheduled_wake = models.DateTimeField(blank=True, null=True)
|
||||
port = models.ManyToManyField(Port, blank=True)
|
||||
shutdown_cmd = models.TextField(null=True, blank=True)
|
||||
|
||||
class Websocket(models.Model):
|
||||
visitors = models.PositiveSmallIntegerField(blank=False, null=False, default=0)
|
||||
@@ -23,3 +23,4 @@ class Settings(models.Model):
|
||||
sort_by = models.SlugField(default="name")
|
||||
scan_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
interval = models.PositiveSmallIntegerField(null=True, blank=True)
|
||||
notifications = models.BooleanField(default=True)
|
||||
|
||||
@@ -5,10 +5,10 @@ import threading
|
||||
from asgiref.sync import async_to_sync
|
||||
from celery import shared_task
|
||||
from channels.layers import get_channel_layer
|
||||
from django_celery_beat.models import PeriodicTask, CrontabSchedule
|
||||
from django_celery_beat.models import CrontabSchedule, PeriodicTask
|
||||
|
||||
from wol.commands import shutdown, wake
|
||||
from wol.models import Device, Port, Websocket
|
||||
from wol.wake import wake
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
|
||||
@@ -37,9 +37,14 @@ class WolDevice:
|
||||
"netmask": dev.netmask,
|
||||
"up": False,
|
||||
"ports": [],
|
||||
"cron": {
|
||||
"wake": {
|
||||
"enabled": False,
|
||||
"value": ""
|
||||
"cron": ""
|
||||
},
|
||||
"shutdown": {
|
||||
"enabled": False,
|
||||
"cron": "",
|
||||
"command": dev.shutdown_cmd
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +61,8 @@ class WolDevice:
|
||||
if self.ping_device(dev.ip):
|
||||
data["up"] = True
|
||||
for port in dev.port.all():
|
||||
index = next(i for i, d in enumerate(data["ports"]) if d["number"] == port.number)
|
||||
index = next(i for i, d in enumerate(
|
||||
data["ports"]) if d["number"] == port.number)
|
||||
if self.check_port(dev.ip, port.number):
|
||||
data["ports"][index]["checked"] = True
|
||||
data["ports"][index]["open"] = True
|
||||
@@ -64,15 +70,19 @@ class WolDevice:
|
||||
data["ports"][index]["checked"] = True
|
||||
data["ports"][index]["open"] = False
|
||||
|
||||
# set cron for scheduled wake
|
||||
try:
|
||||
task = PeriodicTask.objects.filter(name=data["name"], task="wol.tasks.scheduled_wake", crontab_id__isnull=False).get()
|
||||
if task:
|
||||
cron = CrontabSchedule.objects.get(id=task.crontab_id)
|
||||
data["cron"]["enabled"] = task.enabled
|
||||
data["cron"]["value"] = " ".join([cron.minute, cron.hour, cron.day_of_week, cron.day_of_month, cron.month_of_year])
|
||||
except PeriodicTask.DoesNotExist:
|
||||
pass
|
||||
# set cron for wake and shutdown
|
||||
for action in ["wake", "shutdown"]:
|
||||
try:
|
||||
task = PeriodicTask.objects.filter(
|
||||
name=f"{data['name']}-{action}",
|
||||
task=f"wol.tasks.scheduled_{action}", crontab_id__isnull=False).get()
|
||||
if task:
|
||||
wake = CrontabSchedule.objects.get(id=task.crontab_id)
|
||||
data[action]["enabled"] = task.enabled
|
||||
data[action]["cron"] = " ".join(
|
||||
[wake.minute, wake.hour, wake.day_of_week, wake.day_of_month, wake.month_of_year])
|
||||
except PeriodicTask.DoesNotExist:
|
||||
pass
|
||||
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
"wol", {"type": "send_group", "message": {
|
||||
@@ -103,4 +113,32 @@ def scheduled_wake(id):
|
||||
task.delete()
|
||||
return
|
||||
|
||||
wake(device.mac, device.ip, device.netmask)
|
||||
d = WolDevice()
|
||||
up = d.ping_device(device.ip)
|
||||
if not up:
|
||||
wake(device.mac, device.ip, device.netmask)
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
"wol", {"type": "send_group", "message": {
|
||||
"type": "pending",
|
||||
"message": id
|
||||
}})
|
||||
|
||||
|
||||
@shared_task
|
||||
def scheduled_shutdown(id):
|
||||
try:
|
||||
device = Device.objects.get(id=id)
|
||||
except Device.DoesNotExist:
|
||||
for task in PeriodicTask.objects.filter(args=id):
|
||||
task.delete()
|
||||
return
|
||||
|
||||
d = WolDevice()
|
||||
up = d.ping_device(device.ip)
|
||||
if up:
|
||||
shutdown(device.shutdown_cmd)
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
"wol", {"type": "send_group", "message": {
|
||||
"type": "pending",
|
||||
"message": id
|
||||
}})
|
||||
|
||||
@@ -19,11 +19,8 @@
|
||||
store.subscribe(currentMessage => {
|
||||
if (currentMessage.type == "init") {
|
||||
// create devices
|
||||
for (let index = 0; index < currentMessage.message.devices.length; index++) {
|
||||
const element = currentMessage.message.devices[index];
|
||||
devices.push(element);
|
||||
devices = devices;
|
||||
}
|
||||
devices = [...currentMessage.message.devices]
|
||||
devices = devices;
|
||||
devices.sort(compare);
|
||||
settings = currentMessage.message.settings;
|
||||
} else if (currentMessage.type == "status") {
|
||||
@@ -36,16 +33,15 @@
|
||||
devices[index] = currentMessage.message;
|
||||
}
|
||||
devices.sort(compare)
|
||||
|
||||
// set device status
|
||||
if (currentMessage.message.up == true) {
|
||||
setUp(currentMessage.message);
|
||||
} else {
|
||||
setDown(currentMessage.message);
|
||||
}
|
||||
} else if (currentMessage.type == "wake") {
|
||||
// set device waking
|
||||
setWake(currentMessage.message)
|
||||
} else if (currentMessage.type == "pending") {
|
||||
// set device pending
|
||||
setPending(currentMessage.message)
|
||||
} else if (currentMessage.type == "visitor") {
|
||||
// update visitor count
|
||||
visitors = currentMessage.message;
|
||||
@@ -120,7 +116,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setWake(id) {
|
||||
function setPending(id) {
|
||||
const dot = document.querySelector(`#dot-${id}`);
|
||||
const spinner = document.querySelector(`#spinner-${id}`);
|
||||
dot.classList.add("d-none");
|
||||
@@ -178,7 +174,7 @@
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--bg-lighter);
|
||||
background-color: var(--bg-modal);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@@ -224,8 +220,6 @@
|
||||
|
||||
.callout {
|
||||
padding: 1rem;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
border-left-width: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
|
||||
@@ -12,6 +12,13 @@
|
||||
})
|
||||
}
|
||||
|
||||
function shutdown(id) {
|
||||
store.sendMessage({
|
||||
type: "shutdown",
|
||||
id: id
|
||||
})
|
||||
}
|
||||
|
||||
function deleteDevice() {
|
||||
store.sendMessage({
|
||||
type: "delete_device",
|
||||
@@ -74,17 +81,34 @@
|
||||
<div class="row">
|
||||
<div class="col-auto me-auto">
|
||||
<div id="spinner-{device.id}" class="spinner-border warning d-none" role="status"></div>
|
||||
<div class="hover" on:click="{() => wake(device.id)}" role="button">
|
||||
{#if device.up === true || device.up === false}
|
||||
<i id="dot-{device.id}" class="fa-solid fa-power-off fa-2x {device.up ? 'success' : 'danger'}"></i>
|
||||
{#if device.up === true}
|
||||
{#if device.shutdown.command}
|
||||
<div class="hover" on:click="{() => shutdown(device.id)}" data-bs-toggle="tooltip" title="Shutdown command: {device.shutdown.command}" role="button">
|
||||
<i id="dot-{device.id}" class="fa-solid fa-power-off fa-2x success"></i>
|
||||
</div>
|
||||
{:else}
|
||||
<i id="dot-{device.id}" class="fa-solid fa-power-off fa-2x text-muted"></i>
|
||||
<i id="dot-{device.id}" class="fa-solid fa-power-off fa-2x success"></i>
|
||||
{/if}
|
||||
{:else if device.up === false}
|
||||
<div class="hover" on:click="{() => wake(device.id)}" role="button">
|
||||
<i id="dot-{device.id}" class="fa-solid fa-power-off fa-2x danger"></i>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="hover" role="button">
|
||||
<i id="dot-{device.id}" class="fa-solid fa-power-off fa-2x text-muted"></i>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if device.wake.enabled}
|
||||
<div class="col-auto px-2" data-bs-toggle="tooltip" title="Wake cron: {device.wake.cron}">
|
||||
<i class="fa-solid fa-circle-play fa-2x text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto" class:d-none={!device.cron.enabled} data-bs-toggle="tooltip" title="Crontab: {device.cron.value}">
|
||||
<i class="fa-solid fa-repeat fa-2x text-muted"></i>
|
||||
</div>
|
||||
{/if}
|
||||
{#if device.shutdown.enabled}
|
||||
<div class="col-auto px-2" data-bs-toggle="tooltip" title="Shutdown cron: {device.wake.cron}">
|
||||
<i class="fa-solid fa-circle-stop fa-2x text-muted"></i>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="col-auto hover" data-bs-toggle="modal" data-bs-target="#device-modal-{device.id}" role="button" on:click="{() => openModal()}">
|
||||
<i class="fa-solid fa-ellipsis-vertical fa-2x"></i>
|
||||
</div>
|
||||
@@ -112,38 +136,33 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{JSON.stringify(modalDevice)}
|
||||
<form id="form-{modalDevice.id}" on:submit|preventDefault={updateDevice}>
|
||||
<!-- general -->
|
||||
<h5 class="fw-bold">General</h5>
|
||||
<div class="row">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label for="inputName{modalDevice.id}" class="form-label">Device name</label>
|
||||
<input type="text" class="form-control" id="inputName{modalDevice.id}" bind:value="{modalDevice.name}" required>
|
||||
</div>
|
||||
<label for="inputName{modalDevice.id}" class="form-label">Device name</label>
|
||||
<input type="text" class="form-control" id="inputName{modalDevice.id}" bind:value="{modalDevice.name}" required>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label for="inputMac{modalDevice.id}" class="form-label">Mac address</label>
|
||||
<input type="text" class="form-control" id="inputMac{modalDevice.mac}" bind:value="{modalDevice.mac}" required>
|
||||
</div>
|
||||
<label for="inputMac{modalDevice.id}" class="form-label">Mac address</label>
|
||||
<input type="text" class="form-control" id="inputMac{modalDevice.mac}" bind:value="{modalDevice.mac}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label for="inputIp{modalDevice.id}" class="form-label">IP address</label>
|
||||
<input type="text" class="form-control" id="inputIp{modalDevice.id}" bind:value="{modalDevice.ip}" pattern="^([01]?\d\d?|2[0-4]\d|25[0-5])(?:\.(?:[01]?\d\d?|2[0-4]\d|25[0-5])){'{'}3{'}'}$" required>
|
||||
</div>
|
||||
<label for="inputIp{modalDevice.id}" class="form-label">IP address</label>
|
||||
<input type="text" class="form-control" id="inputIp{modalDevice.id}" bind:value="{modalDevice.ip}" pattern="^([01]?\d\d?|2[0-4]\d|25[0-5])(?:\.(?:[01]?\d\d?|2[0-4]\d|25[0-5])){'{'}3{'}'}$" required>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label for="inputNetmask{modalDevice.id}" class="form-label">Netmask</label>
|
||||
<input type="text" class="form-control" id="inputNetmask{modalDevice.id}" bind:value="{modalDevice.netmask}" pattern="^(((255\.){'{'}3{'}'}(255|254|252|248|240|224|192|128|0+))|((255\.){'{'}2{'}'}(255|254|252|248|240|224|192|128|0+)\.0)|((255\.)(255|254|252|248|240|224|192|128|0+)(\.0+){'{'}2{'}'})|((255|254|252|248|240|224|192|128|0+)(\.0+){'{'}3{'}'}))$" required>
|
||||
</div>
|
||||
<label for="inputNetmask{modalDevice.id}" class="form-label">Netmask</label>
|
||||
<input type="text" class="form-control" id="inputNetmask{modalDevice.id}" bind:value="{modalDevice.netmask}" pattern="^(((255\.){'{'}3{'}'}(255|254|252|248|240|224|192|128|0+))|((255\.){'{'}2{'}'}(255|254|252|248|240|224|192|128|0+)\.0)|((255\.)(255|254|252|248|240|224|192|128|0+)(\.0+){'{'}2{'}'})|((255|254|252|248|240|224|192|128|0+)(\.0+){'{'}3{'}'}))$" required>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="fw-bold">Ports</h5>
|
||||
<p>Select ports to check if they are open.</p>
|
||||
<!-- ports -->
|
||||
<h5 class="fw-bold mt-4">Ports</h5>
|
||||
<p class="mb-2">Select ports to check if they are open.</p>
|
||||
{#if modalDevice.ports.length === 0}
|
||||
<p class="mb-0">No ports available. Add ports below.</p>
|
||||
{/if}
|
||||
@@ -153,41 +172,99 @@
|
||||
<label class="form-check-label" for="{device.id}-port-{port.number}">{port.name} <span class="text-muted">({port.number})</span></label>
|
||||
</div>
|
||||
{/each}
|
||||
<label class="form-label mt-3" for="{device.id}-custom-port">Custom port</label>
|
||||
<label class="form-label mt-2" for="{device.id}-custom-port">Custom port</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" id="{device.id}-custom-port" class="form-control" placeholder="Name" aria-label="Name" aria-describedby="button-addon2" bind:value={customPort.name}>
|
||||
<input type="number" min="1" max="65535" class="form-control" placeholder="Port" aria-label="Port" aria-describedby="button-addon2" bind:value={customPort.number} on:input={validatePort}>
|
||||
<input type="text" id="{device.id}-custom-port" class="form-control rounded-0 rounded-start" placeholder="Name" aria-label="Name" aria-describedby="button-addon2" bind:value={customPort.name}>
|
||||
<input type="number" min="1" max="65535" class="form-control rounded-0" placeholder="Port" aria-label="Port" aria-describedby="button-addon2" bind:value={customPort.number} on:input={validatePort}>
|
||||
<button class="btn btn-secondary" type="button" id="button-addon2" on:click="{updatePort}">Update Port</button>
|
||||
</div>
|
||||
<div class="callout callout-info">
|
||||
<p class="mb-0">Ports must be between 1 and 65535. Enter the same port with a differen name to change it. Leave name empty to delete port.</p>
|
||||
<button class="btn btn-secondary mt-2" type="button" data-bs-toggle="collapse" data-bs-target="#info-ports" aria-expanded="false" aria-controls="info-ports">
|
||||
<i class="fa-solid fa-angle-down me-2"></i>How to use
|
||||
</button>
|
||||
<div class="collapse mt-3" id="info-ports">
|
||||
<div class="callout callout-info">
|
||||
<p class="mb-0">Ports must be between 1 and 65535. Enter the same port with a differen name to change it. Leave name empty to delete port.</p>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="fw-bold">Scheduled wake</h5>
|
||||
<p>Wake your devices at a given time.</p>
|
||||
<!-- scheduled wake -->
|
||||
<h5 class="fw-bold mt-4">Scheduled wake</h5>
|
||||
<p class="mb-2">Wake your device at a given time.</p>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="flexRadioSchedule" id="flexRadioDisabled{modalDevice.id}"
|
||||
bind:group={modalDevice["cron"]["enabled"]} value={false} checked={!modalDevice["cron"]["enabled"]}>
|
||||
<label class="form-check-label" for="flexRadioDisabled{modalDevice.id}">
|
||||
<input class="form-check-input" type="radio" name="wake-disable" id="wake-radio-disabled-{modalDevice.id}"
|
||||
bind:group={modalDevice["wake"]["enabled"]} value={false} checked={!modalDevice["wake"]["enabled"]}>
|
||||
<label class="form-check-label" for="wake-radio-disabled-{modalDevice.id}">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="flexRadioSchedule" id="flexRadioCron{modalDevice.id}"
|
||||
bind:group={modalDevice["cron"]["enabled"]} value={true} checked={modalDevice["cron"]["enabled"]}>
|
||||
<label class="form-check-label" for="flexRadioCron{modalDevice.id}">
|
||||
Cron
|
||||
<input class="form-check-input" type="radio" name="wake-cron" id="wake-radio-cron-{modalDevice.id}"
|
||||
bind:group={modalDevice["wake"]["enabled"]} value={true} checked={modalDevice["wake"]["enabled"]}>
|
||||
<label class="form-check-label" for="wake-radio-cron-{modalDevice.id}">
|
||||
Enabled
|
||||
</label>
|
||||
</div>
|
||||
<input id="inputCron{modalDevice.id}" type="text" class="form-control" placeholder="* /4 * * *" aria-label="Crontab" aria-describedby="addon-wrapping" bind:value={modalDevice["cron"]["value"]} hidden={!modalDevice["cron"]["enabled"]}>
|
||||
<div class="callout callout-info">
|
||||
<p class="my-2">The cron field uses common cron syntax. Valid examples:</p>
|
||||
<pre>Minute Hour DayOfMonth Month DayOfWeek
|
||||
* /4 * * * (Wake every 4 hours)
|
||||
0 9 * * 1-5 (Wake from Mo-Fr at 9 a.m.)
|
||||
</pre>
|
||||
<p class="mb-0">Read more about <a href="https://linux.die.net/man/5/crontab" target="_blank">valid syntax here</a> or <a href="https://crontab.guru/" target="_blank">generate</a> it. Expressions starting with "@..." are not supported.</p>
|
||||
<div class="input-group my-1" hidden={!modalDevice["wake"]["enabled"]}>
|
||||
<span class="input-group-text rounded-0 rounded-start" id="wake-cron-{modalDevice.id}">Cron</span>
|
||||
<input type="text" class="form-control rounded-0 rounded-end" placeholder="* /4 * * *" aria-label="Crontab" aria-describedby="wake-cron-{modalDevice.id}" bind:value={modalDevice["wake"]["cron"]}>
|
||||
</div>
|
||||
<button class="btn btn-secondary mt-2" type="button" data-bs-toggle="collapse" data-bs-target="#info-wake" aria-expanded="false" aria-controls="info-wake">
|
||||
<i class="fa-solid fa-angle-down me-2"></i>How to use
|
||||
</button>
|
||||
<div class="collapse mt-3" id="info-wake">
|
||||
<div class="callout callout-info">
|
||||
<p class="mb-2">Cron is a syntax describing a time pattern when to execute jobs. The above field uses common cron syntax. Examples:</p>
|
||||
<pre class="mb-2">Minute Hour DayOfMonth Month DayOfWeek
|
||||
* /4 * * * (Wake every 4 hours)
|
||||
0 9 * * 1-5 (Wake from Mo-Fr at 9 a.m.)
|
||||
</pre>
|
||||
<p class="mb-0">Read more about <a href="https://linux.die.net/man/5/crontab" target="_blank">valid syntax here</a> or <a href="https://crontab.guru/" target="_blank">use a generator</a>. Expressions starting with "@..." are not supported.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- scheduled shutdown -->
|
||||
<h5 class="fw-bold mt-4">Shutdown</h5>
|
||||
<p class="mb-2">Set the shutdown command here. This shell command will be executed when clicking the power button on the device card. You can use cron below, which will then execute the command at the given time.</p>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text rounded-0 rounded-start" id="shutdown-command-{modalDevice.id}">Command</span>
|
||||
<input type="text" class="form-control rounded-0 rounded-end" placeholder="sshpass -p your_password ssh user@hostname 'sudo shutdown'" aria-label="Ccommand" aria-describedby="shutdown-command-{modalDevice.id}" bind:value={modalDevice["shutdown"]["command"]}>
|
||||
</div>
|
||||
<button class="btn btn-secondary mt-2" type="button" data-bs-toggle="collapse" data-bs-target="#info-shutdown" aria-expanded="false" aria-controls="info-shutdown">
|
||||
<i class="fa-solid fa-angle-down me-2"></i>How to use
|
||||
</button>
|
||||
<div class="collapse mt-3" id="info-shutdown">
|
||||
<div class="callout callout-info">
|
||||
<p class="mb-2">This field takes a shell command to trigger the shutdown. You can use <code>sshpass</code> for Linux or <code>net rpc</code> for Windows hosts.</p>
|
||||
<div class="callout callout-danger mb-2">Note: This command is safed as cleartext. Meaning, passwords are clearly visible in the database.</div>
|
||||
<p class="mb-2">Examples:</p>
|
||||
<pre class="mb-2"># wake linux hosts
|
||||
sshpass -p your_password ssh user@hostname 'sudo shutdown'
|
||||
|
||||
# wake windows hosts
|
||||
net rpc shutdown --ipaddress 192.168.0.1 --user user%password
|
||||
</pre>
|
||||
<p class="mb-0">Read more about <a href="https://linux.die.net/man/1/sshpass" target="_blank">sshpass</a> or <a href="https://linux.die.net/man/8/net" target="_blank">net rpc</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label mt-2" for="{device.id}-shutdown-cron">Use cron</label>
|
||||
</div>
|
||||
<div class="form-check" id="{device.id}-shutdown-cron">
|
||||
<input class="form-check-input" type="radio" name="shutdown-disable" id="shutdown-radio-disabled-{modalDevice.id}"
|
||||
bind:group={modalDevice["shutdown"]["enabled"]} value={false} checked={!modalDevice["shutdown"]["enabled"]}>
|
||||
<label class="form-check-label" for="shutdown-radio-disabled-{modalDevice.id}">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="shutdown-enable" id="shutdown-radio-enabled-{modalDevice.id}"
|
||||
bind:group={modalDevice["shutdown"]["enabled"]} value={true} checked={modalDevice["shutdown"]["enabled"]}>
|
||||
<label class="form-check-label" for="shutdown-radio-enabled-{modalDevice.id}">
|
||||
Enabled
|
||||
</label>
|
||||
</div>
|
||||
<div class="input-group my-1" hidden={!modalDevice["shutdown"]["enabled"]}>
|
||||
<span class="input-group-text rounded-0 rounded-start" id="shutdown-cron-{modalDevice.id}">Cron</span>
|
||||
<input type="text" class="form-control rounded-0 rounded-end" placeholder="* /4 * * *" aria-label="Crontab" aria-describedby="shutdown-cron-{modalDevice.id}" bind:value={modalDevice["shutdown"]["cron"]}>
|
||||
</div>
|
||||
<!-- TODO: add shutdown command -->
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
|
||||
@@ -5,9 +5,14 @@
|
||||
export let settings;
|
||||
|
||||
let addDevice = {
|
||||
cron: {
|
||||
wake: {
|
||||
enabled: false,
|
||||
value: ""
|
||||
cron: ""
|
||||
},
|
||||
shutdown: {
|
||||
enabled: false,
|
||||
cron: "",
|
||||
command: ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,13 +130,13 @@
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label for="inputNameAddDevice" class="form-label">Device name</label>
|
||||
<input type="text" class="form-control" id="inputNameAddDevice" bind:value="{addDevice.name}" required>
|
||||
<input type="text" class="form-control" id="inputNameAddDevice" placeholder="Max PC" bind:value="{addDevice.name}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label for="inputMacAddDevice" class="form-label">Mac address</label>
|
||||
<input type="text" class="form-control" id="inputMacAddDevice" bind:value="{addDevice.mac}" pattern="^([0-9A-Fa-f]{'{'}2{'}'}[:-]){'{'}5{'}'}([0-9A-Fa-f]{'{'}2{'}'})|([0-9a-fA-F]{'{'}4{'}'}\\.[0-9a-fA-F]{'{'}4{'}'}\\.[0-9a-fA-F]{'{'}4{'}'})$" required>
|
||||
<input type="text" class="form-control" id="inputMacAddDevice" placeholder="aa:aa:aa:aa:aa:aa" bind:value="{addDevice.mac}" pattern="^([0-9A-Fa-f]{'{'}2{'}'}[:-]){'{'}5{'}'}([0-9A-Fa-f]{'{'}2{'}'})|([0-9a-fA-F]{'{'}4{'}'}\\.[0-9a-fA-F]{'{'}4{'}'}\\.[0-9a-fA-F]{'{'}4{'}'})$" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,13 +144,13 @@
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label for="inputIpAddDevice" class="form-label">IP address</label>
|
||||
<input type="text" class="form-control" id="inputIpAddDevice" bind:value="{addDevice.ip}" pattern="^([01]?\d\d?|2[0-4]\d|25[0-5])(?:\.(?:[01]?\d\d?|2[0-4]\d|25[0-5])){'{'}3{'}'}$" required>
|
||||
<input type="text" class="form-control" id="inputIpAddDevice" placeholder="192.168.1.1" bind:value="{addDevice.ip}" pattern="^([01]?\d\d?|2[0-4]\d|25[0-5])(?:\.(?:[01]?\d\d?|2[0-4]\d|25[0-5])){'{'}3{'}'}$" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label for="inputNetmaskAddDevice" class="form-label">Netmask</label>
|
||||
<input type="text" class="form-control" id="inputNetmaskAddDevice" bind:value="{addDevice.netmask}" pattern="^(((255\.){'{'}3{'}'}(255|254|252|248|240|224|192|128|0+))|((255\.){'{'}2{'}'}(255|254|252|248|240|224|192|128|0+)\.0)|((255\.)(255|254|252|248|240|224|192|128|0+)(\.0+){'{'}2{'}'})|((255|254|252|248|240|224|192|128|0+)(\.0+){'{'}3{'}'}))$" required>
|
||||
<input type="text" class="form-control" id="inputNetmaskAddDevice" placeholder="255.255.255.0" bind:value="{addDevice.netmask}" pattern="^(((255\.){'{'}3{'}'}(255|254|252|248|240|224|192|128|0+))|((255\.){'{'}2{'}'}(255|254|252|248|240|224|192|128|0+)\.0)|((255\.)(255|254|252|248|240|224|192|128|0+)(\.0+){'{'}2{'}'})|((255|254|252|248|240|224|192|128|0+)(\.0+){'{'}3{'}'}))$" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,8 +161,8 @@
|
||||
</div>
|
||||
<h5 class="fw-bold">Network discovery</h5>
|
||||
{#if !settings.discovery}
|
||||
<div class="callout callout-danger">
|
||||
<p class="my-0">To enable this option, please enter your network address in the settings.</p>
|
||||
<div class="callout callout-danger mb-2">
|
||||
<p class="m-0">To enable this option, please enter your network address in the settings.</p>
|
||||
</div>
|
||||
{/if}
|
||||
<button id="btnScan" class="btn btn-secondary" type="button" on:click={scanNetwork} disabled={!settings.discovery}>
|
||||
@@ -239,8 +244,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="fw-bold">Backup/Restore</h5>
|
||||
<div class="callout callout-info">
|
||||
<p class="mb-0">Backup file structure has changed in v2. You can still restore both versions with this file upload.</p>
|
||||
<div class="callout callout-info mb-2">
|
||||
<p class="m-0">Backup file structure has changed in v2. You can still restore both versions with this file upload.</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="inputRestore" class="form-label">Restore from .json</label>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<strong id="toast-title" class="me-auto">{toast.title}</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
<div class="toast-body fw-bold">
|
||||
{toast.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ html[data-theme="light"] {
|
||||
--color-bg: #{$light};
|
||||
--color-text: #{$dark};
|
||||
--bg-lighter: #{$light-darker};
|
||||
--bg-modal: #{$light};
|
||||
--svg-close: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
|
||||
}
|
||||
|
||||
@@ -18,5 +19,6 @@ html[data-theme="dark"] {
|
||||
--color-bg: #{$dark};
|
||||
--color-text: #{$light};
|
||||
--bg-lighter: #{$dark-lighter};
|
||||
--bg-modal: #{$dark-lighter};
|
||||
--svg-close: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user