switch backend to pocketbase
delete frontend for now. needs to be rewritten with pocketbase sdk
@@ -1,4 +0,0 @@
|
||||
.git
|
||||
app/frontend/node_modules
|
||||
app/frontend/build
|
||||
**/db.sqlite3
|
||||
58
.github/workflows/master.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: CI to Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Prepare tags
|
||||
id: prep
|
||||
run: |
|
||||
DOCKER_IMAGE=${{ secrets.DOCKER_HUB_USERNAME }}/upsnap
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${VERSION:0:2},${DOCKER_IMAGE}:latest"
|
||||
echo ::set-output name=tags::${TAGS}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: true
|
||||
tags: ${{ steps.prep.outputs.tags }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
417
.gitignore
vendored
@@ -1,295 +1,196 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,svelte,node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=go,visualstudiocode,svelte,node
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,python,django
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,python,django
|
||||
|
||||
### Django ###
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
__pycache__/
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
media
|
||||
|
||||
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||
# in your Git repository. Update and uncomment the following line accordingly.
|
||||
# <django-project-name>/staticfiles/
|
||||
|
||||
### Django.Python Stack ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
pytestdebug.log
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Django stuff:
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
doc/_build/
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environments
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
pythonenv*
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# profiling data
|
||||
.prof
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
### Svelte ###
|
||||
# gitignore template for the SvelteKit, frontend web component framework
|
||||
# website: https://kit.svelte.dev/
|
||||
|
||||
.svelte-kit/
|
||||
package
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,svelte,node
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
# C extensions
|
||||
|
||||
# Distribution / packaging
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
|
||||
# Installer logs
|
||||
|
||||
# Unit test / coverage reports
|
||||
|
||||
# Translations
|
||||
|
||||
# Django stuff:
|
||||
|
||||
# Flask stuff:
|
||||
|
||||
# Scrapy stuff:
|
||||
|
||||
# Sphinx documentation
|
||||
|
||||
# PyBuilder
|
||||
|
||||
# Jupyter Notebook
|
||||
|
||||
# IPython
|
||||
|
||||
# pyenv
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
|
||||
# Celery stuff
|
||||
|
||||
# SageMath parsed files
|
||||
|
||||
# Environments
|
||||
|
||||
# Spyder project settings
|
||||
|
||||
# Rope project settings
|
||||
|
||||
# mkdocs documentation
|
||||
|
||||
# mypy
|
||||
|
||||
# Pyre type checker
|
||||
|
||||
# pytype static type analyzer
|
||||
|
||||
# profiling data
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,python,django
|
||||
|
||||
.vscode
|
||||
staticfiles
|
||||
|
||||
# JetBrains WebStorm
|
||||
.idea/
|
||||
|
||||
# custom
|
||||
docker-compose.yml
|
||||
db/
|
||||
*.db
|
||||
backend/pb_data
|
||||
|
||||
61
README.md
@@ -1,59 +1,4 @@
|
||||
<div align="center" width="100%">
|
||||
<img src="app/frontend/public/favicon.png" width="128" />
|
||||
</div>
|
||||
# to do
|
||||
|
||||
<div align="center" width="100%">
|
||||
<h2>UpSnap</h2>
|
||||
<p>A simple wake on lan app written with Svelte, Django, Django-Channels (websockets), Celery, Redis and nmap.</p>
|
||||
<a target="_blank" href="https://github.com/seriousm4x/upsnap"><img src="https://img.shields.io/github/stars/seriousm4x/upsnap" /></a> <a target="_blank" href="https://hub.docker.com/r/seriousm4x/upsnap"><img src="https://img.shields.io/docker/pulls/seriousm4x/upsnap" /></a> <a target="_blank" href="https://hub.docker.com/r/seriousm4x/upsnap"><img src="https://img.shields.io/docker/v/seriousm4x/upsnap/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/seriousm4x/upsnap"><img src="https://img.shields.io/github/last-commit/seriousm4x/upsnap" /></a>
|
||||
</div>
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Dashboard to wake up devices with 1 click
|
||||
* Set timed wake and shutdown events via cron
|
||||
* Add custom ports to devices which will be scanned
|
||||
* Discover devices by scanning network
|
||||
* Notifications on status changes
|
||||
* Devices only get pinged when there are 1 or more visitors
|
||||
* Dark/light or system prefered color scheme
|
||||
* [Docker images](https://hub.docker.com/r/seriousm4x/upsnap) for amd64, arm64, arm/v7
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
| Dark | Light |
|
||||
| -------------------- | --------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## 🐳 Run your own instance
|
||||
|
||||
There are 3 example docker-compose files to choose from. The simplest is [docker-compose-sqlite.yml](docker-compose-sqlite.yml).
|
||||
|
||||
The website will be available at [localhost:8000](http://localhost:8000). If you run it on a different pc, it will be `http://<your-ip>:8000`. You can change the port in the docker-compose file.
|
||||
|
||||
### Reverse Proxy
|
||||
|
||||
If you're using a reverse proxy, make sure to set `BACKEND_IS_PROXIED` to true in docker-compose. Set your reverse proxy to the `FRONTEND_PORT` and set `/wol/` to `BACKEND_PORT`.
|
||||
|
||||
**Caddy example**
|
||||
```
|
||||
upsnap.example.com {
|
||||
reverse_proxy localhost:8000
|
||||
reverse_proxy /wol/ localhost:8001
|
||||
}
|
||||
```
|
||||
|
||||
### Databases
|
||||
|
||||
Upsnap supports 3 different databases. Postgres, MySQL and SQLite. If you already have an existing database you want to use, delete the database container from the compose file. Always make sure to set the correct database type environment variable, e.g. DB_TYPE=mysql
|
||||
|
||||
### Windows
|
||||
|
||||
There is a partly working solution in the [issue here](https://github.com/seriousm4x/UpSnap/issues/20#issuecomment-1142593360). Windows has problems with docker networking mode host, which breaks network scan. Sending wol packages works though.
|
||||
|
||||
## 📝 Other infos
|
||||
|
||||
* The app container needs to run in host network mode to send the wakeonlan command on your local network. Therefore all other containers also need to run in host network mode. I don't like it but there is no way around.
|
||||
- add db import from v2
|
||||
- rewrite sveltekit frontend
|
||||
|
||||
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 44 KiB |
@@ -1,111 +0,0 @@
|
||||
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",
|
||||
})
|
||||
}
|
||||
64
backend/cronjobs/cronjobs.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package cronjobs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/robfig/cron"
|
||||
"github.com/seriousm4x/upsnap/backend/logger"
|
||||
"github.com/seriousm4x/upsnap/backend/networking"
|
||||
)
|
||||
|
||||
var Devices []*models.Record
|
||||
|
||||
func RunCron(app *pocketbase.PocketBase) {
|
||||
var interval time.Duration
|
||||
var err error
|
||||
|
||||
// get ping interval from env var
|
||||
// no env: fallback to 5 seconds
|
||||
// below 1 sec: fallback to 1 second
|
||||
interval_default := 5 * time.Second
|
||||
interval_minimum := 1 * time.Second
|
||||
interval_env := os.Getenv("UPSNAP_INTERVAL")
|
||||
if interval_env == "" {
|
||||
interval = interval_default
|
||||
} else {
|
||||
interval, err = time.ParseDuration(interval_env)
|
||||
if err != nil {
|
||||
logger.Error.Println(err)
|
||||
interval = interval_default
|
||||
}
|
||||
if interval < interval_minimum {
|
||||
logger.Warning.Printf("Ping interval below %s is not recommended", interval_minimum)
|
||||
interval = interval_minimum
|
||||
}
|
||||
}
|
||||
logger.Debug.Println("Ping interval set to", interval)
|
||||
|
||||
// init cronjob
|
||||
c := cron.New()
|
||||
c.AddFunc(fmt.Sprintf("@every %s", interval), func() {
|
||||
for _, device := range Devices {
|
||||
go func(device *models.Record) {
|
||||
oldStatus := device.Get("status")
|
||||
newStatus := networking.PingDevice(device)
|
||||
if newStatus {
|
||||
if oldStatus == "offline" || oldStatus == "" {
|
||||
device.Set("status", "online")
|
||||
app.Dao().SaveRecord(device)
|
||||
}
|
||||
} else {
|
||||
if oldStatus == "online" || oldStatus == "" {
|
||||
device.Set("status", "offline")
|
||||
app.Dao().SaveRecord(device)
|
||||
}
|
||||
}
|
||||
}(device)
|
||||
}
|
||||
})
|
||||
c.Run()
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,28 +1,91 @@
|
||||
module github.com/seriousm4x/UpSnap
|
||||
module github.com/seriousm4x/upsnap/backend
|
||||
|
||||
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
|
||||
github.com/go-ping/ping v1.1.0
|
||||
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
|
||||
github.com/pocketbase/dbx v1.8.0
|
||||
github.com/pocketbase/pocketbase v0.11.2
|
||||
github.com/robfig/cron v1.2.0
|
||||
)
|
||||
|
||||
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/AlecAivazis/survey/v2 v2.3.6 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.178 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.47 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.0 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/domodwyer/mailyak/v3 v3.3.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.3.0 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/wire v0.5.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // 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/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.6.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // 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
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
gocloud.dev v0.28.0 // indirect
|
||||
golang.org/x/crypto v0.5.0 // indirect
|
||||
golang.org/x/image v0.3.0 // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
golang.org/x/net v0.5.0 // indirect
|
||||
golang.org/x/oauth2 v0.4.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/term v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.5.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.106.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||
google.golang.org/grpc v1.52.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.2 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/sqlite v1.20.2 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
||||
2828
backend/go.sum
30
backend/logger/logger.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
flags = log.Ldate | log.Ltime | log.Lshortfile
|
||||
)
|
||||
|
||||
var (
|
||||
Info = log.New(os.Stdout, "[INFO] ", flags)
|
||||
Debug = log.New(os.Stdout, "[DEBUG] ", flags)
|
||||
Warning = log.New(os.Stdout, "[WARNING] ", flags)
|
||||
Error = log.New(os.Stderr, "[ERROR] ", flags)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// TODO: decide if you want to log to a file to max
|
||||
// output := io.MultiWriter(os.Stdout, logFile)
|
||||
output := os.Stdout
|
||||
Info.SetOutput(output)
|
||||
Error.SetOutput(output)
|
||||
Debug.SetOutput(output)
|
||||
|
||||
log.SetOutput(Debug.Writer())
|
||||
log.SetPrefix("[DEBUG]")
|
||||
log.SetFlags(flags)
|
||||
}
|
||||
@@ -1,53 +1,9 @@
|
||||
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"
|
||||
"github.com/seriousm4x/upsnap/backend/pb"
|
||||
)
|
||||
|
||||
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"))
|
||||
pb.StartPocketBase()
|
||||
}
|
||||
|
||||
214
backend/migrations/1673733059_collections_snapshot.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
jsonData := `[
|
||||
{
|
||||
"id": "z5lghx2r3tm45n1",
|
||||
"created": "2023-01-14 21:50:42.797Z",
|
||||
"updated": "2023-01-14 21:50:42.797Z",
|
||||
"name": "devices",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "tiqcmnjo",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "1si6ajha",
|
||||
"name": "ip",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "fyqmpon6",
|
||||
"name": "mac",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "gdctb8hj",
|
||||
"name": "link",
|
||||
"type": "url",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"exceptDomains": null,
|
||||
"onlyDomains": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "ilrwvlev",
|
||||
"name": "port",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": null,
|
||||
"collectionId": "cti4l8f4mz8df3r",
|
||||
"cascadeDelete": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "qqvyfrex",
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "1a7yrwo9",
|
||||
"name": "shutdown_cmd",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "s8c5z7n0",
|
||||
"name": "netmask",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "cti4l8f4mz8df3r",
|
||||
"created": "2023-01-14 21:50:42.797Z",
|
||||
"updated": "2023-01-14 21:50:42.797Z",
|
||||
"name": "ports",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "8nwuncgg",
|
||||
"name": "number",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": 65535
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "o0he3pu6",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "nmj3ko20gzkg8n3",
|
||||
"created": "2023-01-14 21:50:42.797Z",
|
||||
"updated": "2023-01-14 21:50:42.797Z",
|
||||
"name": "settings",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "ysutxavs",
|
||||
"name": "interval",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": 1,
|
||||
"max": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
}
|
||||
]`
|
||||
|
||||
collections := []*models.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return daos.New(db).ImportCollections(collections, true, nil)
|
||||
}, func(db dbx.Builder) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
45
backend/networking/magicpacket.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package networking
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SendMagicPacket(mac, netmask string) error {
|
||||
macAddr, err := hex.DecodeString(strings.ReplaceAll(mac, ":", ""))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding mac address: %v", err)
|
||||
}
|
||||
|
||||
// Create an address to listen on
|
||||
address := net.UDPAddr{
|
||||
IP: net.ParseIP(netmask),
|
||||
Port: 7,
|
||||
}
|
||||
|
||||
// create a new UDP packet conn
|
||||
conn, err := net.DialUDP("udp", nil, &address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating UDP packet conn: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// create a magic packet
|
||||
var magicPacket bytes.Buffer
|
||||
for i := 0; i < 6; i++ {
|
||||
magicPacket.WriteByte(0xff)
|
||||
}
|
||||
for i := 0; i < 16; i++ {
|
||||
magicPacket.Write(macAddr)
|
||||
}
|
||||
|
||||
// send the magic packet
|
||||
_, err = conn.Write(magicPacket.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending magic packet: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
28
backend/networking/ping.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package networking
|
||||
|
||||
import (
|
||||
"github.com/go-ping/ping"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/seriousm4x/upsnap/backend/logger"
|
||||
)
|
||||
|
||||
func PingDevice(device *models.Record) bool {
|
||||
pinger, err := ping.NewPinger(device.GetString("ip"))
|
||||
if err != nil {
|
||||
logger.Error.Println(err)
|
||||
return false
|
||||
}
|
||||
pinger.Count = 1
|
||||
pinger.Timeout = 500 * 1000000 // 500ms
|
||||
err = pinger.Run()
|
||||
if err != nil {
|
||||
logger.Error.Println(err)
|
||||
return false
|
||||
}
|
||||
stats := pinger.Statistics()
|
||||
if stats.PacketLoss > 0 {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
72
backend/networking/scan.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package networking
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
)
|
||||
|
||||
type Nmaprun struct {
|
||||
Host []struct {
|
||||
Address []struct {
|
||||
Addr string `xml:"addr,attr" binding:"required"`
|
||||
Addrtype string `xml:"addrtype,attr" binding:"required"`
|
||||
Vendor string `xml:"vendor,attr"`
|
||||
} `xml:"address"`
|
||||
} `xml:"host"`
|
||||
}
|
||||
|
||||
func ScanNetwork(c echo.Context) error {
|
||||
scanRange := os.Getenv("UPSNAP_SCAN_RANGE")
|
||||
_, _, err := net.ParseCIDR(scanRange)
|
||||
if err != nil {
|
||||
return apis.NewBadRequestError(err.Error(), nil)
|
||||
}
|
||||
|
||||
cmdOutput, err := exec.Command(
|
||||
"sudo", "nmap", "-sP", "-oX", "-", scanRange,
|
||||
"--host-timeout", "500ms").Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nmapOutput := Nmaprun{}
|
||||
if err := xml.Unmarshal(cmdOutput, &nmapOutput); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
MAC string `json:"mac"`
|
||||
}
|
||||
data := []Device{}
|
||||
|
||||
for _, host := range nmapOutput.Host {
|
||||
dev := Device{}
|
||||
for _, addr := range host.Address {
|
||||
if addr.Addrtype == "ipv4" {
|
||||
dev.IP = addr.Addr
|
||||
} else if addr.Addrtype == "mac" {
|
||||
dev.MAC = addr.Addr
|
||||
}
|
||||
if addr.Vendor != "" {
|
||||
dev.Name = addr.Vendor
|
||||
}
|
||||
}
|
||||
if dev.IP == "" || dev.MAC == "" {
|
||||
continue
|
||||
}
|
||||
if dev.Name == "" {
|
||||
dev.Name = "Unknown"
|
||||
}
|
||||
data = append(data, dev)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, data)
|
||||
}
|
||||
26
backend/networking/wake.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package networking
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/seriousm4x/upsnap/backend/logger"
|
||||
)
|
||||
|
||||
func WakeDevice(device *models.Record) bool {
|
||||
err := SendMagicPacket(device.GetString("mac"), device.GetString("netmask"))
|
||||
if err != nil {
|
||||
logger.Error.Println(err)
|
||||
return false
|
||||
}
|
||||
|
||||
// we wait 1 minute for the device to come up
|
||||
// after that, we check the state
|
||||
time.Sleep(1 * time.Minute)
|
||||
isOnline := PingDevice(device)
|
||||
if isOnline {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
134
backend/pb/pb.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package pb
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||
"github.com/seriousm4x/upsnap/backend/cronjobs"
|
||||
"github.com/seriousm4x/upsnap/backend/logger"
|
||||
_ "github.com/seriousm4x/upsnap/backend/migrations"
|
||||
"github.com/seriousm4x/upsnap/backend/networking"
|
||||
)
|
||||
|
||||
var app *pocketbase.PocketBase
|
||||
|
||||
func StartPocketBase() {
|
||||
app = pocketbase.New()
|
||||
|
||||
// auto migrate db
|
||||
migratecmd.MustRegister(app, app.RootCmd, &migratecmd.Options{
|
||||
Automigrate: true,
|
||||
})
|
||||
|
||||
// event hooks
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
// set static website path
|
||||
e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public"), true))
|
||||
|
||||
// add wake route to api
|
||||
e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/upsnap/wake/:id",
|
||||
Handler: func(c echo.Context) error {
|
||||
record, err := app.Dao().FindFirstRecordByData("devices", "id", c.PathParam("id"))
|
||||
if err != nil {
|
||||
return apis.NewNotFoundError("The device does not exist.", err)
|
||||
}
|
||||
go func(*models.Record) {
|
||||
record.Set("status", "pending")
|
||||
app.Dao().SaveRecord(record)
|
||||
isOnline := networking.WakeDevice(record)
|
||||
if isOnline {
|
||||
record.Set("status", "online")
|
||||
} else {
|
||||
record.Set("status", "offline")
|
||||
}
|
||||
app.Dao().SaveRecord(record)
|
||||
}(record)
|
||||
return c.JSON(http.StatusOK, record)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
|
||||
// add shutdown route to api
|
||||
e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/upsnap/shutdown/:id",
|
||||
Handler: func(c echo.Context) error {
|
||||
record, err := app.Dao().FindFirstRecordByData("devices", "id", c.PathParam("id"))
|
||||
if err != nil {
|
||||
return apis.NewNotFoundError("The device does not exist.", err)
|
||||
}
|
||||
shutdown_cmd := record.GetString("shutdown_cmd")
|
||||
if shutdown_cmd != "" {
|
||||
cmd := exec.Command(shutdown_cmd)
|
||||
if err := cmd.Run(); err != nil {
|
||||
logger.Error.Println(err)
|
||||
return apis.NewBadRequestError(err.Error(), record)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
// add shutdown route to api
|
||||
e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/upsnap/scan",
|
||||
Handler: networking.ScanNetwork,
|
||||
})
|
||||
|
||||
// reset device states and run ping cronjob
|
||||
devices, err := app.Dao().FindRecordsByExpr("devices")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, device := range devices {
|
||||
device.Set("status", "offline")
|
||||
if err := app.Dao().SaveRecord(device); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cronjobs.Devices = devices
|
||||
go cronjobs.RunCron(app)
|
||||
return nil
|
||||
})
|
||||
|
||||
// refresh the device list on database events
|
||||
app.OnModelAfterCreate().Add(func(e *core.ModelEvent) error {
|
||||
refreshDeviceList()
|
||||
return nil
|
||||
})
|
||||
app.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error {
|
||||
refreshDeviceList()
|
||||
return nil
|
||||
})
|
||||
app.OnModelAfterDelete().Add(func(e *core.ModelEvent) error {
|
||||
refreshDeviceList()
|
||||
return nil
|
||||
})
|
||||
|
||||
// start pocketbase
|
||||
if err := app.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func refreshDeviceList() {
|
||||
var err error
|
||||
cronjobs.Devices, err = app.Dao().FindRecordsByExpr("devices")
|
||||
if err != nil {
|
||||
logger.Error.Println(err)
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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,63 +0,0 @@
|
||||
version: "3"
|
||||
services:
|
||||
app:
|
||||
container_name: upsnap_app
|
||||
image: seriousm4x/upsnap:latest
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- FRONTEND_PORT=8000
|
||||
- BACKEND_PORT=8001
|
||||
- BACKEND_IS_PROXIED=false # set this to true, if you use a reverse proxy
|
||||
- DB_TYPE=mysql # required
|
||||
- REDIS_HOST=127.0.0.1 # required (make sure to use the same ip as below)
|
||||
- REDIS_PORT=6379 # required (make sure to use the same port as below)
|
||||
- DB_HOST=127.0.0.1
|
||||
- DB_PORT=3306
|
||||
- DB_NAME=upsnap
|
||||
- DB_USER=upsnap
|
||||
- DB_PASSWORD=upsnap
|
||||
#- PING_INTERVAL=5 # optional (default: 5 seconds)
|
||||
#- DJANGO_SUPERUSER_USER=admin # optional (default: backend login disabled)
|
||||
#- DJANGO_SUPERUSER_PASSWORD=admin # optional (default: backend login disabled)
|
||||
#- DJANGO_SECRET_KEY=secret # optional (default: randomly generated)
|
||||
#- DJANGO_DEBUG=True # optional (default: False)
|
||||
#- DJANGO_LANGUAGE_CODE=de # optional (default: en)
|
||||
#- 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)
|
||||
#- PAGE_TITLE=Custom Title # optional, set a custom page title (default: UpSnap)
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
container_name: upsnap_redis
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
restart: unless-stopped
|
||||
command: redis-server --loglevel warning
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 10s
|
||||
mysql:
|
||||
container_name: upsnap_mysql
|
||||
image: mariadb
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- MYSQL_DATABASE=upsnap
|
||||
- MYSQL_USER=upsnap
|
||||
- MYSQL_PASSWORD=upsnap
|
||||
- MYSQL_ROOT_PASSWORD=upsnap
|
||||
cap_add:
|
||||
- SYS_NICE
|
||||
volumes:
|
||||
- upsnap_db:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: mysqladmin ping -h localhost -u $$MYSQL_USER -p$$MYSQL_PASSWORD
|
||||
interval: 10s
|
||||
volumes:
|
||||
upsnap_db:
|
||||
@@ -1,60 +0,0 @@
|
||||
version: "3"
|
||||
services:
|
||||
app:
|
||||
container_name: upsnap_app
|
||||
image: seriousm4x/upsnap:latest
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- FRONTEND_PORT=8000
|
||||
- BACKEND_PORT=8001
|
||||
- BACKEND_IS_PROXIED=false # set this to true, if you use a reverse proxy
|
||||
- DB_TYPE=postgres # required
|
||||
- REDIS_HOST=127.0.0.1 # required (make sure to use the same ip as below)
|
||||
- REDIS_PORT=6379 # required (make sure to use the same port as below)
|
||||
- DB_HOST=127.0.0.1
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=upsnap
|
||||
- DB_USER=upsnap
|
||||
- DB_PASSWORD=upsnap
|
||||
#- PING_INTERVAL=5 # optional (default: 5 seconds)
|
||||
#- DJANGO_SUPERUSER_USER=admin # optional (default: backend login disabled)
|
||||
#- DJANGO_SUPERUSER_PASSWORD=admin # optional (default: backend login disabled)
|
||||
#- DJANGO_SECRET_KEY=secret # optional (default: randomly generated)
|
||||
#- DJANGO_DEBUG=True # optional (default: False)
|
||||
#- DJANGO_LANGUAGE_CODE=de # optional (default: en)
|
||||
#- 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)
|
||||
#- PAGE_TITLE=Custom Title # optional, set a custom page title (default: UpSnap)
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
container_name: upsnap_redis
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
restart: unless-stopped
|
||||
command: redis-server --loglevel warning
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 10s
|
||||
postgres:
|
||||
container_name: upsnap_postgres
|
||||
image: postgres:14-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- "POSTGRES_USER=upsnap"
|
||||
- "POSTGRES_PASSWORD=upsnap"
|
||||
- "POSTGRES_DB=upsnap"
|
||||
healthcheck:
|
||||
test: pg_isready -U upsnap
|
||||
interval: 10s
|
||||
volumes:
|
||||
- upsnap_db:/var/lib/postgresql/data
|
||||
volumes:
|
||||
upsnap_db:
|
||||
@@ -1,47 +0,0 @@
|
||||
version: "3"
|
||||
services:
|
||||
upsnap_frontend:
|
||||
container_name: upsnap_frontend
|
||||
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
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- BACKEND_PORT=8001
|
||||
- BACKEND_IS_PROXIED=false # set this to true, if you use a reverse proxy
|
||||
- DB_TYPE=sqlite # required
|
||||
- REDIS_HOST=127.0.0.1 # required (make sure to use the same ip as below)
|
||||
- REDIS_PORT=6379 # required (make sure to use the same port as below)
|
||||
#- PING_INTERVAL=5 # optional (default: 5 seconds)
|
||||
#- DJANGO_SUPERUSER_USER=admin # optional (default: backend login disabled)
|
||||
#- DJANGO_SUPERUSER_PASSWORD=admin # optional (default: backend login disabled)
|
||||
#- DJANGO_SECRET_KEY=secret # optional (default: randomly generated)
|
||||
#- DJANGO_DEBUG=True # optional (default: False)
|
||||
#- DJANGO_LANGUAGE_CODE=de # optional (default: en)
|
||||
#- 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)
|
||||
volumes:
|
||||
- ./db/:/app/backend/db/
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
container_name: upsnap_redis
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
restart: unless-stopped
|
||||
command: redis-server --loglevel warning
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 10s
|
||||
@@ -1,13 +0,0 @@
|
||||
.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
|
||||
@@ -1,20 +0,0 @@
|
||||
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
@@ -1,10 +0,0 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
@@ -1 +0,0 @@
|
||||
engine-strict=true
|
||||
@@ -1,13 +0,0 @@
|
||||
.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
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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"]
|
||||
@@ -1,38 +0,0 @@
|
||||
# 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
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"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
9
frontend/src/app.d.ts
vendored
@@ -1,9 +0,0 @@
|
||||
// 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 {}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,58 +0,0 @@
|
||||
<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>
|
||||
@@ -1,509 +0,0 @@
|
||||
<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>
|
||||
@@ -1,396 +0,0 @@
|
||||
<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>
|
||||
@@ -1,23 +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">
|
||||
</style>
|
||||
@@ -1,188 +0,0 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
<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>
|
||||
@@ -1,60 +0,0 @@
|
||||
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
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
// colors
|
||||
$light: #f6f6fb;
|
||||
$light-darker: #e2e4eb;
|
||||
$success: #06d6a0;
|
||||
$danger: #ef476f;
|
||||
$danger-dark-transparent: #ef476f1f;
|
||||
$warning: #ffd166;
|
||||
$info: #1177b2;
|
||||
$info-dark-transparent: #1177b21f;
|
||||
$dark: #131316;
|
||||
$dark-lighter: #25252b;
|
||||
|
||||
// bootstrap
|
||||
$list-group-border-width: 0px;
|
||||
$list-group-item-padding-x: 0px;
|
||||
$list-group-item-padding-y: 0px;
|
||||
$list-group-bg: transparent;
|
||||
$modal-content-border-radius: 2em;
|
||||
$modal-inner-padding: 1.5rem;
|
||||
$btn-border-radius: 0.7rem;
|
||||
$dropdown-border-radius: 1em;
|
||||
$dropdown-border-width: 0px;
|
||||
$dropdown-min-width: 8rem;
|
||||
$toast-border-radius: 0.7rem;
|
||||
$toast-box-shadow: none;
|
||||
$toast-border-width: 0px;
|
||||
|
||||
// font awesome
|
||||
$fa-font-path: 'webfonts';
|
||||
|
Before Width: | Height: | Size: 8.5 KiB |
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
plugins: [sveltekit()]
|
||||
};
|
||||
|
||||
export default config;
|
||||