frontend: add pre-commit code formatting

This commit is contained in:
Maxi Quoß
2023-01-27 13:36:06 +01:00
parent f29c15c6fc
commit dd5401b265
21 changed files with 4096 additions and 2451 deletions

View File

@@ -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

View File

@@ -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
}
};

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

View File

@@ -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
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -2,5 +2,5 @@ export const prerender = true;
/** @type {import('./$types').PageLoad} */
export function load({ params }) {
return { params }
return { params };
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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')];

View File

@@ -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;

View File

@@ -1,7 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite';
const config = {
plugins: [sveltekit()]
plugins: [sveltekit()]
};
export default config;