mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-03-31 06:24:06 -04:00
sveltekit and go fiber+gorm
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
.git
|
.git
|
||||||
node_modules
|
app/frontend/node_modules
|
||||||
/app/frontend/public/build
|
app/frontend/build
|
||||||
/app/frontend/scripts
|
|
||||||
**/db.sqlite3
|
**/db.sqlite3
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -292,3 +292,4 @@ staticfiles
|
|||||||
# custom
|
# custom
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
db/
|
db/
|
||||||
|
*.db
|
||||||
|
|||||||
36
Dockerfile
36
Dockerfile
@@ -1,36 +0,0 @@
|
|||||||
FROM debian:bullseye-slim as base
|
|
||||||
|
|
||||||
FROM base as python-build
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
WORKDIR /python-build
|
|
||||||
RUN apt-get update &&\
|
|
||||||
apt-get install -y --no-install-recommends build-essential libffi-dev libssl-dev cargo python3 python3-dev python3-pip python3-venv default-libmysqlclient-dev libpq-dev &&\
|
|
||||||
python3 -m venv /opt/venv
|
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
|
||||||
COPY app/backend/requirements.txt .
|
|
||||||
RUN python3 -m pip install --no-cache-dir --upgrade pip wheel &&\
|
|
||||||
pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
FROM node:17-bullseye-slim as node-build
|
|
||||||
WORKDIR /node-build
|
|
||||||
COPY app/frontend/package*.json ./
|
|
||||||
COPY app/frontend/src ./src
|
|
||||||
COPY app/frontend/public ./public
|
|
||||||
COPY app/frontend/rollup.config.js ./
|
|
||||||
|
|
||||||
FROM base
|
|
||||||
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 samba-common-bin openssh-client sshpass curl &&\
|
|
||||||
apt-get clean &&\
|
|
||||||
rm -rf /var/lib/{apt,dpkg,cache,log}/
|
|
||||||
COPY --from=python-build /opt/venv /opt/venv
|
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
|
||||||
COPY --from=node-build /node-build ./frontend
|
|
||||||
COPY app/backend ./backend
|
|
||||||
COPY app/run.sh ./
|
|
||||||
|
|
||||||
CMD ["./run.sh"]
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""
|
|
||||||
ASGI config for backend project.
|
|
||||||
|
|
||||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
|
|
||||||
django_asgi_app = get_asgi_application()
|
|
||||||
|
|
||||||
from channels.auth import AuthMiddlewareStack
|
|
||||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
|
||||||
from wol.routing import ws_urlpatterns
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
|
||||||
|
|
||||||
application = ProtocolTypeRouter({
|
|
||||||
"http": django_asgi_app,
|
|
||||||
"websocket": AuthMiddlewareStack(URLRouter(ws_urlpatterns))
|
|
||||||
})
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from celery import Celery
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
|
|
||||||
|
|
||||||
app = Celery("dango_wol")
|
|
||||||
app.conf.timezone = os.getenv("DJANGO_TIME_ZONE", "UTC")
|
|
||||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
|
||||||
app.autodiscover_tasks()
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
"""
|
|
||||||
Django settings for backend project.
|
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 4.0.2.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.0/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.management.utils import get_random_secret_key
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', get_random_secret_key())
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True'
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
'channels',
|
|
||||||
'django_celery_beat',
|
|
||||||
'wol'
|
|
||||||
]
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'backend.urls'
|
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'DIRS': [],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.request',
|
|
||||||
'django.contrib.auth.context_processors.auth',
|
|
||||||
'django.contrib.messages.context_processors.messages',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
WSGI_APPLICATION = 'backend.wsgi.application'
|
|
||||||
WSGI_APPLICATION = 'backend.wsgi.application'
|
|
||||||
ASGI_APPLICATION = 'backend.asgi.application'
|
|
||||||
CELERY_BROKER_URL = f"redis://{os.getenv('REDIS_HOST', '127.0.0.1')}:{os.getenv('REDIS_PORT', 6379)}"
|
|
||||||
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
|
||||||
|
|
||||||
if os.getenv("DB_TYPE") == "postgres":
|
|
||||||
DATABASES = {
|
|
||||||
"default": {
|
|
||||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
|
||||||
"NAME": os.getenv("DB_NAME", "upsnap"),
|
|
||||||
"USER": os.getenv("DB_USER", "upsnap"),
|
|
||||||
"PASSWORD": os.getenv("DB_PASSWORD", "upsnap"),
|
|
||||||
"HOST": os.getenv("DB_HOST", "127.0.0.1"),
|
|
||||||
"PORT": os.getenv("DB_PORT", 5432),
|
|
||||||
"OPTIONS": {
|
|
||||||
"connect_timeout": 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elif os.getenv("DB_TYPE") == "mysql":
|
|
||||||
DATABASES = {
|
|
||||||
"default": {
|
|
||||||
"ENGINE": "django.db.backends.mysql",
|
|
||||||
"NAME": os.getenv("DB_NAME", "upsnap"),
|
|
||||||
"USER": os.getenv("DB_USER", "upsnap"),
|
|
||||||
"PASSWORD": os.getenv("DB_PASSWORD", "upsnap"),
|
|
||||||
"HOST": os.getenv("DB_HOST", "127.0.0.1"),
|
|
||||||
"PORT": os.getenv("DB_PORT", 3306),
|
|
||||||
"OPTIONS": {
|
|
||||||
"connect_timeout": 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elif os.getenv("DB_TYPE") == "sqlite":
|
|
||||||
DATABASES = {
|
|
||||||
"default": {
|
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
|
||||||
"NAME": "/app/backend/db/db.sqlite3",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CHANNEL_LAYERS = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
|
||||||
"CONFIG": {
|
|
||||||
"hosts": [(os.getenv("REDIS_HOST", "127.0.0.1"), os.getenv("REDIS_PORT", 6379))],
|
|
||||||
"capacity": 1000,
|
|
||||||
"expiry": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/4.0/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = os.getenv("DJANGO_LANGUAGE_CODE", "en")
|
|
||||||
|
|
||||||
TIME_ZONE = os.getenv("DJANGO_TIME_ZONE", "UTC")
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
|
|
||||||
|
|
||||||
# Default primary key field type
|
|
||||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
||||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
|
||||||
VERSION = "v2.3.1"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
"""backend URL Configuration
|
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/4.0/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.urls import include, path
|
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.conf import settings
|
|
||||||
from django.conf.urls.static import static
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('admin/', admin.site.urls)
|
|
||||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
"""
|
|
||||||
WSGI config for backend project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""Django's command-line utility for administrative tasks."""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Run administrative tasks."""
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
|
||||||
try:
|
|
||||||
from django.core.management import execute_from_command_line
|
|
||||||
except ImportError as exc:
|
|
||||||
raise ImportError(
|
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
|
||||||
"available on your PYTHONPATH environment variable? Did you "
|
|
||||||
"forget to activate a virtual environment?"
|
|
||||||
) from exc
|
|
||||||
execute_from_command_line(sys.argv)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
celery[redis]
|
|
||||||
channels
|
|
||||||
channels-redis==3.4.1
|
|
||||||
django
|
|
||||||
django-celery-beat
|
|
||||||
gunicorn
|
|
||||||
mysqlclient
|
|
||||||
psycopg2-binary
|
|
||||||
uvicorn[standard]
|
|
||||||
wakeonlan
|
|
||||||
whitenoise
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django_celery_beat.models import IntervalSchedule, PeriodicTask, PeriodicTasks
|
|
||||||
|
|
||||||
from wol.models import Settings, Websocket
|
|
||||||
|
|
||||||
# create superuser
|
|
||||||
if os.getenv("DJANGO_SUPERUSER_USER") and os.getenv("DJANGO_SUPERUSER_PASSWORD"):
|
|
||||||
if not User.objects.filter(username=os.getenv("DJANGO_SUPERUSER_USER")).exists():
|
|
||||||
User.objects.create_superuser(os.getenv("DJANGO_SUPERUSER_USER"), password=os.getenv("DJANGO_SUPERUSER_PASSWORD"))
|
|
||||||
else:
|
|
||||||
print('Django user exists')
|
|
||||||
|
|
||||||
# reset visitors
|
|
||||||
[i.delete() for i in Websocket.objects.all()]
|
|
||||||
Websocket.objects.create(visitors=0)
|
|
||||||
|
|
||||||
# ping interval
|
|
||||||
if os.environ.get("PING_INTERVAL"):
|
|
||||||
if int(os.environ.get("PING_INTERVAL")) < 5:
|
|
||||||
print("Ping interval lower than 5 seconds is not recommended. Please use an interval of 5 seconds or higher. Automatically set to 5 seconds.")
|
|
||||||
ping_interval = 5
|
|
||||||
else:
|
|
||||||
ping_interval = int(os.environ.get("PING_INTERVAL"))
|
|
||||||
else:
|
|
||||||
ping_interval = 5
|
|
||||||
Settings.objects.update_or_create(
|
|
||||||
id=1,
|
|
||||||
defaults={
|
|
||||||
"interval": ping_interval
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# register device ping task
|
|
||||||
schedule, created = IntervalSchedule.objects.get_or_create(
|
|
||||||
every=ping_interval,
|
|
||||||
period=IntervalSchedule.SECONDS,
|
|
||||||
)
|
|
||||||
if created:
|
|
||||||
PeriodicTask.objects.create(
|
|
||||||
interval=schedule,
|
|
||||||
name="Ping all devices",
|
|
||||||
task="wol.tasks.ping_all_devices"
|
|
||||||
)
|
|
||||||
|
|
||||||
# reset last run to fix time zone changes
|
|
||||||
# https://django-celery-beat.readthedocs.io/en/latest/#important-warning-about-time-zones
|
|
||||||
PeriodicTask.objects.update(last_run_at=None)
|
|
||||||
for task in PeriodicTask.objects.all():
|
|
||||||
PeriodicTasks.changed(task)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from wol.models import Device, Port, Settings
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ["name", "ip", "mac", "netmask", "ports"]
|
|
||||||
search_fields = ["name", "ip", "mac"]
|
|
||||||
list_filter = ["name", "netmask"]
|
|
||||||
|
|
||||||
def ports(self, obj):
|
|
||||||
return ", ".join([p.name for p in obj.port.all()])
|
|
||||||
|
|
||||||
class PortAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ["number", "name"]
|
|
||||||
search_fields = ["number", "name"]
|
|
||||||
list_filter = ["number", "name"]
|
|
||||||
|
|
||||||
class SettingsAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ["sort_by", "scan_address", "interval"]
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Device, DeviceAdmin)
|
|
||||||
admin.site.register(Port, PortAdmin)
|
|
||||||
admin.site.register(Settings, SettingsAdmin)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class WolConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'wol'
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
import ipaddress
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
import pytz
|
|
||||||
from channels.db import database_sync_to_async
|
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
|
||||||
from django_celery_beat.models import (CrontabSchedule, IntervalSchedule,
|
|
||||||
PeriodicTask)
|
|
||||||
|
|
||||||
from wol.commands import shutdown, wake
|
|
||||||
from wol.models import Device, Port, Settings, Websocket
|
|
||||||
|
|
||||||
|
|
||||||
class WSConsumer(AsyncWebsocketConsumer):
|
|
||||||
async def connect(self):
|
|
||||||
await self.channel_layer.group_add("wol", self.channel_name)
|
|
||||||
await self.accept()
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "init",
|
|
||||||
"message": {
|
|
||||||
"devices": await self.get_all_devices(),
|
|
||||||
"settings": await self.get_settings()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
await self.add_visitor()
|
|
||||||
await self.channel_layer.group_send(
|
|
||||||
"wol", {
|
|
||||||
"type": "send_group",
|
|
||||||
"message": {
|
|
||||||
"type": "visitor",
|
|
||||||
"message": await self.get_visitors()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def disconnect(self, _):
|
|
||||||
await self.channel_layer.group_discard("wol", self.channel_name)
|
|
||||||
await self.remove_visitor()
|
|
||||||
await self.channel_layer.group_send(
|
|
||||||
"wol", {
|
|
||||||
"type": "send_group",
|
|
||||||
"message": {
|
|
||||||
"type": "visitor",
|
|
||||||
"message": await self.get_visitors()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def receive(self, text_data=None):
|
|
||||||
received = json.loads(text_data)
|
|
||||||
if received["type"] == "wake":
|
|
||||||
await self.wake_device(received["id"])
|
|
||||||
await self.channel_layer.group_send(
|
|
||||||
"wol", {
|
|
||||||
"type": "send_group",
|
|
||||||
"message": {
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif received["type"] == "delete_device":
|
|
||||||
await self.delete_device(received["id"])
|
|
||||||
await self.channel_layer.group_send(
|
|
||||||
"wol", {
|
|
||||||
"type": "send_group",
|
|
||||||
"message": {
|
|
||||||
"type": "delete",
|
|
||||||
"message": received["id"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif received["type"] == "update_device":
|
|
||||||
try:
|
|
||||||
await self.update_device(received["data"])
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "operationStatus",
|
|
||||||
"message": "Success"
|
|
||||||
}))
|
|
||||||
except Exception:
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "operationStatus",
|
|
||||||
"message": "Error"
|
|
||||||
}))
|
|
||||||
elif received["type"] == "update_port":
|
|
||||||
await self.update_port(received["data"])
|
|
||||||
elif received["type"] == "update_settings":
|
|
||||||
await self.update_settings(received["data"])
|
|
||||||
elif received["type"] == "scan_network":
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "scan_network",
|
|
||||||
"message": await self.scan_network()
|
|
||||||
}))
|
|
||||||
elif received["type"] == "backup":
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "backup",
|
|
||||||
"message": await self.get_all_devices()
|
|
||||||
}))
|
|
||||||
|
|
||||||
async def send_group(self, event):
|
|
||||||
await self.send(json.dumps(event["message"]))
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def add_visitor(self):
|
|
||||||
visitor_count = Websocket.objects.first()
|
|
||||||
visitor_count.visitors += 1
|
|
||||||
visitor_count.save()
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def remove_visitor(self):
|
|
||||||
visitor_count = Websocket.objects.first()
|
|
||||||
visitor_count.visitors -= 1
|
|
||||||
visitor_count.save()
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def get_visitors(self):
|
|
||||||
return Websocket.objects.first().visitors
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def wake_device(self, id):
|
|
||||||
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()
|
|
||||||
d = []
|
|
||||||
for dev in devices:
|
|
||||||
data = {
|
|
||||||
"id": dev.id,
|
|
||||||
"name": dev.name,
|
|
||||||
"ip": dev.ip,
|
|
||||||
"mac": dev.mac,
|
|
||||||
"netmask": dev.netmask,
|
|
||||||
"link": dev.link,
|
|
||||||
"ports": [],
|
|
||||||
"wake": {
|
|
||||||
"enabled": False,
|
|
||||||
"cron": ""
|
|
||||||
},
|
|
||||||
"shutdown": {
|
|
||||||
"enabled": False,
|
|
||||||
"cron": "",
|
|
||||||
"command": dev.shutdown_cmd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for p in Port.objects.all().order_by("number"):
|
|
||||||
data["ports"].append({
|
|
||||||
"number": p.number,
|
|
||||||
"name": p.name,
|
|
||||||
"checked": False,
|
|
||||||
"open": False
|
|
||||||
})
|
|
||||||
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_month, wake.month_of_year, wake.day_of_week])
|
|
||||||
except PeriodicTask.DoesNotExist:
|
|
||||||
pass
|
|
||||||
d.append(data)
|
|
||||||
return d
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def delete_device(self, id):
|
|
||||||
dev = Device.objects.get(id=id)
|
|
||||||
dev.delete()
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def update_device(self, data):
|
|
||||||
obj, _ = Device.objects.update_or_create(
|
|
||||||
mac=data["mac"],
|
|
||||||
defaults={
|
|
||||||
"name": data["name"],
|
|
||||||
"ip": data["ip"],
|
|
||||||
"netmask": data["netmask"],
|
|
||||||
"link": data["link"] if data.get("link") else "",
|
|
||||||
"shutdown_cmd": data["shutdown"]["command"] if data.get("shutdown") else ""
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if data.get("ports"):
|
|
||||||
for port in data.get("ports"):
|
|
||||||
if port["checked"]:
|
|
||||||
p, _ = Port.objects.get_or_create(
|
|
||||||
number=port["number"], name=port["name"])
|
|
||||||
obj.port.add(p)
|
|
||||||
else:
|
|
||||||
p = Port.objects.filter(number=port["number"]).first()
|
|
||||||
if p and p in obj.port.all():
|
|
||||||
obj.port.remove(p)
|
|
||||||
|
|
||||||
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,
|
|
||||||
timezone=pytz.timezone(
|
|
||||||
os.getenv("DJANGO_TIME_ZONE", "UTC"))
|
|
||||||
)
|
|
||||||
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):
|
|
||||||
if data.get("name"):
|
|
||||||
Port.objects.update_or_create(
|
|
||||||
number=data["number"],
|
|
||||||
defaults={
|
|
||||||
"name": data["name"]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
Port.objects.filter(number=data["number"]).delete()
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def get_settings(self):
|
|
||||||
conf = Settings.objects.get(id=1)
|
|
||||||
data = {
|
|
||||||
"discovery": conf.scan_address,
|
|
||||||
"interval": conf.interval,
|
|
||||||
"scan_network": [],
|
|
||||||
"notifications": conf.notifications
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def update_settings(self, data):
|
|
||||||
if data["interval"] > 5:
|
|
||||||
data["interval"] = 5
|
|
||||||
Settings.objects.update_or_create(
|
|
||||||
id=1,
|
|
||||||
defaults={
|
|
||||||
"scan_address": data["discovery"],
|
|
||||||
"interval": data["interval"],
|
|
||||||
"notifications": data["notifications"]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
schedule, _ = IntervalSchedule.objects.get_or_create(
|
|
||||||
every=data["interval"],
|
|
||||||
period=IntervalSchedule.SECONDS,
|
|
||||||
)
|
|
||||||
PeriodicTask.objects.update_or_create(
|
|
||||||
task="wol.tasks.ping_all_devices",
|
|
||||||
defaults={
|
|
||||||
"interval": schedule
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def scan_network(self):
|
|
||||||
data = []
|
|
||||||
conf = Settings.objects.get(id=1)
|
|
||||||
netmask = str(ipaddress.ip_network(conf.scan_address).netmask)
|
|
||||||
|
|
||||||
if not conf.scan_address:
|
|
||||||
return
|
|
||||||
|
|
||||||
p = subprocess.Popen(
|
|
||||||
["nmap", os.getenv("NMAP_ARGS", "-sP"), conf.scan_address], stdout=subprocess.PIPE)
|
|
||||||
out = p.communicate()[0].decode("utf-8")
|
|
||||||
ip_line = "Nmap scan report for"
|
|
||||||
mac_line = "MAC Address:"
|
|
||||||
|
|
||||||
for line in out.splitlines():
|
|
||||||
if line.startswith(ip_line):
|
|
||||||
line_splitted = line.split()
|
|
||||||
if len(line_splitted) == 6:
|
|
||||||
name = line_splitted[4]
|
|
||||||
ip = line_splitted[5]
|
|
||||||
ip = ip.replace("(", "")
|
|
||||||
ip = ip.replace(")", "")
|
|
||||||
else:
|
|
||||||
name = "Unknown"
|
|
||||||
ip = line_splitted[4]
|
|
||||||
elif line.startswith(mac_line):
|
|
||||||
line_splitted = line.split()
|
|
||||||
mac = line_splitted[2]
|
|
||||||
data.append({
|
|
||||||
"name": name,
|
|
||||||
"ip": ip,
|
|
||||||
"netmask": netmask,
|
|
||||||
"mac": mac,
|
|
||||||
"link": "",
|
|
||||||
"ports": [],
|
|
||||||
"wake": {
|
|
||||||
"enabled": False,
|
|
||||||
"cron": ""
|
|
||||||
},
|
|
||||||
"shutdown": {
|
|
||||||
"enabled": False,
|
|
||||||
"cron": "",
|
|
||||||
"command": ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 3.1.3 on 2020-11-15 19:37
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Device',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.SlugField(max_length=100)),
|
|
||||||
('ip', models.GenericIPAddressField()),
|
|
||||||
('mac', models.SlugField(max_length=17)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.1.3 on 2020-11-15 19:43
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='device',
|
|
||||||
name='mac',
|
|
||||||
field=models.CharField(max_length=17),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.1.3 on 2020-11-15 22:50
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0002_auto_20201115_2043'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='device',
|
|
||||||
name='status',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 3.1.3 on 2020-11-16 02:50
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0003_device_status'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='device',
|
|
||||||
name='status',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.1.5 on 2021-01-18 16:21
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0004_remove_device_status'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='device',
|
|
||||||
name='netmask',
|
|
||||||
field=models.CharField(default='255.255.255.0', max_length=15),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Generated by Django 3.2.7 on 2021-09-22 20:02
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0005_device_netmask'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Websocket',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('visitors', models.PositiveSmallIntegerField(default=0)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.2.7 on 2021-09-23 16:17
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0006_websocket'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='device',
|
|
||||||
name='scheduled_wake',
|
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 3.2.7 on 2021-09-27 16:17
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0007_device_scheduled_wake'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Settings',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('ping_interval', models.PositiveSmallIntegerField(default=5)),
|
|
||||||
('enable_notifications', models.BooleanField(default=True)),
|
|
||||||
('enable_console_logging', models.BooleanField(default=True)),
|
|
||||||
('sort_by', models.SlugField(default='name')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 3.2.7 on 2021-09-27 19:05
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0008_settings'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='settings',
|
|
||||||
name='ping_interval',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.2.7 on 2021-09-28 19:00
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0009_remove_settings_ping_interval'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='settings',
|
|
||||||
name='enable_console_logging',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.2.7 on 2021-09-28 20:19
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0010_alter_settings_enable_console_logging'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='settings',
|
|
||||||
name='scan_address',
|
|
||||||
field=models.GenericIPAddressField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.2.7 on 2021-09-28 23:11
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0011_settings_scan_address'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='device',
|
|
||||||
name='name',
|
|
||||||
field=models.SlugField(default='Unknown', max_length=100),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
# Generated by Django 3.2.12 on 2022-03-12 10:05
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wol', '0012_alter_device_name'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Port',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('number', models.PositiveIntegerField()),
|
|
||||||
('name', models.SlugField()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name='settings',
|
|
||||||
old_name='enable_notifications',
|
|
||||||
new_name='notifications',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='device',
|
|
||||||
name='scheduled_wake',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='settings',
|
|
||||||
name='enable_console_logging',
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='device',
|
|
||||||
name='shutdown_cmd',
|
|
||||||
field=models.TextField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='settings',
|
|
||||||
name='interval',
|
|
||||||
field=models.PositiveSmallIntegerField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='device',
|
|
||||||
name='id',
|
|
||||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='settings',
|
|
||||||
name='id',
|
|
||||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='websocket',
|
|
||||||
name='id',
|
|
||||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='device',
|
|
||||||
name='port',
|
|
||||||
field=models.ManyToManyField(blank=True, to='wol.Port'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class Port(models.Model):
|
|
||||||
number = models.PositiveIntegerField()
|
|
||||||
name = models.SlugField()
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Device(models.Model):
|
|
||||||
name = models.SlugField(default="Unknown", max_length=100)
|
|
||||||
ip = models.GenericIPAddressField()
|
|
||||||
mac = models.CharField(max_length=17)
|
|
||||||
netmask = models.CharField(max_length=15, default="255.255.255.0", blank=False, null=False)
|
|
||||||
link = models.URLField(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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
|
|
||||||
from wol.consumers import WSConsumer
|
|
||||||
|
|
||||||
ws_urlpatterns = [
|
|
||||||
path("wol/", WSConsumer.as_asgi())
|
|
||||||
]
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import socket
|
|
||||||
import subprocess
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from celery import shared_task
|
|
||||||
from channels.layers import get_channel_layer
|
|
||||||
from django.db import connection
|
|
||||||
from django_celery_beat.models import CrontabSchedule, PeriodicTask
|
|
||||||
|
|
||||||
from wol.commands import shutdown, wake
|
|
||||||
from wol.models import Device, Port, Websocket
|
|
||||||
|
|
||||||
channel_layer = get_channel_layer()
|
|
||||||
|
|
||||||
|
|
||||||
class WolDevice:
|
|
||||||
def ping_device(self, ip):
|
|
||||||
try:
|
|
||||||
subprocess.check_output(["ping", "-c", "1", "-W", "0.5", ip])
|
|
||||||
return True
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def check_port(self, ip, port):
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.settimeout(0.5)
|
|
||||||
if sock.connect_ex((ip, port)) == 0:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def start(self, dev):
|
|
||||||
data = {
|
|
||||||
"id": dev.id,
|
|
||||||
"name": dev.name,
|
|
||||||
"ip": dev.ip,
|
|
||||||
"mac": dev.mac,
|
|
||||||
"netmask": dev.netmask,
|
|
||||||
"up": False,
|
|
||||||
"link": dev.link,
|
|
||||||
"ports": [],
|
|
||||||
"wake": {
|
|
||||||
"enabled": False,
|
|
||||||
"cron": ""
|
|
||||||
},
|
|
||||||
"shutdown": {
|
|
||||||
"enabled": False,
|
|
||||||
"cron": "",
|
|
||||||
"command": dev.shutdown_cmd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# add ports
|
|
||||||
for p in Port.objects.all().order_by("number"):
|
|
||||||
data["ports"].append({
|
|
||||||
"number": p.number,
|
|
||||||
"name": p.name,
|
|
||||||
"checked": True if p in dev.port.all() else False,
|
|
||||||
"open": False
|
|
||||||
})
|
|
||||||
|
|
||||||
# set status for device and ports
|
|
||||||
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)
|
|
||||||
if self.check_port(dev.ip, port.number):
|
|
||||||
data["ports"][index]["checked"] = True
|
|
||||||
data["ports"][index]["open"] = True
|
|
||||||
else:
|
|
||||||
data["ports"][index]["checked"] = True
|
|
||||||
data["ports"][index]["open"] = False
|
|
||||||
|
|
||||||
# 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": {
|
|
||||||
"type": "status",
|
|
||||||
"message": data
|
|
||||||
}})
|
|
||||||
|
|
||||||
connection.close()
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def ping_all_devices():
|
|
||||||
if Websocket.objects.first().visitors == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
devices = Device.objects.all()
|
|
||||||
|
|
||||||
for dev in devices:
|
|
||||||
d = WolDevice()
|
|
||||||
t = threading.Thread(target=d.start, args=(dev,))
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def scheduled_wake(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 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
|
|
||||||
}})
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
4
app/frontend/.gitignore
vendored
4
app/frontend/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
/node_modules/
|
|
||||||
/public/build/
|
|
||||||
|
|
||||||
.DS_Store
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
*Psst — looking for a more complete solution? Check out [SvelteKit](https://kit.svelte.dev), the official framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.*
|
|
||||||
|
|
||||||
*Looking for a shareable component template instead? You can [use SvelteKit for that as well](https://kit.svelte.dev/docs#packaging) or the older [sveltejs/component-template](https://github.com/sveltejs/component-template)*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# svelte app
|
|
||||||
|
|
||||||
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
|
|
||||||
|
|
||||||
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx degit sveltejs/template svelte-app
|
|
||||||
cd svelte-app
|
|
||||||
```
|
|
||||||
|
|
||||||
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
|
|
||||||
|
|
||||||
|
|
||||||
## Get started
|
|
||||||
|
|
||||||
Install the dependencies...
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd svelte-app
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
...then start [Rollup](https://rollupjs.org):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Navigate to [localhost:8080](http://localhost:8080). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
|
|
||||||
|
|
||||||
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
|
|
||||||
|
|
||||||
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
|
|
||||||
|
|
||||||
## Building and running in production mode
|
|
||||||
|
|
||||||
To create an optimised version of the app:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
|
|
||||||
|
|
||||||
|
|
||||||
## Single-page app mode
|
|
||||||
|
|
||||||
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
|
|
||||||
|
|
||||||
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
|
|
||||||
|
|
||||||
```js
|
|
||||||
"start": "sirv public --single"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using TypeScript
|
|
||||||
|
|
||||||
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node scripts/setupTypeScript.js
|
|
||||||
```
|
|
||||||
|
|
||||||
Or remove the script via:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm scripts/setupTypeScript.js
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to use `baseUrl` or `path` aliases within your `tsconfig`, you need to set up `@rollup/plugin-alias` to tell Rollup to resolve the aliases. For more info, see [this StackOverflow question](https://stackoverflow.com/questions/63427935/setup-tsconfig-path-in-svelte).
|
|
||||||
|
|
||||||
## Deploying to the web
|
|
||||||
|
|
||||||
### With [Vercel](https://vercel.com)
|
|
||||||
|
|
||||||
Install `vercel` if you haven't already:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g vercel
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, from within your project folder:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd public
|
|
||||||
vercel deploy --name my-project
|
|
||||||
```
|
|
||||||
|
|
||||||
### With [surge](https://surge.sh/)
|
|
||||||
|
|
||||||
Install `surge` if you haven't already:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g surge
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, from within your project folder:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
surge public my-project.surge.sh
|
|
||||||
```
|
|
||||||
2872
app/frontend/package-lock.json
generated
2872
app/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "svelte-app",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"build": "rollup -c",
|
|
||||||
"dev": "rollup -c -w",
|
|
||||||
"start": "sirv public --no-clear --host"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@rollup/plugin-commonjs": "^17.0.0",
|
|
||||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
|
||||||
"rollup": "^2.3.4",
|
|
||||||
"rollup-plugin-copy": "^3.4.0",
|
|
||||||
"rollup-plugin-css-only": "^3.1.0",
|
|
||||||
"rollup-plugin-livereload": "^2.0.0",
|
|
||||||
"rollup-plugin-svelte": "^7.0.0",
|
|
||||||
"rollup-plugin-terser": "^7.0.0",
|
|
||||||
"svelte": "^3.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
|
||||||
"@popperjs/core": "^2.11.5",
|
|
||||||
"@rollup/plugin-replace": "^4.0.0",
|
|
||||||
"bootstrap": "^5.1.3",
|
|
||||||
"dotenv": "^16.0.0",
|
|
||||||
"postcss": "^8.4.6",
|
|
||||||
"sass": "^1.49.9",
|
|
||||||
"sirv-cli": "^2.0.0",
|
|
||||||
"svelte-preprocess": "^4.10.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
main.svelte-1tky8bj {
|
|
||||||
text-align: center;
|
|
||||||
padding: 1em;
|
|
||||||
max-width: 240px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1.svelte-1tky8bj {
|
|
||||||
color: #ff3e00;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 4em;
|
|
||||||
font-weight: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
main.svelte-1tky8bj {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset='utf-8'>
|
|
||||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
|
||||||
|
|
||||||
<link rel='icon' type='image/png' href='favicon.png'>
|
|
||||||
<link rel='stylesheet' href='global.css'>
|
|
||||||
<link rel='stylesheet' href='build/bundle.css'>
|
|
||||||
|
|
||||||
<script defer src='build/bundle.js'></script>
|
|
||||||
<script defer src='build/popper.min.js'></script>
|
|
||||||
<script defer src='build/bootstrap.min.js'></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import svelte from 'rollup-plugin-svelte';
|
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
|
||||||
import livereload from 'rollup-plugin-livereload';
|
|
||||||
import { terser } from 'rollup-plugin-terser';
|
|
||||||
import css from 'rollup-plugin-css-only';
|
|
||||||
import preprocess from 'svelte-preprocess';
|
|
||||||
import copy from 'rollup-plugin-copy';
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
import replace from '@rollup/plugin-replace';
|
|
||||||
|
|
||||||
dotenv.config()
|
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH;
|
|
||||||
|
|
||||||
function serve() {
|
|
||||||
let server;
|
|
||||||
|
|
||||||
function toExit() {
|
|
||||||
if (server) server.kill(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
writeBundle() {
|
|
||||||
if (server) return;
|
|
||||||
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
|
|
||||||
stdio: ['ignore', 'inherit', 'inherit'],
|
|
||||||
shell: true
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGTERM', toExit);
|
|
||||||
process.on('exit', toExit);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: 'src/main.js',
|
|
||||||
output: {
|
|
||||||
sourcemap: true,
|
|
||||||
format: 'iife',
|
|
||||||
name: 'app',
|
|
||||||
file: 'public/build/bundle.js'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
svelte({
|
|
||||||
compilerOptions: {
|
|
||||||
// enable run-time checks when not in production
|
|
||||||
dev: !production,
|
|
||||||
},
|
|
||||||
preprocess: preprocess(),
|
|
||||||
}),
|
|
||||||
replace({
|
|
||||||
BACKEND_PORT: JSON.stringify(process.env.BACKEND_PORT),
|
|
||||||
BACKEND_IS_PROXIED: JSON.stringify(process.env.BACKEND_IS_PROXIED.toLowerCase() === 'true'),
|
|
||||||
PAGE_TITLE: JSON.stringify(process.env.PAGE_TITLE),
|
|
||||||
preventAssignment: true
|
|
||||||
}),
|
|
||||||
// we'll extract any component CSS out into
|
|
||||||
// a separate file - better for performance
|
|
||||||
css({ output: 'bundle.css' }),
|
|
||||||
copy({
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
src: [
|
|
||||||
"node_modules/@fortawesome/fontawesome-free/webfonts",
|
|
||||||
"node_modules/bootstrap/dist/js/bootstrap.min.js",
|
|
||||||
"node_modules/bootstrap/dist/js/bootstrap.min.js.map",
|
|
||||||
"node_modules/@popperjs/core/dist/umd/popper.min.js",
|
|
||||||
"node_modules/@popperjs/core/dist/umd/popper.min.js.map"
|
|
||||||
],
|
|
||||||
dest: "public/build/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
|
|
||||||
// If you have external dependencies installed from
|
|
||||||
// npm, you'll most likely need these plugins. In
|
|
||||||
// some cases you'll need additional configuration -
|
|
||||||
// consult the documentation for details:
|
|
||||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
|
||||||
resolve({
|
|
||||||
browser: true,
|
|
||||||
dedupe: ['svelte']
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
|
|
||||||
// In dev mode, call `npm run start` once
|
|
||||||
// the bundle has been generated
|
|
||||||
!production && serve(),
|
|
||||||
|
|
||||||
// Watch the `public` directory and refresh the
|
|
||||||
// browser on changes when not in production
|
|
||||||
!production && livereload('public'),
|
|
||||||
|
|
||||||
// If we're building for production (npm run build
|
|
||||||
// instead of npm run dev), minify
|
|
||||||
production && terser()
|
|
||||||
],
|
|
||||||
watch: {
|
|
||||||
clearScreen: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
/** This script modifies the project to support TS code in .svelte files like:
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export let name: string;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
As well as validating the code for CI.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** To work on this script:
|
|
||||||
rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require("fs")
|
|
||||||
const path = require("path")
|
|
||||||
const { argv } = require("process")
|
|
||||||
|
|
||||||
const projectRoot = argv[2] || path.join(__dirname, "..")
|
|
||||||
|
|
||||||
// Add deps to pkg.json
|
|
||||||
const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
|
|
||||||
packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
|
|
||||||
"svelte-check": "^2.0.0",
|
|
||||||
"svelte-preprocess": "^4.0.0",
|
|
||||||
"@rollup/plugin-typescript": "^8.0.0",
|
|
||||||
"typescript": "^4.0.0",
|
|
||||||
"tslib": "^2.0.0",
|
|
||||||
"@tsconfig/svelte": "^2.0.0"
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add script for checking
|
|
||||||
packageJSON.scripts = Object.assign(packageJSON.scripts, {
|
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
|
||||||
})
|
|
||||||
|
|
||||||
// Write the package JSON
|
|
||||||
fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
|
|
||||||
|
|
||||||
// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
|
|
||||||
const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
|
|
||||||
const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
|
|
||||||
fs.renameSync(beforeMainJSPath, afterMainTSPath)
|
|
||||||
|
|
||||||
// Switch the app.svelte file to use TS
|
|
||||||
const appSveltePath = path.join(projectRoot, "src", "App.svelte")
|
|
||||||
let appFile = fs.readFileSync(appSveltePath, "utf8")
|
|
||||||
appFile = appFile.replace("<script>", '<script lang="ts">')
|
|
||||||
appFile = appFile.replace("export let name;", 'export let name: string;')
|
|
||||||
fs.writeFileSync(appSveltePath, appFile)
|
|
||||||
|
|
||||||
// Edit rollup config
|
|
||||||
const rollupConfigPath = path.join(projectRoot, "rollup.config.js")
|
|
||||||
let rollupConfig = fs.readFileSync(rollupConfigPath, "utf8")
|
|
||||||
|
|
||||||
// Edit imports
|
|
||||||
rollupConfig = rollupConfig.replace(`'rollup-plugin-terser';`, `'rollup-plugin-terser';
|
|
||||||
import sveltePreprocess from 'svelte-preprocess';
|
|
||||||
import typescript from '@rollup/plugin-typescript';`)
|
|
||||||
|
|
||||||
// Replace name of entry point
|
|
||||||
rollupConfig = rollupConfig.replace(`'src/main.js'`, `'src/main.ts'`)
|
|
||||||
|
|
||||||
// Add preprocessor
|
|
||||||
rollupConfig = rollupConfig.replace(
|
|
||||||
'compilerOptions:',
|
|
||||||
'preprocess: sveltePreprocess({ sourceMap: !production }),\n\t\t\tcompilerOptions:'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add TypeScript
|
|
||||||
rollupConfig = rollupConfig.replace(
|
|
||||||
'commonjs(),',
|
|
||||||
'commonjs(),\n\t\ttypescript({\n\t\t\tsourceMap: !production,\n\t\t\tinlineSources: !production\n\t\t}),'
|
|
||||||
);
|
|
||||||
fs.writeFileSync(rollupConfigPath, rollupConfig)
|
|
||||||
|
|
||||||
// Add TSConfig
|
|
||||||
const tsconfig = `{
|
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
|
||||||
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
|
||||||
}`
|
|
||||||
const tsconfigPath = path.join(projectRoot, "tsconfig.json")
|
|
||||||
fs.writeFileSync(tsconfigPath, tsconfig)
|
|
||||||
|
|
||||||
// Add global.d.ts
|
|
||||||
const dtsPath = path.join(projectRoot, "src", "global.d.ts")
|
|
||||||
fs.writeFileSync(dtsPath, `/// <reference types="svelte" />`)
|
|
||||||
|
|
||||||
// Delete this script, but not during testing
|
|
||||||
if (!argv[2]) {
|
|
||||||
// Remove the script
|
|
||||||
fs.unlinkSync(path.join(__filename))
|
|
||||||
|
|
||||||
// Check for Mac's DS_store file, and if it's the only one left remove it
|
|
||||||
const remainingFiles = fs.readdirSync(path.join(__dirname))
|
|
||||||
if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') {
|
|
||||||
fs.unlinkSync(path.join(__dirname, '.DS_store'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the scripts folder is empty
|
|
||||||
if (fs.readdirSync(path.join(__dirname)).length === 0) {
|
|
||||||
// Remove the scripts folder
|
|
||||||
fs.rmdirSync(path.join(__dirname))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds the extension recommendation
|
|
||||||
fs.mkdirSync(path.join(projectRoot, ".vscode"), { recursive: true })
|
|
||||||
fs.writeFileSync(path.join(projectRoot, ".vscode", "extensions.json"), `{
|
|
||||||
"recommendations": ["svelte.svelte-vscode"]
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
|
|
||||||
console.log("Converted to TypeScript.")
|
|
||||||
|
|
||||||
if (fs.existsSync(path.join(projectRoot, "node_modules"))) {
|
|
||||||
console.log("\nYou will need to re-run your dependency manager to get started.")
|
|
||||||
}
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import socketStore from "./socketStore.js";
|
|
||||||
import Navbar from "./components/Navbar.svelte";
|
|
||||||
import DeviceCard from "./components/DeviceCard.svelte";
|
|
||||||
import Toast from "./components/Toast.svelte";
|
|
||||||
|
|
||||||
let visitors = 0;
|
|
||||||
let devices = [];
|
|
||||||
let settings = {};
|
|
||||||
let toast = {
|
|
||||||
title: "",
|
|
||||||
message: "",
|
|
||||||
color: "",
|
|
||||||
show: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
socketStore.subscribeStatus((status) => {
|
|
||||||
if (status == "open") {
|
|
||||||
showToast("Websocket", "Connected", "success");
|
|
||||||
} else if (status == "close") {
|
|
||||||
showToast(
|
|
||||||
"Websocket",
|
|
||||||
"Connection closed. Trying to reconnect ...",
|
|
||||||
"danger"
|
|
||||||
);
|
|
||||||
} else if (status == "error") {
|
|
||||||
showToast(
|
|
||||||
"Websocket",
|
|
||||||
"Error when connecting to websocket",
|
|
||||||
"danger"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socketStore.subscribeMsg((currentMessage) => {
|
|
||||||
if (currentMessage.type == "init") {
|
|
||||||
// create devices
|
|
||||||
devices = [...currentMessage.message.devices];
|
|
||||||
devices = devices;
|
|
||||||
devices.sort(compare);
|
|
||||||
settings = currentMessage.message.settings;
|
|
||||||
} else if (currentMessage.type == "status") {
|
|
||||||
// set device array and sort
|
|
||||||
const index = devices.findIndex(
|
|
||||||
(x) => x.id == currentMessage.message.id
|
|
||||||
);
|
|
||||||
if (devices.length === 0 || index === -1) {
|
|
||||||
devices.push(currentMessage.message);
|
|
||||||
devices = devices;
|
|
||||||
} else {
|
|
||||||
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 == "pending") {
|
|
||||||
// set device pending
|
|
||||||
setPending(currentMessage.message);
|
|
||||||
} else if (currentMessage.type == "visitor") {
|
|
||||||
// update visitor count
|
|
||||||
visitors = currentMessage.message;
|
|
||||||
} else if (currentMessage.type == "delete") {
|
|
||||||
// delete device
|
|
||||||
const devCol = document.querySelector(
|
|
||||||
`#device-col-${currentMessage.message}`
|
|
||||||
);
|
|
||||||
devCol.remove();
|
|
||||||
} else if (currentMessage.type == "scan_network") {
|
|
||||||
// set scanned network devices
|
|
||||||
if (!currentMessage.message) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
settings["scan_network"] = currentMessage.message;
|
|
||||||
const btnScan = document.querySelector("#btnScan");
|
|
||||||
const btnScanSpinner =
|
|
||||||
document.querySelector("#btnScanSpinner");
|
|
||||||
const btnScanText = document.querySelector("#btnScanText");
|
|
||||||
btnScan.disabled = false;
|
|
||||||
btnScanSpinner.classList.add("d-none");
|
|
||||||
btnScanText.innerText = "Scan";
|
|
||||||
} else if (currentMessage.type == "backup") {
|
|
||||||
// download backup file
|
|
||||||
const now = new Date();
|
|
||||||
const fileName = `upsnap_backup_${now.toISOString()}.json`;
|
|
||||||
const a = document.createElement("a");
|
|
||||||
const file = new Blob(
|
|
||||||
[JSON.stringify(currentMessage.message)],
|
|
||||||
{ type: "text/plain" }
|
|
||||||
);
|
|
||||||
a.href = URL.createObjectURL(file);
|
|
||||||
a.download = fileName;
|
|
||||||
a.click();
|
|
||||||
} else if (currentMessage.type == "operationStatus") {
|
|
||||||
if (currentMessage.message == "Success") {
|
|
||||||
showToast(
|
|
||||||
currentMessage.message,
|
|
||||||
"Changes were saved",
|
|
||||||
"success"
|
|
||||||
);
|
|
||||||
} else if (currentMessage.message == "Error") {
|
|
||||||
showToast(
|
|
||||||
currentMessage.message,
|
|
||||||
"Error while saving the device. Please check the logs.",
|
|
||||||
"danger"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function setUp(device) {
|
|
||||||
const dot = document.querySelector(`#dot-${device.id}`);
|
|
||||||
const spinner = document.querySelector(`#spinner-${device.id}`);
|
|
||||||
if (dot) {
|
|
||||||
if (dot.classList.contains("danger")) {
|
|
||||||
showToast(device.name, "Device is up!", "success");
|
|
||||||
}
|
|
||||||
dot.style.animation = "none";
|
|
||||||
dot.offsetWidth;
|
|
||||||
if (!spinner.classList.contains("d-none")) {
|
|
||||||
spinner.classList.add("d-none");
|
|
||||||
dot.classList.remove("d-none", "danger");
|
|
||||||
dot.classList.add("success");
|
|
||||||
} else {
|
|
||||||
dot.style.animation = "on-pulse 1s normal";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDown(device) {
|
|
||||||
const dot = document.querySelector(`#dot-${device.id}`);
|
|
||||||
const spinner = document.querySelector(`#spinner-${device.id}`);
|
|
||||||
if (dot) {
|
|
||||||
if (dot.classList.contains("success")) {
|
|
||||||
showToast(device.name, "Device is down!", "danger");
|
|
||||||
}
|
|
||||||
dot.style.animation = "none";
|
|
||||||
dot.offsetWidth;
|
|
||||||
if (!spinner.classList.contains("d-none")) {
|
|
||||||
spinner.classList.add("d-none");
|
|
||||||
dot.classList.remove("d-none", "success");
|
|
||||||
dot.classList.add("danger");
|
|
||||||
} else {
|
|
||||||
dot.style.animation = "off-pulse 1s normal";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPending(id) {
|
|
||||||
const dot = document.querySelector(`#dot-${id}`);
|
|
||||||
const spinner = document.querySelector(`#spinner-${id}`);
|
|
||||||
dot.classList.add("d-none");
|
|
||||||
spinner.classList.remove("d-none");
|
|
||||||
}
|
|
||||||
|
|
||||||
function showToast(title, message, color) {
|
|
||||||
if (settings.notifications === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toast.title = title;
|
|
||||||
toast.message = message;
|
|
||||||
toast.color = color;
|
|
||||||
toast.show = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.show = false;
|
|
||||||
}, 4000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function compare(a, b) {
|
|
||||||
if (a.name < b.name) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.name > b.name) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<Navbar {settings} {visitors} />
|
|
||||||
<div class="container mb-3">
|
|
||||||
<div class="row">
|
|
||||||
{#each devices as device}
|
|
||||||
<DeviceCard {device} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Toast {toast} />
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<style lang="scss" global>
|
|
||||||
@import "./variables.scss";
|
|
||||||
@import "../node_modules/bootstrap/scss/bootstrap";
|
|
||||||
@import "../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
|
||||||
@import "../node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
|
|
||||||
@import "../node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
|
|
||||||
@import "theme.scss";
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: var(--color-text);
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background-color: var(--bg-modal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
border-bottom: 1px solid var(--color-text);
|
|
||||||
|
|
||||||
.btn-close {
|
|
||||||
background: var(--svg-close);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
border-top: 1px solid var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn,
|
|
||||||
button {
|
|
||||||
&.btn-light {
|
|
||||||
color: var(--color-text);
|
|
||||||
background-color: var(--bg-lighter);
|
|
||||||
border-width: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.btn-outline-success {
|
|
||||||
border-color: $success;
|
|
||||||
&:hover {
|
|
||||||
background-color: $success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.btn-outline-danger {
|
|
||||||
border-color: $danger;
|
|
||||||
&:hover {
|
|
||||||
background-color: $danger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
&:focus {
|
|
||||||
border-color: inherit;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.callout {
|
|
||||||
padding: 1rem;
|
|
||||||
border-left-width: 0.25rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
|
|
||||||
&.callout-info {
|
|
||||||
background-color: var(--info-dark-transparent);
|
|
||||||
border: 1px solid var(--info-dark-transparent);
|
|
||||||
border-left: 5px solid var(--info);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.callout-danger {
|
|
||||||
background-color: var(--danger-dark-transparent);
|
|
||||||
border: 1px solid var(--danger-dark-transparent);
|
|
||||||
border-left: 5px solid var(--danger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes on-pulse {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0 0 0 0 $success;
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
box-shadow: 0 0 0 15px rgba(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes off-pulse {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0 0 0 0 $danger;
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
box-shadow: 0 0 0 15px rgba(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<script>
|
|
||||||
const preferesDark = window.matchMedia("(prefers-color-scheme: dark)");
|
|
||||||
|
|
||||||
preferesDark.addEventListener("change", (e) => {
|
|
||||||
setTheme(e.matches ? "dark" : "light");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (localStorage.getItem("data-theme") === null) {
|
|
||||||
setTheme(preferesDark.matches ? "dark" : "light");
|
|
||||||
} else {
|
|
||||||
setTheme(localStorage.getItem("data-theme"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTheme(color) {
|
|
||||||
if (color == "system") {
|
|
||||||
document.documentElement.setAttribute(
|
|
||||||
"data-theme",
|
|
||||||
preferesDark ? "dark" : "light"
|
|
||||||
);
|
|
||||||
localStorage.setItem("data-theme", preferesDark ? "dark" : "light");
|
|
||||||
} else {
|
|
||||||
document.documentElement.setAttribute("data-theme", color);
|
|
||||||
localStorage.setItem("data-theme", color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="dropdown">
|
|
||||||
<button
|
|
||||||
class="btn btn-light dropdown-toggle px-3 me-2 py-2"
|
|
||||||
type="button"
|
|
||||||
id="darkModeButton"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-palette me-2" />Theme
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="darkModeButton">
|
|
||||||
<li>
|
|
||||||
<button class="dropdown-item" on:click={() => setTheme("dark")}>
|
|
||||||
<i class="fa-solid fa-moon me-2" />Dark
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button class="dropdown-item" on:click={() => setTheme("light")}>
|
|
||||||
<i class="fa-solid fa-sun me-2" />Light
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button class="dropdown-item" on:click={() => setTheme("system")}>
|
|
||||||
<i class="fa-solid fa-desktop me-2" />System
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
@@ -1,671 +0,0 @@
|
|||||||
<script>
|
|
||||||
import socketStore from "../socketStore.js";
|
|
||||||
export let device;
|
|
||||||
|
|
||||||
let modalDevice = JSON.parse(JSON.stringify(device));
|
|
||||||
let customPort = {};
|
|
||||||
|
|
||||||
function wake(id) {
|
|
||||||
socketStore.sendMessage({
|
|
||||||
type: "wake",
|
|
||||||
id: id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function shutdown(id) {
|
|
||||||
socketStore.sendMessage({
|
|
||||||
type: "shutdown",
|
|
||||||
id: id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteDevice() {
|
|
||||||
socketStore.sendMessage({
|
|
||||||
type: "delete_device",
|
|
||||||
id: modalDevice.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDevice() {
|
|
||||||
device = modalDevice;
|
|
||||||
socketStore.sendMessage({
|
|
||||||
type: "update_device",
|
|
||||||
data: modalDevice,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePort() {
|
|
||||||
if (!customPort.number) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const index = modalDevice.ports.findIndex(
|
|
||||||
(x) => x.number == customPort.number
|
|
||||||
);
|
|
||||||
if (customPort.name) {
|
|
||||||
// add port
|
|
||||||
if (index === -1) {
|
|
||||||
customPort.checked = true;
|
|
||||||
modalDevice.ports.push(JSON.parse(JSON.stringify(customPort)));
|
|
||||||
} else {
|
|
||||||
customPort.checked = modalDevice.ports[index].checked;
|
|
||||||
modalDevice.ports[index] = JSON.parse(
|
|
||||||
JSON.stringify(customPort)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// delete port
|
|
||||||
if (index >= 0) {
|
|
||||||
modalDevice.ports.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modalDevice = modalDevice;
|
|
||||||
// send to backend
|
|
||||||
socketStore.sendMessage({
|
|
||||||
type: "update_port",
|
|
||||||
data: customPort,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openModal() {
|
|
||||||
modalDevice = Object.assign({}, device);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validatePort() {
|
|
||||||
if (typeof customPort.number != "number") {
|
|
||||||
customPort.number = 1;
|
|
||||||
} else if (customPort.number > 65535) {
|
|
||||||
customPort.number = 65535;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="device-col-{device.id}"
|
|
||||||
class="col-xs-12 col-sm-6 col-md-4 col-lg-3 g-4"
|
|
||||||
>
|
|
||||||
<div class="card border-0 p-3 pt-2">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-auto me-auto">
|
|
||||||
<div
|
|
||||||
id="spinner-{device.id}"
|
|
||||||
class="spinner-border warning d-none"
|
|
||||||
role="status"
|
|
||||||
/>
|
|
||||||
{#if device.up === true}
|
|
||||||
{#if device.shutdown.command}
|
|
||||||
<div
|
|
||||||
class="hover"
|
|
||||||
on:click={() => shutdown(device.id)}
|
|
||||||
on:keydown={() => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<i
|
|
||||||
id="dot-{device.id}"
|
|
||||||
class="fa-solid fa-power-off fa-2x success"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{:else if device.up === false}
|
|
||||||
<div
|
|
||||||
class="hover"
|
|
||||||
on:click={() => wake(device.id)}
|
|
||||||
on:keydown={() => wake(device.id)}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
id="dot-{device.id}"
|
|
||||||
class="fa-solid fa-power-off fa-2x danger"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="hover" role="button">
|
|
||||||
<i
|
|
||||||
id="dot-{device.id}"
|
|
||||||
class="fa-solid fa-power-off fa-2x text-muted"
|
|
||||||
/>
|
|
||||||
</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" />
|
|
||||||
</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" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div
|
|
||||||
class="col-auto hover"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#device-modal-{device.id}"
|
|
||||||
role="button"
|
|
||||||
on:click={() => openModal()}
|
|
||||||
on:keydown={() => openModal()}
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-ellipsis-vertical fa-2x" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if device.link}
|
|
||||||
<h5 class="card-title fw-bold my-2">
|
|
||||||
<a class="inherit-color" href={device.link}>{device.name}</a
|
|
||||||
>
|
|
||||||
</h5>
|
|
||||||
{:else}
|
|
||||||
<h5 class="card-title fw-bold my-2">{device.name}</h5>
|
|
||||||
{/if}
|
|
||||||
<h6 class="card-subtitle mb-2 text-muted">{device.ip}</h6>
|
|
||||||
<ul class="list-group">
|
|
||||||
{#each device.ports as port}
|
|
||||||
{#if port.checked === true}
|
|
||||||
<li class="list-group-item">
|
|
||||||
<i
|
|
||||||
class="fa-solid fa-circle align-middle {port.open
|
|
||||||
? 'success'
|
|
||||||
: 'danger'}"
|
|
||||||
/>
|
|
||||||
{port.name}
|
|
||||||
<span class="text-muted">({port.number})</span>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="modal fade"
|
|
||||||
id="device-modal-{device.id}"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-labelledby="device-modal-{device.id}-label"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div class="modal-dialog modal-dialog-scrollable">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5
|
|
||||||
class="modal-title fw-bold"
|
|
||||||
id="device-modal-{modalDevice.id}-label"
|
|
||||||
>
|
|
||||||
{modalDevice.name}
|
|
||||||
</h5>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-close"
|
|
||||||
data-bs-dismiss="modal"
|
|
||||||
aria-label="Close"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form
|
|
||||||
id="form-{modalDevice.id}"
|
|
||||||
on:submit|preventDefault={updateDevice}
|
|
||||||
>
|
|
||||||
<!-- general -->
|
|
||||||
<h5 class="fw-bold">General</h5>
|
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-sm">
|
|
||||||
<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">
|
|
||||||
<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 mb-2">
|
|
||||||
<div class="col-sm">
|
|
||||||
<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}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm">
|
|
||||||
<label
|
|
||||||
for="inputNetmask{modalDevice.id}"
|
|
||||||
class="form-label">Netmask</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="inputNetmask{modalDevice.id}"
|
|
||||||
bind:value={modalDevice.netmask}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label
|
|
||||||
for="inputLinkAddDevice"
|
|
||||||
class="form-label">Web link</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="inputILinkAddDevice"
|
|
||||||
placeholder="http://...."
|
|
||||||
bind:value={modalDevice.link}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 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}
|
|
||||||
{#each modalDevice.ports as port}
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="form-check-input"
|
|
||||||
id="{device.id}-port-{port.number}"
|
|
||||||
bind:checked={port["checked"]}
|
|
||||||
/>
|
|
||||||
<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-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 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>
|
|
||||||
<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" />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>
|
|
||||||
<!-- 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="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="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>
|
|
||||||
<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" />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"
|
|
||||||
rel="noreferrer">valid syntax here</a
|
|
||||||
>
|
|
||||||
or
|
|
||||||
<a
|
|
||||||
href="https://crontab.guru/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer">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 -o 'StrictHostKeyChecking=no' 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" />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 -o 'StrictHostKeyChecking=no' user@hostname 'sudo shutdown'
|
|
||||||
|
|
||||||
# wake windows hosts
|
|
||||||
net rpc shutdown --ipaddress 192.168.1.1 --user user%password
|
|
||||||
</pre>
|
|
||||||
<p class="mb-0">
|
|
||||||
Read more about <a
|
|
||||||
href="https://linux.die.net/man/1/sshpass"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer">sshpass</a
|
|
||||||
>
|
|
||||||
or
|
|
||||||
<a
|
|
||||||
href="https://linux.die.net/man/8/net"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer">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>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer justify-content-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-danger"
|
|
||||||
data-bs-dismiss="modal"
|
|
||||||
on:click={deleteDevice}>Delete</button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
form="form-{modalDevice.id}"
|
|
||||||
class="btn btn-outline-success">Save changes</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.success {
|
|
||||||
color: var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
color: var(--warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
white-space: pre-line;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
border-radius: 2em;
|
|
||||||
background-color: var(--bg-lighter);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner-border {
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover {
|
|
||||||
&:hover {
|
|
||||||
text-shadow: 0px 0px 20px rgb(155, 155, 155);
|
|
||||||
transition: all 0.1s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-2x {
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-power-off {
|
|
||||||
border-radius: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-circle {
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item {
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inherit-color {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
<script>
|
|
||||||
import socketStore from "../socketStore.js";
|
|
||||||
import DarkToggle from "./DarkToggle.svelte";
|
|
||||||
export let visitors;
|
|
||||||
export let settings;
|
|
||||||
const pagetitle = PAGE_TITLE ? PAGE_TITLE : "UpSnap";
|
|
||||||
|
|
||||||
let addDevice = {
|
|
||||||
wake: {
|
|
||||||
enabled: false,
|
|
||||||
cron: "",
|
|
||||||
},
|
|
||||||
shutdown: {
|
|
||||||
enabled: false,
|
|
||||||
cron: "",
|
|
||||||
command: "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateDevice(data) {
|
|
||||||
if (Object.keys(data).length < 4) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socketStore.sendMessage({
|
|
||||||
type: "update_device",
|
|
||||||
data: data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSettings() {
|
|
||||||
socketStore.sendMessage({
|
|
||||||
type: "update_settings",
|
|
||||||
data: settings,
|
|
||||||
});
|
|
||||||
hideModal("settings");
|
|
||||||
}
|
|
||||||
|
|
||||||
function scanNetwork() {
|
|
||||||
socketStore.sendMessage({
|
|
||||||
type: "scan_network",
|
|
||||||
});
|
|
||||||
const btnScan = document.querySelector("#btnScan");
|
|
||||||
const btnScanSpinner = document.querySelector("#btnScanSpinner");
|
|
||||||
const btnScanText = document.querySelector("#btnScanText");
|
|
||||||
btnScan.disabled = true;
|
|
||||||
btnScanSpinner.classList.remove("d-none");
|
|
||||||
btnScanText.innerText = "Scanning...";
|
|
||||||
}
|
|
||||||
|
|
||||||
function addScan(i) {
|
|
||||||
document.querySelector(`#btnAdd${i}`).disabled = true;
|
|
||||||
const dev = settings.scan_network[i];
|
|
||||||
updateDevice(dev);
|
|
||||||
}
|
|
||||||
|
|
||||||
const restoreFromFile = (e) => {
|
|
||||||
let file = e.target.files[0];
|
|
||||||
let reader = new FileReader();
|
|
||||||
reader.readAsText(file);
|
|
||||||
reader.onload = (e) => {
|
|
||||||
let data = JSON.parse(e.target.result);
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
// v2 file restore
|
|
||||||
data.forEach((device) => {
|
|
||||||
updateDevice(device);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// v1 file restore
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
|
||||||
value["mac"] = key;
|
|
||||||
value["link"] = "";
|
|
||||||
updateDevice(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
hideModal("settings");
|
|
||||||
};
|
|
||||||
|
|
||||||
function backupToFile() {
|
|
||||||
socketStore.sendMessage({
|
|
||||||
type: "backup",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideModal(id) {
|
|
||||||
const modalEl = document.querySelector(`#${id}`);
|
|
||||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
|
||||||
modal.hide();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>{pagetitle}</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-sm">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="navbar-brand" href="/">
|
|
||||||
<img
|
|
||||||
src="favicon.png"
|
|
||||||
alt="Logo"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
class="me-2"
|
|
||||||
/>
|
|
||||||
{pagetitle}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="navbar-toggler"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#navbarNavAltMarkup"
|
|
||||||
aria-controls="navbarNavAltMarkup"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-label="Toggle navigation"
|
|
||||||
>
|
|
||||||
<span class="navbar-toggler-icon" />
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
|
|
||||||
<span class="ms-auto d-flex">
|
|
||||||
<DarkToggle />
|
|
||||||
<div class="dropdown">
|
|
||||||
<button
|
|
||||||
class="btn btn-light dropdown-toggle px-3 me-2 py-2"
|
|
||||||
type="button"
|
|
||||||
id="dropdownMenuButton1"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-wrench me-2" />More
|
|
||||||
</button>
|
|
||||||
<ul
|
|
||||||
class="dropdown-menu"
|
|
||||||
aria-labelledby="dropdownMenuButton1"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
class="dropdown-item"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#addDevice"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-plus me-2" />Add device
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
class="dropdown-item"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#settings"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-sliders me-2" />Settings
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="btn btn-light px-3 me-2 py-2 pe-none">
|
|
||||||
<i
|
|
||||||
class="me-2 fa-solid {visitors === 1
|
|
||||||
? 'fa-user'
|
|
||||||
: 'fa-user-group'}"
|
|
||||||
/>{visitors}
|
|
||||||
{visitors === 1 ? "Visitor" : "Visitors"}
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="modal fade"
|
|
||||||
id="addDevice"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-labelledby="addDeviceLabel"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title fw-bold" id="addDeviceLabel">
|
|
||||||
Add device
|
|
||||||
</h5>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-close"
|
|
||||||
data-bs-dismiss="modal"
|
|
||||||
aria-label="Close"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form
|
|
||||||
id="addForm"
|
|
||||||
on:submit|preventDefault={() => updateDevice(addDevice)}
|
|
||||||
>
|
|
||||||
<div class="row">
|
|
||||||
<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"
|
|
||||||
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"
|
|
||||||
placeholder="aa:aa:aa:aa:aa:aa"
|
|
||||||
bind:value={addDevice.mac}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<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"
|
|
||||||
placeholder="192.168.1.1"
|
|
||||||
bind:value={addDevice.ip}
|
|
||||||
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"
|
|
||||||
placeholder="255.255.255.0"
|
|
||||||
bind:value={addDevice.netmask}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="inputLinkAddDevice" class="form-label"
|
|
||||||
>Web link</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="inputILinkAddDevice"
|
|
||||||
placeholder="http://...."
|
|
||||||
bind:value={addDevice.link}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-auto ms-auto">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
form="addForm"
|
|
||||||
class="btn btn-outline-success"
|
|
||||||
>Add device</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h5 class="fw-bold">Network discovery</h5>
|
|
||||||
{#if !settings.discovery}
|
|
||||||
<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}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
id="btnScanSpinner"
|
|
||||||
class="spinner-grow spinner-grow-sm d-none"
|
|
||||||
role="status"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span id="btnScanText">Scan</span>
|
|
||||||
</button>
|
|
||||||
{#if settings.scan_network?.length}
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>IP</td>
|
|
||||||
<td>Netmask</td>
|
|
||||||
<td>Mac</td>
|
|
||||||
<td>Add</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each settings.scan_network as device, i}
|
|
||||||
<tr>
|
|
||||||
<td>{device.name}</td>
|
|
||||||
<td>{device.ip}</td>
|
|
||||||
<td>{device.netmask}</td>
|
|
||||||
<td>{device.mac}</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="btnAdd{i}"
|
|
||||||
class="btn btn-outline-secondary py-0"
|
|
||||||
on:click={() => addScan(i)}
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="fa-solid fa-plus fa-sm"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{/if}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="modal fade"
|
|
||||||
id="settings"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-labelledby="settingsLabel"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title fw-bold" id="settingsLabel">Settings</h5>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-close"
|
|
||||||
data-bs-dismiss="modal"
|
|
||||||
aria-label="Close"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form
|
|
||||||
id="settingsForm"
|
|
||||||
on:submit|preventDefault={updateSettings}
|
|
||||||
>
|
|
||||||
<h5 class="fw-bold">General</h5>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label
|
|
||||||
for="inputNetworkDiscovery"
|
|
||||||
class="form-label"
|
|
||||||
>Network discovery address</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="inputNetworkDiscovery"
|
|
||||||
placeholder="192.168.1.0/24"
|
|
||||||
bind:value={settings.discovery}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label
|
|
||||||
for="inputIntervalSettings"
|
|
||||||
class="form-label">Interval (seconds)</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="form-control"
|
|
||||||
id="inputIntervalSettings"
|
|
||||||
min="5"
|
|
||||||
bind:value={settings.interval}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm">
|
|
||||||
<div class="form-check">
|
|
||||||
<label
|
|
||||||
class="form-check-label"
|
|
||||||
for="flexCheckDefault"
|
|
||||||
>
|
|
||||||
Enable notifications
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
value=""
|
|
||||||
id="flexCheckDefault"
|
|
||||||
bind:checked={settings.notifications}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-auto ms-auto">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
form="settingsForm"
|
|
||||||
class="btn btn-outline-success">Save</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h5 class="fw-bold">Backup/Restore</h5>
|
|
||||||
<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
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="inputRestore"
|
|
||||||
type="file"
|
|
||||||
class="form-control"
|
|
||||||
accept=".json"
|
|
||||||
on:change={(e) => restoreFromFile(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
on:click={backupToFile}
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-download me-2" />
|
|
||||||
Export .json
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss" global>
|
|
||||||
.table {
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
background-color: var(--bg-lighter);
|
|
||||||
|
|
||||||
li:first-child > button {
|
|
||||||
border-radius: 0.5rem 0.5rem 0rem 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li:last-child > button {
|
|
||||||
border-radius: 0rem 0rem 0.5rem 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li:only-child > button {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
color: var(--color-text);
|
|
||||||
background-color: var(--bg-lighter);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-bg);
|
|
||||||
background-color: var(--color-text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let toast;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="position-fixed bottom-0 end-0 p-3 toast-container">
|
|
||||||
<div
|
|
||||||
class="toast fade {toast.show ? 'show' : 'hide'} {toast.color}-bg"
|
|
||||||
role="alert"
|
|
||||||
aria-live="assertive"
|
|
||||||
aria-atomic="true"
|
|
||||||
>
|
|
||||||
<div class="toast-header">
|
|
||||||
<strong id="toast-title" class="me-auto">{toast.title}</strong>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-close"
|
|
||||||
data-bs-dismiss="toast"
|
|
||||||
aria-label="Close"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="toast-body fw-bold">
|
|
||||||
{toast.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import "../variables.scss";
|
|
||||||
|
|
||||||
.toast-container {
|
|
||||||
z-index: 11;
|
|
||||||
border-radius: $toast-border-radius;
|
|
||||||
|
|
||||||
div > .toast-body {
|
|
||||||
color: $dark-lighter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-bg {
|
|
||||||
background-color: var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger-bg {
|
|
||||||
background-color: var(--danger);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import App from './App.svelte';
|
|
||||||
|
|
||||||
|
|
||||||
const app = new App({
|
|
||||||
target: document.body
|
|
||||||
});
|
|
||||||
|
|
||||||
export default app;
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import {
|
|
||||||
writable
|
|
||||||
} from 'svelte/store';
|
|
||||||
|
|
||||||
const status = writable('');
|
|
||||||
const message = writable('');
|
|
||||||
let socket;
|
|
||||||
|
|
||||||
function initSocket() {
|
|
||||||
if (BACKEND_IS_PROXIED) {
|
|
||||||
const socketUrl = new URL('wol/', window.location.href);
|
|
||||||
socketUrl.protocol = socketUrl.protocol.replace('http', 'ws');
|
|
||||||
|
|
||||||
socket = new WebSocket(socketUrl);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
socket = new WebSocket(`ws://${location.hostname}:${BACKEND_PORT}/wol/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connection opened
|
|
||||||
socket.addEventListener('open', function () {
|
|
||||||
status.set("open");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connection closed
|
|
||||||
socket.addEventListener('close', function () {
|
|
||||||
status.set("close");
|
|
||||||
setTimeout(function () {
|
|
||||||
initSocket();
|
|
||||||
}, 3000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connection error
|
|
||||||
socket.addEventListener('error', function () {
|
|
||||||
status.set("error");
|
|
||||||
socket.close()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for messages
|
|
||||||
socket.addEventListener('message', function (event) {
|
|
||||||
message.set(JSON.parse(event.data));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initSocket()
|
|
||||||
|
|
||||||
const sendMessage = (message) => {
|
|
||||||
if (socket.readyState <= 1) {
|
|
||||||
socket.send(JSON.stringify(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
subscribeMsg: message.subscribe,
|
|
||||||
subscribeStatus: status.subscribe,
|
|
||||||
sendMessage
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
:root {
|
|
||||||
--success: #{$success};
|
|
||||||
--warning: #{$warning};
|
|
||||||
--danger: #{$danger};
|
|
||||||
--danger-dark-transparent: #{$danger-dark-transparent};
|
|
||||||
--info: #{$info};
|
|
||||||
--info-dark-transparent: #{$info-dark-transparent};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
17
app/run.sh
17
app/run.sh
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# init django
|
|
||||||
cd /app/backend/ || exit
|
|
||||||
python3 manage.py makemigrations
|
|
||||||
python3 manage.py migrate
|
|
||||||
python3 manage.py collectstatic --noinput
|
|
||||||
python3 manage.py shell < setup.py
|
|
||||||
python3 -m celery -A backend worker &
|
|
||||||
python3 -m celery -A backend beat &
|
|
||||||
python3 -m gunicorn --bind 0.0.0.0:"${BACKEND_PORT}" --workers 4 backend.asgi:application -k uvicorn.workers.UvicornWorker &
|
|
||||||
|
|
||||||
# start frontend
|
|
||||||
cd /app/frontend/ || exit
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
PORT=$FRONTEND_PORT npm start
|
|
||||||
111
backend/controllers/device_controller.go
Normal file
111
backend/controllers/device_controller.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/seriousm4x/UpSnap/models"
|
||||||
|
"github.com/seriousm4x/UpSnap/queries"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDevices(c *fiber.Ctx) error {
|
||||||
|
var devices []models.Device
|
||||||
|
|
||||||
|
if err := queries.GetAllDevices(&devices); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": true,
|
||||||
|
"msg": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||||
|
"error": false,
|
||||||
|
"devices": devices,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateDevice(c *fiber.Ctx) error {
|
||||||
|
var device models.Device
|
||||||
|
|
||||||
|
if err := c.BodyParser(&device); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": true,
|
||||||
|
"msg": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := queries.CreateDevice(&device); err != nil {
|
||||||
|
return c.Status(fiber.StatusConflict).JSON(fiber.Map{
|
||||||
|
"error": true,
|
||||||
|
"msg": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||||
|
"error": false,
|
||||||
|
"msg": "created",
|
||||||
|
"device": device,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func PatchDevice(c *fiber.Ctx) error {
|
||||||
|
var params map[string]interface{}
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": true,
|
||||||
|
"msg": "strconv: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(¶ms); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": true,
|
||||||
|
"msg": "paramsparser: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := queries.PatchDevice(¶ms, id); err != nil {
|
||||||
|
return c.Status(fiber.StatusConflict).JSON(fiber.Map{
|
||||||
|
"error": true,
|
||||||
|
"msg": "patching: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||||
|
"error": false,
|
||||||
|
"msg": "patched",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteDevice(c *fiber.Ctx) error {
|
||||||
|
var device models.Device
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": true,
|
||||||
|
"msg": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := queries.GetOneDevice(&device, id); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": true,
|
||||||
|
"msg": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := queries.DeleteDevice(&device, id); err != nil {
|
||||||
|
return c.Status(fiber.StatusConflict).JSON(fiber.Map{
|
||||||
|
"error": true,
|
||||||
|
"msg": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||||
|
"error": false,
|
||||||
|
"msg": "deleted",
|
||||||
|
})
|
||||||
|
}
|
||||||
31
backend/database/database.go
Normal file
31
backend/database/database.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/seriousm4x/UpSnap/models"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to connect database")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate the schema
|
||||||
|
db.AutoMigrate(&models.Settings{}, &models.Port{}, &models.Device{})
|
||||||
|
|
||||||
|
DB = db
|
||||||
|
}
|
||||||
|
|
||||||
|
func Close() error {
|
||||||
|
db, err := DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("gorm.DB get database: %v", err)
|
||||||
|
}
|
||||||
|
return db.Close()
|
||||||
|
}
|
||||||
28
backend/go.mod
Normal file
28
backend/go.mod
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
module github.com/seriousm4x/UpSnap
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gofiber/fiber/v2 v2.40.1
|
||||||
|
github.com/gofiber/websocket/v2 v2.1.2
|
||||||
|
gorm.io/driver/sqlite v1.3.6
|
||||||
|
gorm.io/gorm v1.23.9
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
|
github.com/fasthttp/websocket v1.5.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/klauspost/compress v1.15.9 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.12 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.41.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
|
||||||
|
)
|
||||||
58
backend/go.sum
Normal file
58
backend/go.sum
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/fasthttp/websocket v1.5.0 h1:B4zbe3xXyvIdnqjOZrafVFklCUq5ZLo/TqCt5JA1wLE=
|
||||||
|
github.com/fasthttp/websocket v1.5.0/go.mod h1:n0BlOQvJdPbTuBkZT0O5+jk/sp/1/VCzquR1BehI2F4=
|
||||||
|
github.com/gofiber/fiber/v2 v2.40.1 h1:pc7n9VVpGIqNsvg9IPLQhyFEMJL8gCs1kneH5D1pIl4=
|
||||||
|
github.com/gofiber/fiber/v2 v2.40.1/go.mod h1:Gko04sLksnHbzLSRBFWPFdzM9Ws9pRxvvIaohJK1dsk=
|
||||||
|
github.com/gofiber/websocket/v2 v2.1.2 h1:EulKyLB/fJgui5+6c8irwEnYQ9FRsrLZfkrq9OfTDGc=
|
||||||
|
github.com/gofiber/websocket/v2 v2.1.2/go.mod h1:S+sKWo0xeC7Wnz5h4/8f6D/NxsrLFIdWDYB3SyVO9pE=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/klauspost/compress v1.14.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
|
||||||
|
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 h1:Orn7s+r1raRTBKLSc9DmbktTT04sL+vkzsbRD2Q8rOI=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.33.0/go.mod h1:KJRK/MXx0J+yd0c5hlR+s1tIHD72sniU8ZJjl97LIw4=
|
||||||
|
github.com/valyala/fasthttp v1.41.0 h1:zeR0Z1my1wDHTRiamBCXVglQdbUwgb9uWG3k1HQz6jY=
|
||||||
|
github.com/valyala/fasthttp v1.41.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
|
||||||
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ=
|
||||||
|
gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE=
|
||||||
|
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
|
gorm.io/gorm v1.23.9 h1:NSHG021i+MCznokeXR3udGaNyFyBQJW8MbjrJMVCfGw=
|
||||||
|
gorm.io/gorm v1.23.9/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||||
53
backend/main.go
Normal file
53
backend/main.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/monitor"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||||
|
"github.com/gofiber/websocket/v2"
|
||||||
|
"github.com/seriousm4x/UpSnap/controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(recover.New())
|
||||||
|
app.Get("/metrics", monitor.New(monitor.Config{Title: "UpSnap Metrics Page"}))
|
||||||
|
|
||||||
|
devicesGroup := app.Group("/devices")
|
||||||
|
devicesGroup.Get("/", controllers.GetDevices)
|
||||||
|
devicesGroup.Post("/", controllers.CreateDevice)
|
||||||
|
devicesGroup.Patch("/:id", controllers.PatchDevice)
|
||||||
|
devicesGroup.Delete("/:id", controllers.DeleteDevice)
|
||||||
|
|
||||||
|
app.Use("/ws", func(c *fiber.Ctx) error {
|
||||||
|
if websocket.IsWebSocketUpgrade(c) {
|
||||||
|
c.Locals("allowed", true)
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
return fiber.ErrUpgradeRequired
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/ws/:id", websocket.New(func(c *websocket.Conn) {
|
||||||
|
var (
|
||||||
|
mt int
|
||||||
|
msg []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
if mt, msg, err = c.ReadMessage(); err != nil {
|
||||||
|
log.Println("read:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Printf("recv: %s", msg)
|
||||||
|
|
||||||
|
if err = c.WriteMessage(mt, msg); err != nil {
|
||||||
|
log.Println("write:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}))
|
||||||
|
log.Fatal(app.Listen(":3000"))
|
||||||
|
}
|
||||||
14
backend/models/device_model.go
Normal file
14
backend/models/device_model.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
type Device struct {
|
||||||
|
gorm.Model
|
||||||
|
Name string `gorm:"not null; size:100" json:"name" binding:"required"`
|
||||||
|
IP string `gorm:"not null" json:"ip" binding:"required"`
|
||||||
|
Mac string `gorm:"not null; size:17" json:"mac" binding:"required"`
|
||||||
|
Netmask string `gorm:"not null; size:15; default:255.255.255.0" json:"netmask" binding:"required"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
Ports []Port `gorm:"many2many:device_ports;" json:"ports"`
|
||||||
|
ShutdownCmd string `json:"shutdown_cmd"`
|
||||||
|
}
|
||||||
9
backend/models/port_model.go
Normal file
9
backend/models/port_model.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
type Port struct {
|
||||||
|
gorm.Model
|
||||||
|
Number uint16 `gorm:"not null" json:"number" binding:"number"`
|
||||||
|
Name string `gorm:"not null" json:"name" binding:"name"`
|
||||||
|
}
|
||||||
11
backend/models/settings_model.go
Normal file
11
backend/models/settings_model.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
gorm.Model
|
||||||
|
SortBy string `gorm:"not null; default:name" json:"sort_by" binding:"required"`
|
||||||
|
ScanAddress string `gorm:"not null" json:"scan_address"`
|
||||||
|
PingInterval uint `gorm:"not null; default:5" json:"ping_interval"`
|
||||||
|
NotificationsEnabled bool `gorm:"not null; default:true" json:"notifications_enabled" binding:"required"`
|
||||||
|
}
|
||||||
51
backend/queries/device_queries.go
Normal file
51
backend/queries/device_queries.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/seriousm4x/UpSnap/database"
|
||||||
|
"github.com/seriousm4x/UpSnap/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAllDevices(d *[]models.Device) error {
|
||||||
|
if result := database.DB.Model(d).Find(d); result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateDevice(d *models.Device) error {
|
||||||
|
if result := database.DB.Model(d).Create(&d); result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOneDevice(d *models.Device, id int) error {
|
||||||
|
result := database.DB.Model(d).Where("id = ?", id).Find(d)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PatchDevice(changes *map[string]interface{}, id int) error {
|
||||||
|
var device models.Device
|
||||||
|
if err := GetOneDevice(&device, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if result := database.DB.Model(device).Where("id = ?", id).Updates(changes); result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteDevice(d *models.Device, id int) error {
|
||||||
|
var device models.Device
|
||||||
|
if result := database.DB.Where("id = ?", id).Delete(&device); result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
app:
|
upsnap_frontend:
|
||||||
container_name: upsnap_app
|
container_name: upsnap_frontend
|
||||||
image: seriousm4x/upsnap:latest
|
build:
|
||||||
|
context: app/frontend/
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PAGE_TITLE=Custom Title # optional, set a custom page title (default: UpSnap)
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
upsnap_backend:
|
||||||
|
container_name: upsnap_backend
|
||||||
|
#image: seriousm4x/upsnap:latest
|
||||||
|
build:
|
||||||
|
context: app/backend/
|
||||||
network_mode: host
|
network_mode: host
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- FRONTEND_PORT=8000
|
|
||||||
- BACKEND_PORT=8001
|
- BACKEND_PORT=8001
|
||||||
- BACKEND_IS_PROXIED=false # set this to true, if you use a reverse proxy
|
- BACKEND_IS_PROXIED=false # set this to true, if you use a reverse proxy
|
||||||
- DB_TYPE=sqlite # required
|
- DB_TYPE=sqlite # required
|
||||||
@@ -20,7 +30,6 @@ services:
|
|||||||
#- DJANGO_LANGUAGE_CODE=de # optional (default: en)
|
#- DJANGO_LANGUAGE_CODE=de # optional (default: en)
|
||||||
#- DJANGO_TIME_ZONE=Europe/Berlin # optional (default: UTC)
|
#- DJANGO_TIME_ZONE=Europe/Berlin # optional (default: UTC)
|
||||||
#- NMAP_ARGS=-sP # optional, set this if your devices need special nmap args so they can be found (default: -sP)
|
#- NMAP_ARGS=-sP # optional, set this if your devices need special nmap args so they can be found (default: -sP)
|
||||||
#- PAGE_TITLE=Custom Title # optional, set a custom page title (default: UpSnap)
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./db/:/app/backend/db/
|
- ./db/:/app/backend/db/
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
13
frontend/.eslintignore
Normal file
13
frontend/.eslintignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
20
frontend/.eslintrc.cjs
Normal file
20
frontend/.eslintrc.cjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||||
|
plugins: ['svelte3', '@typescript-eslint'],
|
||||||
|
ignorePatterns: ['*.cjs'],
|
||||||
|
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||||
|
settings: {
|
||||||
|
'svelte3/typescript': () => require('typescript')
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
}
|
||||||
|
};
|
||||||
10
frontend/.gitignore
vendored
Normal file
10
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
13
frontend/.prettierignore
Normal file
13
frontend/.prettierignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
9
frontend/.prettierrc
Normal file
9
frontend/.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"pluginSearchDirs": ["."],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
||||||
14
frontend/Dockerfile
Normal file
14
frontend/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM node:alpine as build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN ls -lah && npm i && \
|
||||||
|
npm run build && \
|
||||||
|
npm prune --omit=dev
|
||||||
|
|
||||||
|
FROM node:alpine
|
||||||
|
USER node:node
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build --chown=node:node /app/build ./build
|
||||||
|
COPY --from=build --chown=node:node /app/node_modules ./node_modules
|
||||||
|
COPY --chown=node:node package.json .
|
||||||
|
CMD ["node","build"]
|
||||||
38
frontend/README.md
Normal file
38
frontend/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# create-svelte
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npm create svelte@latest
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npm create svelte@latest my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||||
5210
frontend/package-lock.json
generated
Normal file
5210
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
frontend/package.json
Normal file
37
frontend/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
|
"format": "prettier --plugin-search-dir . --write ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-node": "^1.0.0",
|
||||||
|
"@sveltejs/kit": "^1.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
|
"prettier": "^2.8.0",
|
||||||
|
"prettier-plugin-svelte": "^2.8.1",
|
||||||
|
"svelte": "^3.54.0",
|
||||||
|
"svelte-check": "^2.9.2",
|
||||||
|
"tslib": "^2.4.1",
|
||||||
|
"typescript": "^4.9.3",
|
||||||
|
"vite": "^4.0.0"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^6.2.1",
|
||||||
|
"@popperjs/core": "^2.11.6",
|
||||||
|
"bootstrap": "5.2.3",
|
||||||
|
"sass": "^1.57.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1919
frontend/pnpm-lock.yaml
generated
Normal file
1919
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
frontend/src/app.d.ts
vendored
Normal file
9
frontend/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
// and what to do when importing types
|
||||||
|
declare namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
12
frontend/src/app.html
Normal file
12
frontend/src/app.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
58
frontend/src/components/DarkToggle.svelte
Normal file
58
frontend/src/components/DarkToggle.svelte
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script>
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
const preferesDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
preferesDark.addEventListener('change', (e) => {
|
||||||
|
setTheme(e.matches ? 'dark' : 'light');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (localStorage.getItem('data-theme') === null) {
|
||||||
|
setTheme(preferesDark.matches ? 'dark' : 'light');
|
||||||
|
} else {
|
||||||
|
setTheme(localStorage.getItem('data-theme'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTheme(color) {
|
||||||
|
const preferesDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
if (color == 'system') {
|
||||||
|
document.documentElement.setAttribute('data-theme', preferesDark ? 'dark' : 'light');
|
||||||
|
localStorage.setItem('data-theme', preferesDark ? 'dark' : 'light');
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-theme', color);
|
||||||
|
localStorage.setItem('data-theme', color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dropdown">
|
||||||
|
<button
|
||||||
|
class="btn btn-light dropdown-toggle px-3 me-2 py-2"
|
||||||
|
type="button"
|
||||||
|
id="darkModeButton"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-palette me-2" />Theme
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="darkModeButton">
|
||||||
|
<li>
|
||||||
|
<button class="dropdown-item" on:click={() => setTheme('dark')}>
|
||||||
|
<i class="fa-solid fa-moon me-2" />Dark
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button class="dropdown-item" on:click={() => setTheme('light')}>
|
||||||
|
<i class="fa-solid fa-sun me-2" />Light
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button class="dropdown-item" on:click={() => setTheme('system')}>
|
||||||
|
<i class="fa-solid fa-desktop me-2" />System
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
509
frontend/src/components/DeviceCard.svelte
Normal file
509
frontend/src/components/DeviceCard.svelte
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
<script>
|
||||||
|
import socketStore from '@stores/socket';
|
||||||
|
export let device;
|
||||||
|
|
||||||
|
let modalDevice = JSON.parse(JSON.stringify(device));
|
||||||
|
let customPort = {};
|
||||||
|
|
||||||
|
function wake(id) {
|
||||||
|
socketStore.sendMessage({
|
||||||
|
type: 'wake',
|
||||||
|
id: id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shutdown(id) {
|
||||||
|
socketStore.sendMessage({
|
||||||
|
type: 'shutdown',
|
||||||
|
id: id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteDevice() {
|
||||||
|
socketStore.sendMessage({
|
||||||
|
type: 'delete_device',
|
||||||
|
id: modalDevice.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDevice() {
|
||||||
|
device = modalDevice;
|
||||||
|
socketStore.sendMessage({
|
||||||
|
type: 'update_device',
|
||||||
|
data: modalDevice
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePort() {
|
||||||
|
if (!customPort.number) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = modalDevice.ports.findIndex((x) => x.number == customPort.number);
|
||||||
|
if (customPort.name) {
|
||||||
|
// add port
|
||||||
|
if (index === -1) {
|
||||||
|
customPort.checked = true;
|
||||||
|
modalDevice.ports.push(JSON.parse(JSON.stringify(customPort)));
|
||||||
|
} else {
|
||||||
|
customPort.checked = modalDevice.ports[index].checked;
|
||||||
|
modalDevice.ports[index] = JSON.parse(JSON.stringify(customPort));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// delete port
|
||||||
|
if (index >= 0) {
|
||||||
|
modalDevice.ports.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modalDevice = modalDevice;
|
||||||
|
// send to backend
|
||||||
|
socketStore.sendMessage({
|
||||||
|
type: 'update_port',
|
||||||
|
data: customPort
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
modalDevice = Object.assign({}, device);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePort() {
|
||||||
|
if (typeof customPort.number != 'number') {
|
||||||
|
customPort.number = 1;
|
||||||
|
} else if (customPort.number > 65535) {
|
||||||
|
customPort.number = 65535;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="device-col-{device.id}" class="col-xs-12 col-sm-6 col-md-4 col-lg-3 g-4">
|
||||||
|
<div class="card border-0 p-3 pt-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto me-auto">
|
||||||
|
<div id="spinner-{device.id}" class="spinner-border warning d-none" role="status" />
|
||||||
|
{#if device.up === true}
|
||||||
|
{#if device.shutdown.command}
|
||||||
|
<div
|
||||||
|
class="hover"
|
||||||
|
on:click={() => shutdown(device.id)}
|
||||||
|
on:keydown={() => 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" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<i id="dot-{device.id}" class="fa-solid fa-power-off fa-2x success" />
|
||||||
|
{/if}
|
||||||
|
{:else if device.up === false}
|
||||||
|
<div
|
||||||
|
class="hover"
|
||||||
|
on:click={() => wake(device.id)}
|
||||||
|
on:keydown={() => wake(device.id)}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<i id="dot-{device.id}" class="fa-solid fa-power-off fa-2x danger" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="hover" role="button">
|
||||||
|
<i id="dot-{device.id}" class="fa-solid fa-power-off fa-2x text-muted" />
|
||||||
|
</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" />
|
||||||
|
</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" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="col-auto hover"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#device-modal-{device.id}"
|
||||||
|
role="button"
|
||||||
|
on:click={() => openModal()}
|
||||||
|
on:keydown={() => openModal()}
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-ellipsis-vertical fa-2x" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if device.link}
|
||||||
|
<h5 class="card-title fw-bold my-2">
|
||||||
|
<a class="inherit-color" href={device.link}>{device.name}</a>
|
||||||
|
</h5>
|
||||||
|
{:else}
|
||||||
|
<h5 class="card-title fw-bold my-2">{device.name}</h5>
|
||||||
|
{/if}
|
||||||
|
<h6 class="card-subtitle mb-2 text-muted">{device.ip}</h6>
|
||||||
|
<ul class="list-group">
|
||||||
|
{#each device.ports as port}
|
||||||
|
{#if port.checked === true}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<i class="fa-solid fa-circle align-middle {port.open ? 'success' : 'danger'}" />
|
||||||
|
{port.name}
|
||||||
|
<span class="text-muted">({port.number})</span>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
id="device-modal-{device.id}"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="device-modal-{device.id}-label"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold" id="device-modal-{modalDevice.id}-label">
|
||||||
|
{modalDevice.name}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="form-{modalDevice.id}" on:submit|preventDefault={updateDevice}>
|
||||||
|
<!-- general -->
|
||||||
|
<h5 class="fw-bold">General</h5>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm">
|
||||||
|
<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">
|
||||||
|
<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 mb-2">
|
||||||
|
<div class="col-sm">
|
||||||
|
<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}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<label for="inputNetmask{modalDevice.id}" class="form-label">Netmask</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="inputNetmask{modalDevice.id}"
|
||||||
|
bind:value={modalDevice.netmask}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="inputLinkAddDevice" class="form-label">Web link</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="inputILinkAddDevice"
|
||||||
|
placeholder="http://...."
|
||||||
|
bind:value={modalDevice.link}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 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}
|
||||||
|
{#each modalDevice.ports as port}
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="{device.id}-port-{port.number}"
|
||||||
|
bind:checked={port['checked']}
|
||||||
|
/>
|
||||||
|
<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-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 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>
|
||||||
|
<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" />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>
|
||||||
|
<!-- 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="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="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>
|
||||||
|
<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" />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"
|
||||||
|
rel="noreferrer">valid syntax here</a
|
||||||
|
>
|
||||||
|
or
|
||||||
|
<a href="https://crontab.guru/" target="_blank" rel="noreferrer"
|
||||||
|
>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 -o 'StrictHostKeyChecking=no' 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" />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 -o 'StrictHostKeyChecking=no' user@hostname 'sudo shutdown'
|
||||||
|
|
||||||
|
# wake windows hosts
|
||||||
|
net rpc shutdown --ipaddress 192.168.1.1 --user user%password
|
||||||
|
</pre>
|
||||||
|
<p class="mb-0">
|
||||||
|
Read more about <a
|
||||||
|
href="https://linux.die.net/man/1/sshpass"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer">sshpass</a
|
||||||
|
>
|
||||||
|
or
|
||||||
|
<a href="https://linux.die.net/man/8/net" target="_blank" rel="noreferrer"
|
||||||
|
>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>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
on:click={deleteDevice}>Delete</button
|
||||||
|
>
|
||||||
|
<button type="submit" form="form-{modalDevice.id}" class="btn btn-outline-success"
|
||||||
|
>Save changes</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
</style>
|
||||||
396
frontend/src/components/Navbar.svelte
Normal file
396
frontend/src/components/Navbar.svelte
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
<script>
|
||||||
|
import socketStore from '@stores/socket';
|
||||||
|
import DarkToggle from '@components/DarkToggle.svelte';
|
||||||
|
export let visitors;
|
||||||
|
export let settings;
|
||||||
|
const pagetitle = import.meta.env.PAGE_TITLE ? import.meta.env.PAGE_TITLE : 'UpSnap';
|
||||||
|
|
||||||
|
let addDevice = {
|
||||||
|
wake: {
|
||||||
|
enabled: false,
|
||||||
|
cron: ''
|
||||||
|
},
|
||||||
|
shutdown: {
|
||||||
|
enabled: false,
|
||||||
|
cron: '',
|
||||||
|
command: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateDevice(data) {
|
||||||
|
if (Object.keys(data).length < 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socketStore.sendMessage({
|
||||||
|
type: 'update_device',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSettings() {
|
||||||
|
socketStore.sendMessage({
|
||||||
|
type: 'update_settings',
|
||||||
|
data: settings
|
||||||
|
});
|
||||||
|
hideModal('settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanNetwork() {
|
||||||
|
socketStore.sendMessage({
|
||||||
|
type: 'scan_network'
|
||||||
|
});
|
||||||
|
const btnScan = document.querySelector('#btnScan');
|
||||||
|
const btnScanSpinner = document.querySelector('#btnScanSpinner');
|
||||||
|
const btnScanText = document.querySelector('#btnScanText');
|
||||||
|
btnScan.disabled = true;
|
||||||
|
btnScanSpinner.classList.remove('d-none');
|
||||||
|
btnScanText.innerText = 'Scanning...';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addScan(i) {
|
||||||
|
document.querySelector(`#btnAdd${i}`).disabled = true;
|
||||||
|
const dev = settings.scan_network[i];
|
||||||
|
updateDevice(dev);
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreFromFile = (e) => {
|
||||||
|
let file = e.target.files[0];
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.readAsText(file);
|
||||||
|
reader.onload = (e) => {
|
||||||
|
let data = JSON.parse(e.target.result);
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
// v2 file restore
|
||||||
|
data.forEach((device) => {
|
||||||
|
updateDevice(device);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// v1 file restore
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
value['mac'] = key;
|
||||||
|
value['link'] = '';
|
||||||
|
updateDevice(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
hideModal('settings');
|
||||||
|
};
|
||||||
|
|
||||||
|
function backupToFile() {
|
||||||
|
socketStore.sendMessage({
|
||||||
|
type: 'backup'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideModal(id) {
|
||||||
|
const modalEl = document.querySelector(`#${id}`);
|
||||||
|
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{pagetitle}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-sm">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="navbar-brand" href="/">
|
||||||
|
<img src="favicon.png" alt="Logo" width="24" height="24" class="me-2" />
|
||||||
|
{pagetitle}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbarNavAltMarkup"
|
||||||
|
aria-controls="navbarNavAltMarkup"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<span class="navbar-toggler-icon" />
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
|
||||||
|
<span class="ms-auto d-flex">
|
||||||
|
<DarkToggle />
|
||||||
|
<div class="dropdown">
|
||||||
|
<button
|
||||||
|
class="btn btn-light dropdown-toggle px-3 me-2 py-2"
|
||||||
|
type="button"
|
||||||
|
id="dropdownMenuButton1"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-wrench me-2" />More
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
||||||
|
<li>
|
||||||
|
<button class="dropdown-item" data-bs-toggle="modal" data-bs-target="#addDevice">
|
||||||
|
<i class="fa-solid fa-plus me-2" />Add device
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button class="dropdown-item" data-bs-toggle="modal" data-bs-target="#settings">
|
||||||
|
<i class="fa-solid fa-sliders me-2" />Settings
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="btn btn-light px-3 me-2 py-2 pe-none">
|
||||||
|
<i class="me-2 fa-solid {visitors === 1 ? 'fa-user' : 'fa-user-group'}" />{visitors}
|
||||||
|
{visitors === 1 ? 'Visitor' : 'Visitors'}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
id="addDevice"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="addDeviceLabel"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold" id="addDeviceLabel">Add device</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="addForm" on:submit|preventDefault={() => updateDevice(addDevice)}>
|
||||||
|
<div class="row">
|
||||||
|
<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"
|
||||||
|
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"
|
||||||
|
placeholder="aa:aa:aa:aa:aa:aa"
|
||||||
|
bind:value={addDevice.mac}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<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"
|
||||||
|
placeholder="192.168.1.1"
|
||||||
|
bind:value={addDevice.ip}
|
||||||
|
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"
|
||||||
|
placeholder="255.255.255.0"
|
||||||
|
bind:value={addDevice.netmask}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="inputLinkAddDevice" class="form-label">Web link</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="inputILinkAddDevice"
|
||||||
|
placeholder="http://...."
|
||||||
|
bind:value={addDevice.link}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto ms-auto">
|
||||||
|
<button type="submit" form="addForm" class="btn btn-outline-success"
|
||||||
|
>Add device</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="fw-bold">Network discovery</h5>
|
||||||
|
{#if !settings.discovery}
|
||||||
|
<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}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
id="btnScanSpinner"
|
||||||
|
class="spinner-grow spinner-grow-sm d-none"
|
||||||
|
role="status"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span id="btnScanText">Scan</span>
|
||||||
|
</button>
|
||||||
|
{#if settings.scan_network?.length}
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>IP</td>
|
||||||
|
<td>Netmask</td>
|
||||||
|
<td>Mac</td>
|
||||||
|
<td>Add</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each settings.scan_network as device, i}
|
||||||
|
<tr>
|
||||||
|
<td>{device.name}</td>
|
||||||
|
<td>{device.ip}</td>
|
||||||
|
<td>{device.netmask}</td>
|
||||||
|
<td>{device.mac}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="btnAdd{i}"
|
||||||
|
class="btn btn-outline-secondary py-0"
|
||||||
|
on:click={() => addScan(i)}
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-plus fa-sm" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
id="settings"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="settingsLabel"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold" id="settingsLabel">Settings</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="settingsForm" on:submit|preventDefault={updateSettings}>
|
||||||
|
<h5 class="fw-bold">General</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="inputNetworkDiscovery" class="form-label"
|
||||||
|
>Network discovery address</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="inputNetworkDiscovery"
|
||||||
|
placeholder="192.168.1.0/24"
|
||||||
|
bind:value={settings.discovery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="inputIntervalSettings" class="form-label">Interval (seconds)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
id="inputIntervalSettings"
|
||||||
|
min="5"
|
||||||
|
bind:value={settings.interval}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="form-check-label" for="flexCheckDefault">
|
||||||
|
Enable notifications
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
id="flexCheckDefault"
|
||||||
|
bind:checked={settings.notifications}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-auto ms-auto">
|
||||||
|
<button type="submit" form="settingsForm" class="btn btn-outline-success">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="fw-bold">Backup/Restore</h5>
|
||||||
|
<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>
|
||||||
|
<input
|
||||||
|
id="inputRestore"
|
||||||
|
type="file"
|
||||||
|
class="form-control"
|
||||||
|
accept=".json"
|
||||||
|
on:change={(e) => restoreFromFile(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<button type="button" class="btn btn-secondary" on:click={backupToFile}>
|
||||||
|
<i class="fa-solid fa-download me-2" />
|
||||||
|
Export .json
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
</style>
|
||||||
23
frontend/src/components/Toast.svelte
Normal file
23
frontend/src/components/Toast.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script>
|
||||||
|
export let toast;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="position-fixed bottom-0 end-0 p-3 toast-container">
|
||||||
|
<div
|
||||||
|
class="toast fade {toast.show ? 'show' : 'hide'} {toast.color}-bg"
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
aria-atomic="true"
|
||||||
|
>
|
||||||
|
<div class="toast-header">
|
||||||
|
<strong id="toast-title" class="me-auto">{toast.title}</strong>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="toast-body fw-bold">
|
||||||
|
{toast.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
</style>
|
||||||
188
frontend/src/main.scss
Normal file
188
frontend/src/main.scss
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
@import '../node_modules/bootstrap/scss/bootstrap';
|
||||||
|
@import '../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss';
|
||||||
|
@import '../node_modules/@fortawesome/fontawesome-free/scss/regular.scss';
|
||||||
|
@import '../node_modules/@fortawesome/fontawesome-free/scss/solid.scss';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--success: #{$success};
|
||||||
|
--warning: #{$warning};
|
||||||
|
--danger: #{$danger};
|
||||||
|
--danger-dark-transparent: #{$danger-dark-transparent};
|
||||||
|
--info: #{$info};
|
||||||
|
--info-dark-transparent: #{$info-dark-transparent};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: var(--bg-modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
border-bottom: 1px solid var(--color-text);
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
background: var(--svg-close);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn,
|
||||||
|
button {
|
||||||
|
&.btn-light {
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--bg-lighter);
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-outline-success {
|
||||||
|
border-color: $success;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-outline-danger {
|
||||||
|
border-color: $danger;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
&:focus {
|
||||||
|
border-color: inherit;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes on-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 15px rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes off-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 15px rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-line;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 2em;
|
||||||
|
background-color: var(--bg-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover {
|
||||||
|
&:hover {
|
||||||
|
text-shadow: 0px 0px 20px rgb(155, 155, 155);
|
||||||
|
transition: all 0.1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-2x {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-power-off {
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-circle {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inherit-color {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout {
|
||||||
|
padding: 1rem;
|
||||||
|
border-left-width: 0.25rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
|
&.callout-info {
|
||||||
|
background-color: var(--info-dark-transparent);
|
||||||
|
border: 1px solid var(--info-dark-transparent);
|
||||||
|
border-left: 5px solid var(--info);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.callout-danger {
|
||||||
|
background-color: var(--danger-dark-transparent);
|
||||||
|
border: 1px solid var(--danger-dark-transparent);
|
||||||
|
border-left: 5px solid var(--danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
184
frontend/src/routes/+page.svelte
Normal file
184
frontend/src/routes/+page.svelte
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import socketStore from '@stores/socket';
|
||||||
|
import Navbar from '@components/Navbar.svelte';
|
||||||
|
import DeviceCard from '@components/DeviceCard.svelte';
|
||||||
|
import Toast from '@components/Toast.svelte';
|
||||||
|
|
||||||
|
let visitors = 0;
|
||||||
|
let devices = [];
|
||||||
|
let settings = {};
|
||||||
|
let toast = {
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
color: '',
|
||||||
|
show: false
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!browser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socketStore.subscribeStatus((status) => {
|
||||||
|
if (status == 'open') {
|
||||||
|
showToast('Websocket', 'Connected', 'success');
|
||||||
|
} else if (status == 'close') {
|
||||||
|
showToast('Websocket', 'Connection closed. Trying to reconnect ...', 'danger');
|
||||||
|
} else if (status == 'error') {
|
||||||
|
showToast('Websocket', 'Error when connecting to websocket', 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socketStore.subscribeMsg((currentMessage) => {
|
||||||
|
if (currentMessage.type == 'init') {
|
||||||
|
// create devices
|
||||||
|
devices = [...currentMessage.message.devices];
|
||||||
|
devices = devices;
|
||||||
|
devices.sort(compare);
|
||||||
|
settings = currentMessage.message.settings;
|
||||||
|
} else if (currentMessage.type == 'status') {
|
||||||
|
// set device array and sort
|
||||||
|
const index = devices.findIndex((x) => x.id == currentMessage.message.id);
|
||||||
|
if (devices.length === 0 || index === -1) {
|
||||||
|
devices.push(currentMessage.message);
|
||||||
|
devices = devices;
|
||||||
|
} else {
|
||||||
|
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 == 'pending') {
|
||||||
|
// set device pending
|
||||||
|
setPending(currentMessage.message);
|
||||||
|
} else if (currentMessage.type == 'visitor') {
|
||||||
|
// update visitor count
|
||||||
|
visitors = currentMessage.message;
|
||||||
|
} else if (currentMessage.type == 'delete') {
|
||||||
|
// delete device
|
||||||
|
const devCol = document.querySelector(`#device-col-${currentMessage.message}`);
|
||||||
|
devCol.remove();
|
||||||
|
} else if (currentMessage.type == 'scan_network') {
|
||||||
|
// set scanned network devices
|
||||||
|
if (!currentMessage.message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settings['scan_network'] = currentMessage.message;
|
||||||
|
const btnScan = document.querySelector('#btnScan');
|
||||||
|
const btnScanSpinner = document.querySelector('#btnScanSpinner');
|
||||||
|
const btnScanText = document.querySelector('#btnScanText');
|
||||||
|
btnScan.disabled = false;
|
||||||
|
btnScanSpinner.classList.add('d-none');
|
||||||
|
btnScanText.innerText = 'Scan';
|
||||||
|
} else if (currentMessage.type == 'backup') {
|
||||||
|
// download backup file
|
||||||
|
const now = new Date();
|
||||||
|
const fileName = `upsnap_backup_${now.toISOString()}.json`;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
const file = new Blob([JSON.stringify(currentMessage.message)], { type: 'text/plain' });
|
||||||
|
a.href = URL.createObjectURL(file);
|
||||||
|
a.download = fileName;
|
||||||
|
a.click();
|
||||||
|
} else if (currentMessage.type == 'operationStatus') {
|
||||||
|
if (currentMessage.message == 'Success') {
|
||||||
|
showToast(currentMessage.message, 'Changes were saved', 'success');
|
||||||
|
} else if (currentMessage.message == 'Error') {
|
||||||
|
showToast(
|
||||||
|
currentMessage.message,
|
||||||
|
'Error while saving the device. Please check the logs.',
|
||||||
|
'danger'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setUp(device) {
|
||||||
|
const dot = document.querySelector(`#dot-${device.id}`);
|
||||||
|
const spinner = document.querySelector(`#spinner-${device.id}`);
|
||||||
|
if (dot) {
|
||||||
|
if (dot.classList.contains('danger')) {
|
||||||
|
showToast(device.name, 'Device is up!', 'success');
|
||||||
|
}
|
||||||
|
dot.style.animation = 'none';
|
||||||
|
dot.offsetWidth;
|
||||||
|
if (!spinner.classList.contains('d-none')) {
|
||||||
|
spinner.classList.add('d-none');
|
||||||
|
dot.classList.remove('d-none', 'danger');
|
||||||
|
dot.classList.add('success');
|
||||||
|
} else {
|
||||||
|
dot.style.animation = 'on-pulse 1s normal';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDown(device) {
|
||||||
|
const dot = document.querySelector(`#dot-${device.id}`);
|
||||||
|
const spinner = document.querySelector(`#spinner-${device.id}`);
|
||||||
|
if (dot) {
|
||||||
|
if (dot.classList.contains('success')) {
|
||||||
|
showToast(device.name, 'Device is down!', 'danger');
|
||||||
|
}
|
||||||
|
dot.style.animation = 'none';
|
||||||
|
dot.offsetWidth;
|
||||||
|
if (!spinner.classList.contains('d-none')) {
|
||||||
|
spinner.classList.add('d-none');
|
||||||
|
dot.classList.remove('d-none', 'success');
|
||||||
|
dot.classList.add('danger');
|
||||||
|
} else {
|
||||||
|
dot.style.animation = 'off-pulse 1s normal';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPending(id) {
|
||||||
|
const dot = document.querySelector(`#dot-${id}`);
|
||||||
|
const spinner = document.querySelector(`#spinner-${id}`);
|
||||||
|
dot.classList.add('d-none');
|
||||||
|
spinner.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(title, message, color) {
|
||||||
|
if (settings.notifications === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.title = title;
|
||||||
|
toast.message = message;
|
||||||
|
toast.color = color;
|
||||||
|
toast.show = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.show = false;
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(a, b) {
|
||||||
|
if (a.name < b.name) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.name > b.name) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<Navbar {settings} {visitors} />
|
||||||
|
<div class="container mb-3">
|
||||||
|
<div class="row">
|
||||||
|
{#each devices as device}
|
||||||
|
<DeviceCard {device} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Toast {toast} />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// @import "../main.scss";
|
||||||
|
</style>
|
||||||
60
frontend/src/stores/socket.ts
Normal file
60
frontend/src/stores/socket.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
const BACKEND_IS_PROXIED = import.meta.env.VITE_BACKEND_IS_PROXIED;
|
||||||
|
const BACKEND_PORT = import.meta.env.VITE_BACKEND_PORT;
|
||||||
|
|
||||||
|
const status = writable('');
|
||||||
|
const message = writable('');
|
||||||
|
|
||||||
|
let socket: WebSocket;
|
||||||
|
|
||||||
|
function initSocket() {
|
||||||
|
if (BACKEND_IS_PROXIED) {
|
||||||
|
const socketUrl = new URL('ws/wol', window.location.href);
|
||||||
|
socketUrl.protocol = socketUrl.protocol.replace('http', 'ws');
|
||||||
|
socket = new WebSocket(socketUrl);
|
||||||
|
} else {
|
||||||
|
socket = new WebSocket(`ws://${location.hostname}:${BACKEND_PORT}/ws/wol`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection opened
|
||||||
|
socket.addEventListener('open', function () {
|
||||||
|
status.set('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connection closed
|
||||||
|
socket.addEventListener('close', function () {
|
||||||
|
status.set('close');
|
||||||
|
setTimeout(function () {
|
||||||
|
initSocket();
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connection error
|
||||||
|
socket.addEventListener('error', function () {
|
||||||
|
status.set('error');
|
||||||
|
socket.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for messages
|
||||||
|
socket.addEventListener('message', function (event) {
|
||||||
|
message.set(JSON.parse(event.data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
initSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMessage = (message: string | object) => {
|
||||||
|
if (socket.readyState <= 1) {
|
||||||
|
socket.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
subscribeMsg: message.subscribe,
|
||||||
|
subscribeStatus: status.subscribe,
|
||||||
|
sendMessage
|
||||||
|
};
|
||||||
@@ -26,4 +26,4 @@ $toast-box-shadow: none;
|
|||||||
$toast-border-width: 0px;
|
$toast-border-width: 0px;
|
||||||
|
|
||||||
// font awesome
|
// font awesome
|
||||||
$fa-font-path: "webfonts";
|
$fa-font-path: 'webfonts';
|
||||||
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
20
frontend/svelte.config.js
Normal file
20
frontend/svelte.config.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-node';
|
||||||
|
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
adapter: adapter(),
|
||||||
|
alias: {
|
||||||
|
'@components': path.resolve('./src/components'),
|
||||||
|
'@stores': path.resolve('./src/stores')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
3
frontend/tsconfig.json
Normal file
3
frontend/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json"
|
||||||
|
}
|
||||||
8
frontend/vite.config.js
Normal file
8
frontend/vite.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
|
/** @type {import('vite').UserConfig} */
|
||||||
|
const config = {
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
Reference in New Issue
Block a user