mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-07-29 08:52:51 -04:00
frontend: add pre-commit code formatting
This commit is contained in:
@@ -4,10 +4,11 @@ node_modules
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
.env.\*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['eslint:recommended', 'prettier'],
|
||||
plugins: ['svelte3'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
root: true,
|
||||
extends: ['eslint:recommended', 'prettier'],
|
||||
plugins: ['svelte3'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
||||
|
||||
4
frontend/.husky/pre-commit
Normal file
4
frontend/.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
"useTabs": false,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^1.0.5",
|
||||
"@sveltejs/kit": "^1.2.2",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"vite": "^4.0.4"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"bootstrap": "5.3.0-alpha1",
|
||||
"date-fns": "^2.29.3",
|
||||
"pocketbase": "^0.10.1",
|
||||
"sass": "^1.57.1",
|
||||
"svelte-fa": "^3.0.3"
|
||||
}
|
||||
"name": "frontend",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "cd .. && husky install frontend/.husky",
|
||||
"lint": "prettier --ignore-path ../.gitignore --check --plugin-search-dir=. . && eslint --fix .",
|
||||
"format": "prettier --ignore-path ../.gitignore --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^1.0.5",
|
||||
"@sveltejs/kit": "^1.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-svelte3": "^3.4.1",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.1.0",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.4"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"bootstrap": "5.3.0-alpha1",
|
||||
"date-fns": "^2.29.3",
|
||||
"pocketbase": "^0.10.1",
|
||||
"sass": "^1.57.1",
|
||||
"svelte-fa": "^3.0.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,svelte}": "eslint --fix --ignore-path .gitignore frontend/",
|
||||
"*.{js,css,md,svelte,scss}": "prettier --write --ignore-path .gitignore --plugin-search-dir=frontend/ frontend/"
|
||||
}
|
||||
}
|
||||
|
||||
4587
frontend/pnpm-lock.yaml
generated
4587
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-100">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>UpSnap</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" class="h-100">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>UpSnap</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" class="h-100">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,96 +1,107 @@
|
||||
<script>
|
||||
import { dev } from '$app/environment';
|
||||
import { parseISO, formatDistance } from 'date-fns';
|
||||
import { faPowerOff, faEllipsisVertical, faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
import { dev } from '$app/environment';
|
||||
import { parseISO, formatDistance } from 'date-fns';
|
||||
import { faPowerOff, faEllipsisVertical, faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
|
||||
export let device;
|
||||
export let now;
|
||||
export let device;
|
||||
export let now;
|
||||
|
||||
function shutdown() {
|
||||
fetch(`${dev ? 'http://127.0.0.1:8090' : ''}/api/upsnap/shutdown/${device.id}`);
|
||||
}
|
||||
function shutdown() {
|
||||
fetch(`${dev ? 'http://127.0.0.1:8090' : ''}/api/upsnap/shutdown/${device.id}`);
|
||||
}
|
||||
|
||||
function wake() {
|
||||
fetch(`${dev ? 'http://127.0.0.1:8090' : ''}/api/upsnap/wake/${device.id}`);
|
||||
}
|
||||
function wake() {
|
||||
fetch(`${dev ? 'http://127.0.0.1:8090' : ''}/api/upsnap/wake/${device.id}`);
|
||||
}
|
||||
|
||||
function sortPorts(a, b) {
|
||||
return a.number - b.number;
|
||||
}
|
||||
function sortPorts(a, b) {
|
||||
return a.number - b.number;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 g-4">
|
||||
<div
|
||||
class="card border-0 rounded-5 px-3 py-2 shadow-sm"
|
||||
class:offline={device.status == 'offline' ? true : false}
|
||||
class:online={device.status == 'online' ? true : false}
|
||||
class:pending={device.status == 'pending' ? true : false}
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-auto me-auto">
|
||||
{#if device.status === 'offline'}
|
||||
<span class="text-danger" on:click={() => wake()} on:keydown={() => wake()}>
|
||||
<div role="button">
|
||||
<Fa icon={faPowerOff} class="fs-4 power-hover" />
|
||||
</div>
|
||||
</span>
|
||||
{:else if device.status === 'online'}
|
||||
{#if device.shutdown_cmd !== ''}
|
||||
<span class="text-success" on:click={() => shutdown()} on:keydown={() => shutdown()}>
|
||||
<div role="button">
|
||||
<Fa icon={faPowerOff} class="fs-4 power-hover" />
|
||||
</div>
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-success">
|
||||
<div>
|
||||
<Fa icon={faPowerOff} class="fs-4" />
|
||||
</div>
|
||||
</span>
|
||||
{/if}
|
||||
{:else if device.status === 'pending'}
|
||||
<div
|
||||
class="spinner-border text-warning"
|
||||
style="width: 23px;height: 23px;"
|
||||
role="status"
|
||||
>
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-auto fs-5">
|
||||
<a href="/device/{device.id}" class="text-reset text-center ellipsis power-hover">
|
||||
<Fa icon={faEllipsisVertical} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{#if device.link}
|
||||
<p class="m-0 fw-bold fs-5">
|
||||
<a class="text-reset" target="_blank" rel="noreferrer" href={device.link}>{device.name}</a
|
||||
>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="m-0 fw-bold fs-5">{device.name}</p>
|
||||
{/if}
|
||||
<p class="text-muted mb-2">{device.ip}</p>
|
||||
{#if device?.expand?.ports}
|
||||
<div class="mb-2">
|
||||
{#each device.expand.ports.sort(sortPorts) as port}
|
||||
<p class="m-0">
|
||||
<Fa icon={faCircle} class="me-1 {port.status ? 'port-up' : 'port-down'}" />{port.name}
|
||||
<span class="text-muted">({port.number})</span>
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<p class="text-muted m-0">
|
||||
{formatDistance(parseISO(device.updated), now, {
|
||||
includeSeconds: true,
|
||||
addSuffix: true
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="card border-0 rounded-5 px-3 py-2 shadow-sm"
|
||||
class:offline={device.status == 'offline' ? true : false}
|
||||
class:online={device.status == 'online' ? true : false}
|
||||
class:pending={device.status == 'pending' ? true : false}
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-auto me-auto">
|
||||
{#if device.status === 'offline'}
|
||||
<span class="text-danger" on:click={() => wake()} on:keydown={() => wake()}>
|
||||
<div role="button">
|
||||
<Fa icon={faPowerOff} class="fs-4 power-hover" />
|
||||
</div>
|
||||
</span>
|
||||
{:else if device.status === 'online'}
|
||||
{#if device.shutdown_cmd !== ''}
|
||||
<span
|
||||
class="text-success"
|
||||
on:click={() => shutdown()}
|
||||
on:keydown={() => shutdown()}
|
||||
>
|
||||
<div role="button">
|
||||
<Fa icon={faPowerOff} class="fs-4 power-hover" />
|
||||
</div>
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-success">
|
||||
<div>
|
||||
<Fa icon={faPowerOff} class="fs-4" />
|
||||
</div>
|
||||
</span>
|
||||
{/if}
|
||||
{:else if device.status === 'pending'}
|
||||
<div
|
||||
class="spinner-border text-warning"
|
||||
style="width: 23px;height: 23px;"
|
||||
role="status"
|
||||
>
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-auto fs-5">
|
||||
<a
|
||||
href="/device/{device.id}"
|
||||
class="text-reset text-center ellipsis power-hover"
|
||||
>
|
||||
<Fa icon={faEllipsisVertical} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{#if device.link}
|
||||
<p class="m-0 fw-bold fs-5">
|
||||
<a class="text-reset" target="_blank" rel="noreferrer" href={device.link}
|
||||
>{device.name}</a
|
||||
>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="m-0 fw-bold fs-5">{device.name}</p>
|
||||
{/if}
|
||||
<p class="text-muted mb-2">{device.ip}</p>
|
||||
{#if device?.expand?.ports}
|
||||
<div class="mb-2">
|
||||
{#each device.expand.ports.sort(sortPorts) as port}
|
||||
<p class="m-0">
|
||||
<Fa
|
||||
icon={faCircle}
|
||||
class="me-1 {port.status ? 'port-up' : 'port-down'}"
|
||||
/>{port.name}
|
||||
<span class="text-muted">({port.number})</span>
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<p class="text-muted m-0">
|
||||
{formatDistance(parseISO(device.updated), now, {
|
||||
includeSeconds: true,
|
||||
addSuffix: true
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,350 +1,354 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
import { faEye, faEyeSlash, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
import { faEye, faEyeSlash, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
|
||||
export let device;
|
||||
export let mode;
|
||||
export let device;
|
||||
export let mode;
|
||||
|
||||
let pb;
|
||||
let timeout;
|
||||
let button = {
|
||||
state: 'none',
|
||||
error: ''
|
||||
};
|
||||
let deleteButton = {
|
||||
state: 'none',
|
||||
error: ''
|
||||
};
|
||||
let newPort = {
|
||||
name: '',
|
||||
number: null
|
||||
};
|
||||
let passwordShow = false;
|
||||
$: passwordType = passwordShow ? 'text' : 'password';
|
||||
let pb;
|
||||
let timeout;
|
||||
let button = {
|
||||
state: 'none',
|
||||
error: ''
|
||||
};
|
||||
let deleteButton = {
|
||||
state: 'none',
|
||||
error: ''
|
||||
};
|
||||
let newPort = {
|
||||
name: '',
|
||||
number: null
|
||||
};
|
||||
let passwordShow = false;
|
||||
$: passwordType = passwordShow ? 'text' : 'password';
|
||||
|
||||
onMount(async () => {
|
||||
pocketbase.subscribe((conn) => {
|
||||
pb = conn;
|
||||
});
|
||||
});
|
||||
onMount(async () => {
|
||||
pocketbase.subscribe((conn) => {
|
||||
pb = conn;
|
||||
});
|
||||
});
|
||||
|
||||
async function addOrUpdateDevice() {
|
||||
button.state = 'waiting';
|
||||
try {
|
||||
// validate password length
|
||||
if (
|
||||
device.password.length != 0 &&
|
||||
device.password.length != 4 &&
|
||||
device.password.length != 6
|
||||
) {
|
||||
throw 'Password must be 0, 4 or 6 characters long';
|
||||
}
|
||||
async function addOrUpdateDevice() {
|
||||
button.state = 'waiting';
|
||||
try {
|
||||
// validate password length
|
||||
if (
|
||||
device.password.length != 0 &&
|
||||
device.password.length != 4 &&
|
||||
device.password.length != 6
|
||||
) {
|
||||
throw 'Password must be 0, 4 or 6 characters long';
|
||||
}
|
||||
|
||||
// add ports not in db
|
||||
for (let i = 0; i < device.expand.ports.length; i++) {
|
||||
const port = device.expand.ports[i];
|
||||
if (!port.id) {
|
||||
const result = await pb.collection('ports').create(port);
|
||||
device.ports = [...device.ports, result.id];
|
||||
}
|
||||
}
|
||||
// add ports not in db
|
||||
for (let i = 0; i < device.expand.ports.length; i++) {
|
||||
const port = device.expand.ports[i];
|
||||
if (!port.id) {
|
||||
const result = await pb.collection('ports').create(port);
|
||||
device.ports = [...device.ports, result.id];
|
||||
}
|
||||
}
|
||||
|
||||
// create or update device
|
||||
if (mode === 'add') {
|
||||
await pb.collection('devices').create(device);
|
||||
} else {
|
||||
await pb.collection('devices').update(device.id);
|
||||
}
|
||||
// create or update device
|
||||
if (mode === 'add') {
|
||||
await pb.collection('devices').create(device);
|
||||
} else {
|
||||
await pb.collection('devices').update(device.id);
|
||||
}
|
||||
|
||||
// show button with timeout
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
button.state = 'none';
|
||||
}, 3000);
|
||||
button.state = 'success';
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
setTimeout(() => {
|
||||
button.error = '';
|
||||
button.state = 'none';
|
||||
}, 3000);
|
||||
button.error = error;
|
||||
button.state = 'failed';
|
||||
}
|
||||
}
|
||||
// show button with timeout
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
button.state = 'none';
|
||||
}, 3000);
|
||||
button.state = 'success';
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
setTimeout(() => {
|
||||
button.error = '';
|
||||
button.state = 'none';
|
||||
}, 3000);
|
||||
button.error = error;
|
||||
button.state = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteDevice() {
|
||||
deleteButton.state = 'waiting';
|
||||
try {
|
||||
device.ports.forEach(async (port) => {
|
||||
await pb.collection('ports').delete(port);
|
||||
});
|
||||
await pb.collection('devices').delete(device.id);
|
||||
goto('/');
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
deleteButton.error = '';
|
||||
deleteButton.state = 'none';
|
||||
}, 3000);
|
||||
deleteButton.error = error;
|
||||
deleteButton.state = 'failed';
|
||||
}
|
||||
}
|
||||
async function deleteDevice() {
|
||||
deleteButton.state = 'waiting';
|
||||
try {
|
||||
device.ports.forEach(async (port) => {
|
||||
await pb.collection('ports').delete(port);
|
||||
});
|
||||
await pb.collection('devices').delete(device.id);
|
||||
goto('/');
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
deleteButton.error = '';
|
||||
deleteButton.state = 'none';
|
||||
}, 3000);
|
||||
deleteButton.error = error;
|
||||
deleteButton.state = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePort(idx) {
|
||||
// delete port from db if it has an id
|
||||
// ports with id exist in db, ports without id are not created yet
|
||||
const port = device.expand.ports[idx];
|
||||
if (port.id) {
|
||||
await pb.collection('ports').delete(port.id);
|
||||
const i = device.ports.indexOf(port.id);
|
||||
if (i !== -1) {
|
||||
device.ports.splice(i, 1);
|
||||
device.ports = device.ports;
|
||||
}
|
||||
}
|
||||
device.expand.ports.splice(idx, 1);
|
||||
device.expand.ports = device.expand.ports;
|
||||
}
|
||||
async function deletePort(idx) {
|
||||
// delete port from db if it has an id
|
||||
// ports with id exist in db, ports without id are not created yet
|
||||
const port = device.expand.ports[idx];
|
||||
if (port.id) {
|
||||
await pb.collection('ports').delete(port.id);
|
||||
const i = device.ports.indexOf(port.id);
|
||||
if (i !== -1) {
|
||||
device.ports.splice(i, 1);
|
||||
device.ports = device.ports;
|
||||
}
|
||||
}
|
||||
device.expand.ports.splice(idx, 1);
|
||||
device.expand.ports = device.expand.ports;
|
||||
}
|
||||
|
||||
function addPort() {
|
||||
device.expand.ports = [...device.expand.ports, JSON.parse(JSON.stringify(newPort))];
|
||||
newPort.name = '';
|
||||
newPort.number = null;
|
||||
}
|
||||
function addPort() {
|
||||
device.expand.ports = [...device.expand.ports, JSON.parse(JSON.stringify(newPort))];
|
||||
newPort.name = '';
|
||||
newPort.number = null;
|
||||
}
|
||||
|
||||
function onPasswordInput(event) {
|
||||
device.password = event.target.value;
|
||||
}
|
||||
function onPasswordInput(event) {
|
||||
device.password = event.target.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="m-0 mt-4 p-4 shadow-sm">
|
||||
<h3 class="mb-3">{mode === 'add' ? 'Add new device' : device.name}</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form on:submit|preventDefault={addOrUpdateDevice}>
|
||||
<h5>Required:</h5>
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">Name</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Office Pc"
|
||||
aria-label="Name"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
required
|
||||
bind:value={device.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">IP</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="192.168.1.34"
|
||||
aria-label="IP"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
required
|
||||
bind:value={device.ip}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">MAC</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="aa:bb:cc:dd:ee:ff"
|
||||
aria-label="MAC"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
required
|
||||
bind:value={device.mac}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Netmask</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Most likely 255.255.255.0 or 255.255.0.0"
|
||||
aria-label="Netmask"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
required
|
||||
bind:value={device.netmask}
|
||||
/>
|
||||
</div>
|
||||
<h5 class="mt-4">Optional:</h5>
|
||||
{#if device?.expand?.ports}
|
||||
{#each device?.expand?.ports as port, idx}
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">Port</span>
|
||||
<input
|
||||
type="text"
|
||||
aria-label="Name"
|
||||
class="form-control"
|
||||
placeholder="Name"
|
||||
bind:value={port.name}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
max="65535"
|
||||
aria-label="Number"
|
||||
class="form-control"
|
||||
placeholder="Number"
|
||||
bind:value={port.number}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
on:click={() => deletePort(idx)}
|
||||
>
|
||||
<Fa icon={faTrashCan} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Port</span>
|
||||
<input
|
||||
type="text"
|
||||
aria-label="Name"
|
||||
class="form-control"
|
||||
placeholder="Name"
|
||||
bind:value={newPort.name}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
max="65535"
|
||||
aria-label="Number"
|
||||
class="form-control"
|
||||
placeholder="Number"
|
||||
bind:value={newPort.number}
|
||||
/>
|
||||
<button type="button" class="btn btn-outline-secondary" on:click={() => addPort()}>
|
||||
<Fa icon={faPlus} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Link</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Clickable link on device card"
|
||||
aria-label="Link"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="url"
|
||||
bind:value={device.link}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">Wake Cron<sup>(1)</sup></span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Automatically wake device with cron"
|
||||
aria-label="Wake Cron"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
bind:value={device.wake_cron}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">Shutdown Cron<sup>(1)</sup></span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Automatically shutdown device with cron"
|
||||
aria-label="Shutdown Cron"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
bind:value={device.shutdown_cron}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Shutdown Cmd<sup>(2)</sup></span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Command to shutdown device"
|
||||
aria-label="Shutdown Cmd"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
bind:value={device.shutdown_cmd}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Password<sup>(3)</sup></span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="BIOS password for wol"
|
||||
aria-label="IP"
|
||||
aria-describedby="addon-wrapping"
|
||||
type={passwordType}
|
||||
value={device.password}
|
||||
maxlength="6"
|
||||
on:input={onPasswordInput}
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
on:click={() => (passwordShow = !passwordShow)}
|
||||
>
|
||||
<Fa icon={passwordShow ? faEyeSlash : faEye} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary"
|
||||
class:btn-success={button.state === 'success' ? true : false}
|
||||
class:btn-warning={button.state === 'waiting' ? true : false}
|
||||
class:btn-danger={button.state === 'failed' ? true : false}
|
||||
disabled={button.state !== 'none' ? true : false}
|
||||
>
|
||||
{#if button.state === 'none'}
|
||||
{mode === 'add' ? 'Add device' : 'Save device'}
|
||||
{:else if button.state === 'success'}
|
||||
{mode === 'add' ? 'Added successfully' : 'Saved successfully'}
|
||||
{:else if button.state === 'waiting'}
|
||||
Waiting
|
||||
{:else if button.state === 'failed'}
|
||||
Failed: {button.error}
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex align-items-start flex-column">
|
||||
<div class="callout callout-info mb-auto">
|
||||
<h5>Optional:</h5>
|
||||
<p class="m-0">(1) Same cron syntax as for ping interval.</p>
|
||||
<p class="m-0">(2) Shell command to be executed. e.g.:</p>
|
||||
<ul>
|
||||
<li>Windows: "net rpc shutdown -I 192.168.1.13 -U test%test"</li>
|
||||
<li>
|
||||
Linux: "sshpass -p your_password ssh -o 'StrictHostKeyChecking=no' user@hostname 'sudo
|
||||
shutdown'"
|
||||
</li>
|
||||
</ul>
|
||||
<p class="m-0">
|
||||
(3) Some network cards have the option to set a password for magic packets, also called
|
||||
"SecureON". Password can only be 0, 4 or 6 characters in length.
|
||||
</p>
|
||||
</div>
|
||||
{#if mode === 'edit'}
|
||||
<div class="text-end mt-3 align-self-end">
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
on:click={() => deleteDevice()}
|
||||
on:keydown={() => deleteDevice()}
|
||||
>
|
||||
{#if deleteButton.state === 'none'}
|
||||
Delete device
|
||||
{:else if deleteButton.state === 'waiting'}
|
||||
Waiting
|
||||
{:else if deleteButton.state === 'failed'}
|
||||
Failed: {deleteButton.error}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mb-3">{mode === 'add' ? 'Add new device' : device.name}</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form on:submit|preventDefault={addOrUpdateDevice}>
|
||||
<h5>Required:</h5>
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">Name</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Office Pc"
|
||||
aria-label="Name"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
required
|
||||
bind:value={device.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">IP</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="192.168.1.34"
|
||||
aria-label="IP"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
required
|
||||
bind:value={device.ip}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">MAC</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="aa:bb:cc:dd:ee:ff"
|
||||
aria-label="MAC"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
required
|
||||
bind:value={device.mac}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Netmask</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Most likely 255.255.255.0 or 255.255.0.0"
|
||||
aria-label="Netmask"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
required
|
||||
bind:value={device.netmask}
|
||||
/>
|
||||
</div>
|
||||
<h5 class="mt-4">Optional:</h5>
|
||||
{#if device?.expand?.ports}
|
||||
{#each device?.expand?.ports as port, idx}
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">Port</span>
|
||||
<input
|
||||
type="text"
|
||||
aria-label="Name"
|
||||
class="form-control"
|
||||
placeholder="Name"
|
||||
bind:value={port.name}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
max="65535"
|
||||
aria-label="Number"
|
||||
class="form-control"
|
||||
placeholder="Number"
|
||||
bind:value={port.number}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
on:click={() => deletePort(idx)}
|
||||
>
|
||||
<Fa icon={faTrashCan} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Port</span>
|
||||
<input
|
||||
type="text"
|
||||
aria-label="Name"
|
||||
class="form-control"
|
||||
placeholder="Name"
|
||||
bind:value={newPort.name}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
max="65535"
|
||||
aria-label="Number"
|
||||
class="form-control"
|
||||
placeholder="Number"
|
||||
bind:value={newPort.number}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
on:click={() => addPort()}
|
||||
>
|
||||
<Fa icon={faPlus} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Link</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Clickable link on device card"
|
||||
aria-label="Link"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="url"
|
||||
bind:value={device.link}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">Wake Cron<sup>(1)</sup></span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Automatically wake device with cron"
|
||||
aria-label="Wake Cron"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
bind:value={device.wake_cron}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">Shutdown Cron<sup>(1)</sup></span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Automatically shutdown device with cron"
|
||||
aria-label="Shutdown Cron"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
bind:value={device.shutdown_cron}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Shutdown Cmd<sup>(2)</sup></span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Command to shutdown device"
|
||||
aria-label="Shutdown Cmd"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
bind:value={device.shutdown_cmd}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Password<sup>(3)</sup></span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="BIOS password for wol"
|
||||
aria-label="IP"
|
||||
aria-describedby="addon-wrapping"
|
||||
type={passwordType}
|
||||
value={device.password}
|
||||
maxlength="6"
|
||||
on:input={onPasswordInput}
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
on:click={() => (passwordShow = !passwordShow)}
|
||||
>
|
||||
<Fa icon={passwordShow ? faEyeSlash : faEye} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary"
|
||||
class:btn-success={button.state === 'success' ? true : false}
|
||||
class:btn-warning={button.state === 'waiting' ? true : false}
|
||||
class:btn-danger={button.state === 'failed' ? true : false}
|
||||
disabled={button.state !== 'none' ? true : false}
|
||||
>
|
||||
{#if button.state === 'none'}
|
||||
{mode === 'add' ? 'Add device' : 'Save device'}
|
||||
{:else if button.state === 'success'}
|
||||
{mode === 'add' ? 'Added successfully' : 'Saved successfully'}
|
||||
{:else if button.state === 'waiting'}
|
||||
Waiting
|
||||
{:else if button.state === 'failed'}
|
||||
Failed: {button.error}
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex align-items-start flex-column">
|
||||
<div class="callout callout-info mb-auto">
|
||||
<h5>Optional:</h5>
|
||||
<p class="m-0">(1) Same cron syntax as for ping interval.</p>
|
||||
<p class="m-0">(2) Shell command to be executed. e.g.:</p>
|
||||
<ul>
|
||||
<li>Windows: "net rpc shutdown -I 192.168.1.13 -U test%test"</li>
|
||||
<li>
|
||||
Linux: "sshpass -p your_password ssh -o 'StrictHostKeyChecking=no'
|
||||
user@hostname 'sudo shutdown'"
|
||||
</li>
|
||||
</ul>
|
||||
<p class="m-0">
|
||||
(3) Some network cards have the option to set a password for magic packets, also
|
||||
called "SecureON". Password can only be 0, 4 or 6 characters in length.
|
||||
</p>
|
||||
</div>
|
||||
{#if mode === 'edit'}
|
||||
<div class="text-end mt-3 align-self-end">
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
on:click={() => deleteDevice()}
|
||||
on:keydown={() => deleteDevice()}
|
||||
>
|
||||
{#if deleteButton.state === 'none'}
|
||||
Delete device
|
||||
{:else if deleteButton.state === 'waiting'}
|
||||
Waiting
|
||||
{:else if deleteButton.state === 'failed'}
|
||||
Failed: {deleteButton.error}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,76 +1,78 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { theme } from '@stores/theme';
|
||||
import { faSun, faMoon, faCircleHalfStroke, faBrush } from '@fortawesome/free-solid-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
import { page } from '$app/stores';
|
||||
import { theme } from '@stores/theme';
|
||||
import { faSun, faMoon, faCircleHalfStroke, faBrush } from '@fortawesome/free-solid-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
</script>
|
||||
|
||||
<nav class="navbar navbar-expand">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="/favicon.png" alt="UpSnap" width="30" height="30" />
|
||||
</a>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" class:active={$page.url.pathname === '/' ? true : false} href="/"
|
||||
>Home</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
class:active={$page.url.pathname === '/settings' ? true : false}
|
||||
href="/settings">Settings</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item dropdown ms-3">
|
||||
<div
|
||||
class="nav-link dropdown-toggle"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<Fa icon={faBrush} class="me-2" />
|
||||
Theme
|
||||
</div>
|
||||
<ul class="dropdown-menu border-0 p-1">
|
||||
<li>
|
||||
<div
|
||||
class="dropdown-item"
|
||||
class:active={$theme === 'light'}
|
||||
on:click={() => ($theme = 'light')}
|
||||
on:keydown={() => ($theme = 'light')}
|
||||
>
|
||||
<Fa icon={faSun} class="me-2" />
|
||||
Light
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="dropdown-item"
|
||||
class:active={$theme === 'dark'}
|
||||
on:click={() => ($theme = 'dark')}
|
||||
on:keydown={() => ($theme = 'dark')}
|
||||
>
|
||||
<Fa icon={faMoon} class="me-2" />
|
||||
Dark
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="dropdown-item"
|
||||
class:active={$theme === 'auto'}
|
||||
on:click={() => ($theme = 'auto')}
|
||||
on:keydown={() => ($theme = 'auto')}
|
||||
>
|
||||
<Fa icon={faCircleHalfStroke} class="me-2" />
|
||||
Auto
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="/favicon.png" alt="UpSnap" width="30" height="30" />
|
||||
</a>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
class:active={$page.url.pathname === '/' ? true : false}
|
||||
href="/">Home</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
class:active={$page.url.pathname === '/settings' ? true : false}
|
||||
href="/settings">Settings</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item dropdown ms-3">
|
||||
<div
|
||||
class="nav-link dropdown-toggle"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<Fa icon={faBrush} class="me-2" />
|
||||
Theme
|
||||
</div>
|
||||
<ul class="dropdown-menu border-0 p-1">
|
||||
<li>
|
||||
<div
|
||||
class="dropdown-item"
|
||||
class:active={$theme === 'light'}
|
||||
on:click={() => ($theme = 'light')}
|
||||
on:keydown={() => ($theme = 'light')}
|
||||
>
|
||||
<Fa icon={faSun} class="me-2" />
|
||||
Light
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="dropdown-item"
|
||||
class:active={$theme === 'dark'}
|
||||
on:click={() => ($theme = 'dark')}
|
||||
on:keydown={() => ($theme = 'dark')}
|
||||
>
|
||||
<Fa icon={faMoon} class="me-2" />
|
||||
Dark
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="dropdown-item"
|
||||
class:active={$theme === 'auto'}
|
||||
on:click={() => ($theme = 'auto')}
|
||||
on:keydown={() => ($theme = 'auto')}
|
||||
>
|
||||
<Fa icon={faCircleHalfStroke} class="me-2" />
|
||||
Auto
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import { fly } from 'svelte/transition';
|
||||
export let url;
|
||||
import { fly } from 'svelte/transition';
|
||||
export let url;
|
||||
</script>
|
||||
|
||||
{#key url}
|
||||
<div in:fly={{ y: 50, duration: 300 }}>
|
||||
<slot />
|
||||
</div>
|
||||
<div in:fly={{ y: 50, duration: 300 }}>
|
||||
<slot />
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import Navbar from '@components/Navbar.svelte';
|
||||
import Transition from '@components/Transition.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { theme } from '@stores/theme';
|
||||
import { onMount } from 'svelte';
|
||||
import Navbar from '@components/Navbar.svelte';
|
||||
import Transition from '@components/Transition.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { theme } from '@stores/theme';
|
||||
|
||||
let preferesDark;
|
||||
let preferesDark;
|
||||
|
||||
onMount(() => {
|
||||
// import bootstrap js
|
||||
@@ -53,9 +53,9 @@
|
||||
<Navbar />
|
||||
|
||||
<Transition url={$page.url}>
|
||||
<slot />
|
||||
<slot />
|
||||
</Transition>
|
||||
|
||||
<style lang="scss" global>
|
||||
@import '../scss/main.scss';
|
||||
@import '../scss/main.scss';
|
||||
</style>
|
||||
|
||||
@@ -1,69 +1,71 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import DeviceCard from '@components/DeviceCard.svelte';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
import { onMount } from 'svelte';
|
||||
import DeviceCard from '@components/DeviceCard.svelte';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
|
||||
let devices = {};
|
||||
let devices = {};
|
||||
|
||||
onMount(async () => {
|
||||
let pb;
|
||||
pocketbase.subscribe(conn => {
|
||||
pb = conn;
|
||||
});
|
||||
onMount(async () => {
|
||||
let pb;
|
||||
pocketbase.subscribe((conn) => {
|
||||
pb = conn;
|
||||
});
|
||||
|
||||
// get all devices in pocketbase
|
||||
const result = await pb.collection('devices').getFullList(200, {
|
||||
expand: 'ports',
|
||||
sort: 'name'
|
||||
});
|
||||
result.forEach((device) => {
|
||||
devices[device.id] = device;
|
||||
});
|
||||
// get all devices in pocketbase
|
||||
const result = await pb.collection('devices').getFullList(200, {
|
||||
expand: 'ports',
|
||||
sort: 'name'
|
||||
});
|
||||
result.forEach((device) => {
|
||||
devices[device.id] = device;
|
||||
});
|
||||
|
||||
// subscribe to database events
|
||||
pb.collection('devices').subscribe('*', async (e) => {
|
||||
if (e.action === 'create') {
|
||||
devices[e.record.id] = e.record;
|
||||
} else if (e.action === 'update') {
|
||||
const device = await pb.collection('devices').getOne(e.record.id, {
|
||||
expand: 'ports'
|
||||
})
|
||||
devices[device.id] = device
|
||||
} else if (e.action === 'delete') {
|
||||
delete devices[e.record.id]
|
||||
}
|
||||
});
|
||||
pb.collection('ports').subscribe('*', async (e) => {
|
||||
if (e.action === 'update') {
|
||||
const device = await pb.collection('devices').getFirstListItem(`ports.id ?= "${e.record.id}"`, {
|
||||
expand: 'ports'
|
||||
})
|
||||
devices[device.id] = device
|
||||
}
|
||||
});
|
||||
});
|
||||
// subscribe to database events
|
||||
pb.collection('devices').subscribe('*', async (e) => {
|
||||
if (e.action === 'create') {
|
||||
devices[e.record.id] = e.record;
|
||||
} else if (e.action === 'update') {
|
||||
const device = await pb.collection('devices').getOne(e.record.id, {
|
||||
expand: 'ports'
|
||||
});
|
||||
devices[device.id] = device;
|
||||
} else if (e.action === 'delete') {
|
||||
delete devices[e.record.id];
|
||||
}
|
||||
});
|
||||
pb.collection('ports').subscribe('*', async (e) => {
|
||||
if (e.action === 'update') {
|
||||
const device = await pb
|
||||
.collection('devices')
|
||||
.getFirstListItem(`ports.id ?= "${e.record.id}"`, {
|
||||
expand: 'ports'
|
||||
});
|
||||
devices[device.id] = device;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// update device date
|
||||
let now = Date.now();
|
||||
let clear;
|
||||
$: {
|
||||
clearInterval(clear);
|
||||
clear = setInterval(() => {
|
||||
now = Date.now();
|
||||
}, 1000);
|
||||
}
|
||||
// update device date
|
||||
let now = Date.now();
|
||||
let clear;
|
||||
$: {
|
||||
clearInterval(clear);
|
||||
clear = setInterval(() => {
|
||||
now = Date.now();
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container text-body-emphasis">
|
||||
{#if Object.keys(devices).length > 0}
|
||||
<div class="row">
|
||||
{#each Object.entries(devices) as [_, device]}
|
||||
<DeviceCard {device} {now} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<h4 class="text-muted">No devices</h4>
|
||||
</div>
|
||||
{/if}
|
||||
{#if Object.keys(devices).length > 0}
|
||||
<div class="row">
|
||||
{#each Object.entries(devices) as [_, device]}
|
||||
<DeviceCard {device} {now} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<h4 class="text-muted">No devices</h4>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,5 +2,5 @@ export const prerender = true;
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export function load({ params }) {
|
||||
return { params }
|
||||
return { params };
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
import DeviceForm from '@components/DeviceForm.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
import DeviceForm from '@components/DeviceForm.svelte';
|
||||
|
||||
export let data;
|
||||
const id = data.params.id;
|
||||
let pb;
|
||||
let device = {
|
||||
id: id
|
||||
};
|
||||
export let data;
|
||||
const id = data.params.id;
|
||||
let pb;
|
||||
let device = {
|
||||
id: id
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
pocketbase.subscribe((conn) => {
|
||||
pb = conn;
|
||||
});
|
||||
onMount(async () => {
|
||||
pocketbase.subscribe((conn) => {
|
||||
pb = conn;
|
||||
});
|
||||
|
||||
const result = await pb.collection('devices').getOne(id, {
|
||||
expand: 'ports'
|
||||
});
|
||||
device = result;
|
||||
});
|
||||
const result = await pb.collection('devices').getOne(id, {
|
||||
expand: 'ports'
|
||||
});
|
||||
device = result;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<DeviceForm bind:device mode="edit" />
|
||||
<DeviceForm bind:device mode="edit" />
|
||||
</div>
|
||||
|
||||
@@ -1,239 +1,245 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
import DeviceForm from '@components/DeviceForm.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
import DeviceForm from '@components/DeviceForm.svelte';
|
||||
|
||||
let pb;
|
||||
let files;
|
||||
let settings = {};
|
||||
let timeout;
|
||||
let buttons = {
|
||||
settings: {
|
||||
state: 'none',
|
||||
error: ''
|
||||
},
|
||||
restore: {
|
||||
state: 'none',
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
let newDevice = {
|
||||
name: '',
|
||||
ip: '',
|
||||
mac: '',
|
||||
netmask: '255.255.255.0',
|
||||
expand: {
|
||||
ports: []
|
||||
},
|
||||
ports: [],
|
||||
link: '',
|
||||
wake_cron: '',
|
||||
shutdown_cron: '',
|
||||
shutdown_cmd: '',
|
||||
password: ''
|
||||
};
|
||||
let pb;
|
||||
let files;
|
||||
let settings = {};
|
||||
let timeout;
|
||||
let buttons = {
|
||||
settings: {
|
||||
state: 'none',
|
||||
error: ''
|
||||
},
|
||||
restore: {
|
||||
state: 'none',
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
let newDevice = {
|
||||
name: '',
|
||||
ip: '',
|
||||
mac: '',
|
||||
netmask: '255.255.255.0',
|
||||
expand: {
|
||||
ports: []
|
||||
},
|
||||
ports: [],
|
||||
link: '',
|
||||
wake_cron: '',
|
||||
shutdown_cron: '',
|
||||
shutdown_cmd: '',
|
||||
password: ''
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
pocketbase.subscribe((conn) => {
|
||||
pb = conn;
|
||||
});
|
||||
onMount(async () => {
|
||||
pocketbase.subscribe((conn) => {
|
||||
pb = conn;
|
||||
});
|
||||
|
||||
const result = await pb.collection('settings').getList(1, 1);
|
||||
settings = result.items[0];
|
||||
});
|
||||
const result = await pb.collection('settings').getList(1, 1);
|
||||
settings = result.items[0];
|
||||
});
|
||||
|
||||
async function saveSettings() {
|
||||
buttons.settings.state = 'waiting';
|
||||
try {
|
||||
if (settings.interval === '') {
|
||||
settings.interval = '@every 3s';
|
||||
}
|
||||
await pb.collection('settings').update(settings.id, {
|
||||
interval: settings.interval,
|
||||
notifications: settings.notifications
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
buttons.settings.state = 'none';
|
||||
}, 3000);
|
||||
buttons.settings.state = 'success';
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
buttons.settings.error = '';
|
||||
buttons.settings.state = 'none';
|
||||
}, 3000);
|
||||
buttons.settings.error = error;
|
||||
buttons.settings.state = 'failed';
|
||||
}
|
||||
}
|
||||
async function saveSettings() {
|
||||
buttons.settings.state = 'waiting';
|
||||
try {
|
||||
if (settings.interval === '') {
|
||||
settings.interval = '@every 3s';
|
||||
}
|
||||
await pb.collection('settings').update(settings.id, {
|
||||
interval: settings.interval,
|
||||
notifications: settings.notifications
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
buttons.settings.state = 'none';
|
||||
}, 3000);
|
||||
buttons.settings.state = 'success';
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
buttons.settings.error = '';
|
||||
buttons.settings.state = 'none';
|
||||
}, 3000);
|
||||
buttons.settings.error = error;
|
||||
buttons.settings.state = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreV2Backup() {
|
||||
buttons.restore.state = 'waiting';
|
||||
async function restoreV2Backup() {
|
||||
buttons.restore.state = 'waiting';
|
||||
|
||||
try {
|
||||
// delete all devices in pocketbase
|
||||
let result = await pb.collection('devices').getFullList();
|
||||
for (let index = 0; index < result.length; index++) {
|
||||
pb.collection('devices').delete(result[index].id);
|
||||
}
|
||||
// delete all ports in pocketbase
|
||||
result = await pb.collection('ports').getFullList();
|
||||
for (let index = 0; index < result.length; index++) {
|
||||
await pb.collection('ports').delete(result[index].id);
|
||||
}
|
||||
try {
|
||||
// delete all devices in pocketbase
|
||||
let result = await pb.collection('devices').getFullList();
|
||||
for (let index = 0; index < result.length; index++) {
|
||||
pb.collection('devices').delete(result[index].id);
|
||||
}
|
||||
// delete all ports in pocketbase
|
||||
result = await pb.collection('ports').getFullList();
|
||||
for (let index = 0; index < result.length; index++) {
|
||||
await pb.collection('ports').delete(result[index].id);
|
||||
}
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.readAsText(files[0]);
|
||||
reader.onload = async (e) => {
|
||||
// parse uploaded file
|
||||
let data = JSON.parse(e.target.result);
|
||||
if (!Array.isArray(data)) {
|
||||
return;
|
||||
}
|
||||
let reader = new FileReader();
|
||||
reader.readAsText(files[0]);
|
||||
reader.onload = async (e) => {
|
||||
// parse uploaded file
|
||||
let data = JSON.parse(e.target.result);
|
||||
if (!Array.isArray(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// loop devices
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
const device = data[index];
|
||||
// loop devices
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
const device = data[index];
|
||||
|
||||
// create ports
|
||||
let thisDevicePorts = [];
|
||||
for (let index = 0; index < device.ports.length; index++) {
|
||||
const port = device.ports[index];
|
||||
const record = await pb.collection('ports').create({
|
||||
name: port.name,
|
||||
number: port.number
|
||||
});
|
||||
thisDevicePorts.push(record.id);
|
||||
}
|
||||
// create ports
|
||||
let thisDevicePorts = [];
|
||||
for (let index = 0; index < device.ports.length; index++) {
|
||||
const port = device.ports[index];
|
||||
const record = await pb.collection('ports').create({
|
||||
name: port.name,
|
||||
number: port.number
|
||||
});
|
||||
thisDevicePorts.push(record.id);
|
||||
}
|
||||
|
||||
// create device
|
||||
await pb.collection('devices').create({
|
||||
name: device.name,
|
||||
ip: device.ip,
|
||||
mac: device.mac,
|
||||
netmask: device.netmask,
|
||||
ports: thisDevicePorts,
|
||||
link: device.link,
|
||||
wake: device.wake,
|
||||
shutdown: device.shutdown
|
||||
});
|
||||
}
|
||||
};
|
||||
setTimeout(() => {
|
||||
buttons.restore.state = 'none';
|
||||
}, 3000);
|
||||
buttons.restore.state = 'success';
|
||||
} catch (error) {
|
||||
setTimeout(() => {
|
||||
buttons.restore.error = '';
|
||||
buttons.restore.state = 'none';
|
||||
}, 3000);
|
||||
buttons.restore.error = error;
|
||||
buttons.restore.state = 'failed';
|
||||
}
|
||||
}
|
||||
// create device
|
||||
await pb.collection('devices').create({
|
||||
name: device.name,
|
||||
ip: device.ip,
|
||||
mac: device.mac,
|
||||
netmask: device.netmask,
|
||||
ports: thisDevicePorts,
|
||||
link: device.link,
|
||||
wake: device.wake,
|
||||
shutdown: device.shutdown
|
||||
});
|
||||
}
|
||||
};
|
||||
setTimeout(() => {
|
||||
buttons.restore.state = 'none';
|
||||
}, 3000);
|
||||
buttons.restore.state = 'success';
|
||||
} catch (error) {
|
||||
setTimeout(() => {
|
||||
buttons.restore.error = '';
|
||||
buttons.restore.state = 'none';
|
||||
}, 3000);
|
||||
buttons.restore.error = error;
|
||||
buttons.restore.state = 'failed';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<DeviceForm bind:device={newDevice} mode="add" />
|
||||
<section class="m-0 mt-4 p-4 shadow-sm">
|
||||
<form on:submit|preventDefault={saveSettings}>
|
||||
<h3 class="mb-3">Ping interval</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p>Sets the interval in which the devices are pinged.</p>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Cron</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="e.g. '@every 5s' or '@every 1m'"
|
||||
aria-label="Interval"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
bind:value={settings.interval}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info m-0">
|
||||
<p class="m-0">
|
||||
Leave blank to use default value of <span class="fw-bold">"@every 3s"</span>.
|
||||
</p>
|
||||
<p class="m-0">
|
||||
Read more about valid cron syntax at <a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://pkg.go.dev/github.com/robfig/cron"
|
||||
>pkg.go.dev/github.com/robfig/cron</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="my-3">Notifications</h3>
|
||||
<p>Show toast notifications in the bottom right corner.</p>
|
||||
<div class="form-check form-switch">
|
||||
<label class="form-check-label" for="settings-notifications">Enable notifications</label>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="settings-notifications"
|
||||
role="switch"
|
||||
bind:checked={settings.notifications}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary mt-3"
|
||||
class:btn-success={buttons.settings.state === 'success' ? true : false}
|
||||
class:btn-warning={buttons.settings.state === 'waiting' ? true : false}
|
||||
class:btn-danger={buttons.settings.state === 'failed' ? true : false}
|
||||
disabled={buttons.settings.state !== 'none' ? true : false}
|
||||
>
|
||||
{#if buttons.settings.state === 'none'}
|
||||
Save
|
||||
{:else if buttons.settings.state === 'success'}
|
||||
Saved
|
||||
{:else if buttons.settings.state === 'waiting'}
|
||||
Waiting
|
||||
{:else if buttons.settings.state === 'failed'}
|
||||
Failed: {buttons.settings.error}
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="m-0 my-4 p-4 shadow-sm">
|
||||
<h3 class="mb-3">Restore</h3>
|
||||
<p>If you had UpSnap v2.3.2 (or higher) running before, you can restore your devices here.</p>
|
||||
<div class="callout callout-danger fw-bold">This will wipe the existing database!</div>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Username"
|
||||
aria-label="Username"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="file"
|
||||
accept=".json"
|
||||
bind:files
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary mt-3"
|
||||
class:btn-success={buttons.restore.state === 'success' ? true : false}
|
||||
class:btn-warning={buttons.restore.state === 'waiting' ? true : false}
|
||||
class:btn-danger={buttons.restore.state === 'failed' ? true : false}
|
||||
disabled={files ? false : true}
|
||||
on:click={() => restoreV2Backup()}
|
||||
>
|
||||
{#if buttons.restore.state === 'none'}
|
||||
Restore
|
||||
{:else if buttons.restore.state === 'success'}
|
||||
Restored
|
||||
{:else if buttons.restore.state === 'waiting'}
|
||||
Waiting
|
||||
{:else if buttons.restore.state === 'failed'}
|
||||
Failed: {buttons.restore.error}
|
||||
{/if}
|
||||
</button>
|
||||
</section>
|
||||
<DeviceForm bind:device={newDevice} mode="add" />
|
||||
<section class="m-0 mt-4 p-4 shadow-sm">
|
||||
<form on:submit|preventDefault={saveSettings}>
|
||||
<h3 class="mb-3">Ping interval</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p>Sets the interval in which the devices are pinged.</p>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Cron</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="e.g. '@every 5s' or '@every 1m'"
|
||||
aria-label="Interval"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="text"
|
||||
bind:value={settings.interval}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info m-0">
|
||||
<p class="m-0">
|
||||
Leave blank to use default value of <span class="fw-bold"
|
||||
>"@every 3s"</span
|
||||
>.
|
||||
</p>
|
||||
<p class="m-0">
|
||||
Read more about valid cron syntax at <a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://pkg.go.dev/github.com/robfig/cron"
|
||||
>pkg.go.dev/github.com/robfig/cron</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="my-3">Notifications</h3>
|
||||
<p>Show toast notifications in the bottom right corner.</p>
|
||||
<div class="form-check form-switch">
|
||||
<label class="form-check-label" for="settings-notifications"
|
||||
>Enable notifications</label
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="settings-notifications"
|
||||
role="switch"
|
||||
bind:checked={settings.notifications}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary mt-3"
|
||||
class:btn-success={buttons.settings.state === 'success' ? true : false}
|
||||
class:btn-warning={buttons.settings.state === 'waiting' ? true : false}
|
||||
class:btn-danger={buttons.settings.state === 'failed' ? true : false}
|
||||
disabled={buttons.settings.state !== 'none' ? true : false}
|
||||
>
|
||||
{#if buttons.settings.state === 'none'}
|
||||
Save
|
||||
{:else if buttons.settings.state === 'success'}
|
||||
Saved
|
||||
{:else if buttons.settings.state === 'waiting'}
|
||||
Waiting
|
||||
{:else if buttons.settings.state === 'failed'}
|
||||
Failed: {buttons.settings.error}
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="m-0 my-4 p-4 shadow-sm">
|
||||
<h3 class="mb-3">Restore</h3>
|
||||
<p>
|
||||
If you had UpSnap v2.3.2 (or higher) running before, you can restore your devices here.
|
||||
</p>
|
||||
<div class="callout callout-danger fw-bold">This will wipe the existing database!</div>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Username"
|
||||
aria-label="Username"
|
||||
aria-describedby="addon-wrapping"
|
||||
type="file"
|
||||
accept=".json"
|
||||
bind:files
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary mt-3"
|
||||
class:btn-success={buttons.restore.state === 'success' ? true : false}
|
||||
class:btn-warning={buttons.restore.state === 'waiting' ? true : false}
|
||||
class:btn-danger={buttons.restore.state === 'failed' ? true : false}
|
||||
disabled={files ? false : true}
|
||||
on:click={() => restoreV2Backup()}
|
||||
>
|
||||
{#if buttons.restore.state === 'none'}
|
||||
Restore
|
||||
{:else if buttons.restore.state === 'success'}
|
||||
Restored
|
||||
{:else if buttons.restore.state === 'waiting'}
|
||||
Waiting
|
||||
{:else if buttons.restore.state === 'failed'}
|
||||
Failed: {buttons.restore.error}
|
||||
{/if}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
// custom colors
|
||||
$gray-1000: #111;
|
||||
$grays: (
|
||||
'100': $gray-100,
|
||||
'200': $gray-200,
|
||||
'300': $gray-300,
|
||||
'400': $gray-400,
|
||||
'500': $gray-500,
|
||||
'600': $gray-600,
|
||||
'700': $gray-700,
|
||||
'800': $gray-800,
|
||||
'900': $gray-900,
|
||||
'1000': $gray-1000
|
||||
'100': $gray-100,
|
||||
'200': $gray-200,
|
||||
'300': $gray-300,
|
||||
'400': $gray-400,
|
||||
'500': $gray-500,
|
||||
'600': $gray-600,
|
||||
'700': $gray-700,
|
||||
'800': $gray-800,
|
||||
'900': $gray-900,
|
||||
'1000': $gray-1000
|
||||
) !default;
|
||||
|
||||
$body-bg-dark: $gray-1000;
|
||||
@@ -44,99 +44,101 @@ $dropdown-min-width: 0;
|
||||
@import '../../node_modules/bootstrap/scss/utilities/api';
|
||||
|
||||
section {
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 0.3rem 0.6rem !important;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.3rem 0.6rem !important;
|
||||
border-radius: 0.4rem;
|
||||
|
||||
&.active {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
&.active {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.text-success, .port-up {
|
||||
color: $teal-500 !important;
|
||||
.text-success,
|
||||
.port-up {
|
||||
color: $teal-500 !important;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: $yellow !important;
|
||||
color: $yellow !important;
|
||||
}
|
||||
|
||||
.text-danger, .port-down {
|
||||
color: #ef476f !important;
|
||||
.text-danger,
|
||||
.port-down {
|
||||
color: #ef476f !important;
|
||||
}
|
||||
|
||||
.callout {
|
||||
padding: 1rem;
|
||||
border-radius: 0.3rem;
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
border-radius: 0.3rem;
|
||||
margin: 1rem 0;
|
||||
|
||||
&.callout-info {
|
||||
background-color: #1177b21f;
|
||||
border-left: 0.5rem solid #1177b21f;
|
||||
}
|
||||
&.callout-info {
|
||||
background-color: #1177b21f;
|
||||
border-left: 0.5rem solid #1177b21f;
|
||||
}
|
||||
|
||||
&.callout-danger {
|
||||
background-color: #ef476f1f;
|
||||
border-left: 0.5rem solid #ef476f1f;
|
||||
}
|
||||
&.callout-danger {
|
||||
background-color: #ef476f1f;
|
||||
border-left: 0.5rem solid #ef476f1f;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
border-radius: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.power-hover {
|
||||
&:hover {
|
||||
filter: drop-shadow(0px 0px 10px rgb(155, 155, 155));
|
||||
transition: all 0.1s;
|
||||
}
|
||||
&:hover {
|
||||
filter: drop-shadow(0px 0px 10px rgb(155, 155, 155));
|
||||
transition: all 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
width: 1rem;
|
||||
display: block;
|
||||
width: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-bs-theme='light'] {
|
||||
html,
|
||||
body {
|
||||
background: #f0f1f2;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
background: #f0f1f2;
|
||||
}
|
||||
|
||||
section,
|
||||
.card {
|
||||
background: #ffffff;
|
||||
}
|
||||
section,
|
||||
.card {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-bs-theme='dark'] {
|
||||
html,
|
||||
body {
|
||||
background: #131316;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
background: #131316;
|
||||
}
|
||||
|
||||
section,
|
||||
.card {
|
||||
background: #25252b;
|
||||
}
|
||||
section,
|
||||
.card {
|
||||
background: #25252b;
|
||||
}
|
||||
|
||||
.nav-link,
|
||||
.dropdown-menu {
|
||||
&.active {
|
||||
background-color: $gray-800;
|
||||
}
|
||||
}
|
||||
.nav-link,
|
||||
.dropdown-menu {
|
||||
&.active {
|
||||
background-color: $gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background-color: $gray-800;
|
||||
}
|
||||
.dropdown-menu {
|
||||
background-color: $gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://127.0.0.1:8090')
|
||||
pb.autoCancellation(false)
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
pb.autoCancellation(false);
|
||||
|
||||
export let pocketbase = writable(pb);
|
||||
export const pocketbase = writable(pb);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const theme = writable();
|
||||
if (typeof window !== 'undefined') [theme.set(localStorage.getItem('theme') ? localStorage.getItem('theme') : 'auto')];
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const theme = writable();
|
||||
if (typeof window !== 'undefined')
|
||||
[theme.set(localStorage.getItem('theme') ? localStorage.getItem('theme') : 'auto')];
|
||||
|
||||
@@ -4,20 +4,20 @@ import path from 'path';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: preprocess(),
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: preprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
alias: {
|
||||
'@components': path.resolve('./src/components'),
|
||||
'@stores': path.resolve('./src/stores')
|
||||
},
|
||||
prerender: {
|
||||
entries: ['/device/[id]']
|
||||
}
|
||||
}
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
alias: {
|
||||
'@components': path.resolve('./src/components'),
|
||||
'@stores': path.resolve('./src/stores')
|
||||
},
|
||||
prerender: {
|
||||
entries: ['/device/[id]']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
const config = {
|
||||
plugins: [sveltekit()]
|
||||
plugins: [sveltekit()]
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user