mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-03-31 06:24:09 -04:00
first commit for tailwind rewrite
- uses daisyUI - signup, login and logout is done (need some more testing)
This commit is contained in:
@@ -4,11 +4,10 @@ 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,23 +1,30 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:svelte/recommended', 'prettier'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
10
frontend/.gitignore
vendored
Normal file
10
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
1
frontend/.husky/.gitignore
vendored
Normal file
1
frontend/.husky/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend && npx lint-staged
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
engine-strict=true
|
||||
resolution-mode=highest
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
"name": "frontend",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
@@ -41,4 +42,50 @@
|
||||
"*.{js,svelte}": "eslint --fix --ignore-path ../.gitignore .",
|
||||
"*.{js,css,md,svelte,scss}": "prettier --write --ignore-path ../.gitignore --plugin-search-dir=. ."
|
||||
}
|
||||
=======
|
||||
"name": "upsnap-frontend",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "eslint --fix --cache --ignore-path ../.gitignore .",
|
||||
"format": "prettier --write --cache --cache-strategy content --ignore-path ../.gitignore --plugin-search-dir=. .",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"daisyui": "^3.3.1",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"husky": "^8.0.0",
|
||||
"postcss": "^8.4.27",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"type": "module",
|
||||
"lint-staged": {
|
||||
"*.{js,svelte}": "eslint --fix --cache --ignore-path ../.gitignore .",
|
||||
"*.{js,css,md,svelte,scss}": "prettier --write --cache --cache-strategy content --ignore-path ../.gitignore --plugin-search-dir=. ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"pocketbase": "^0.15.3",
|
||||
"svelte-fa": "^3.0.4"
|
||||
}
|
||||
>>>>>>> 5340d83 (first commit for tailwind rewrite)
|
||||
}
|
||||
|
||||
794
frontend/pnpm-lock.yaml
generated
794
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
3
frontend/src/app.css
Normal file
3
frontend/src/app.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
12
frontend/src/app.d.ts
vendored
Normal file
12
frontend/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,12 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-100">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover" class="h-100">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
<script>
|
||||
import { dev } from '$app/environment';
|
||||
import { parseISO, formatDistance } from 'date-fns';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
import Fa from 'svelte-fa/src/fa.svelte';
|
||||
import {
|
||||
faPowerOff,
|
||||
faEllipsisVertical,
|
||||
faCircle,
|
||||
faCircleUp,
|
||||
faCircleDown,
|
||||
faLock
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { sortPorts } from '../sorts';
|
||||
|
||||
export let device;
|
||||
export let now;
|
||||
|
||||
function shutdown() {
|
||||
fetch(`${dev ? 'http://localhost:8090' : ''}/api/upsnap/shutdown/${device.id}`, {
|
||||
headers: {
|
||||
Authorization: $pocketbase.authStore.baseToken
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function wake() {
|
||||
fetch(`${dev ? 'http://localhost:8090' : ''}/api/upsnap/wake/${device.id}`, {
|
||||
headers: {
|
||||
Authorization: $pocketbase.authStore.baseToken
|
||||
}
|
||||
});
|
||||
}
|
||||
</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"
|
||||
role="none"
|
||||
on:click={() => wake()}
|
||||
on:keydown={() => wake()}
|
||||
>
|
||||
<div>
|
||||
<Fa icon={faPowerOff} class="fs-4 power-hover" />
|
||||
</div>
|
||||
</span>
|
||||
{:else if device.status === 'online'}
|
||||
{#if device.shutdown_cmd !== ''}
|
||||
<span
|
||||
class="text-success"
|
||||
role="none"
|
||||
on:click={() => shutdown()}
|
||||
on:keydown={() => shutdown()}
|
||||
>
|
||||
<div>
|
||||
<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 text-body-emphasis">
|
||||
<a class="text-reset" target="_blank" rel="noreferrer" href={device.link}
|
||||
>{device.name}</a
|
||||
>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="m-0 fw-bold fs-5 text-body-emphasis">{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}
|
||||
{#if device.wake_cron !== ''}
|
||||
<span
|
||||
class="badge rounded-pill {device.wake_cron_enabled
|
||||
? 'text-bg-success'
|
||||
: 'text-bg-danger'}"
|
||||
data-toggle="tooltip"
|
||||
title="Wake cron {device.wake_cron_enabled ? 'enabled' : 'disabled'}"
|
||||
>
|
||||
<Fa icon={faCircleUp} class="me-1" />
|
||||
{device.wake_cron}
|
||||
</span>
|
||||
{/if}
|
||||
{#if device.shutdown_cron !== ''}
|
||||
<span
|
||||
class="badge rounded-pill {device.shutdown_cron_enabled
|
||||
? 'text-bg-success'
|
||||
: 'text-bg-danger'}"
|
||||
data-toggle="tooltip"
|
||||
title="Shutdown cron {device.shutdown_cron_enabled ? 'enabled' : 'disabled'}"
|
||||
>
|
||||
<Fa icon={faCircleDown} class="me-1" />
|
||||
{device.shutdown_cron}
|
||||
</span>
|
||||
{/if}
|
||||
{#if device.password !== ''}
|
||||
<span
|
||||
class="badge rounded-pill text-bg-secondary"
|
||||
data-toggle="tooltip"
|
||||
title="Wake password set"
|
||||
>
|
||||
<Fa icon={faLock} class="mx-1" />
|
||||
</span>
|
||||
{/if}
|
||||
<p
|
||||
class="text-muted m-0"
|
||||
data-toggle="tooltip"
|
||||
title="Last status change: {device.updated}"
|
||||
>
|
||||
{formatDistance(parseISO(device.updated), now, {
|
||||
includeSeconds: true,
|
||||
addSuffix: true
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,418 +0,0 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { pocketbase, devices, groups } from '@stores/pocketbase';
|
||||
import Fa from 'svelte-fa/src/fa.svelte';
|
||||
import { faEye, faEyeSlash, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export let device;
|
||||
export let mode;
|
||||
|
||||
let timeout;
|
||||
let button = {
|
||||
state: 'none',
|
||||
error: ''
|
||||
};
|
||||
let deleteButton = {
|
||||
state: 'none',
|
||||
error: ''
|
||||
};
|
||||
let newPort = {
|
||||
name: '',
|
||||
number: null
|
||||
};
|
||||
let passwordShow = false;
|
||||
$: passwordType = passwordShow ? 'text' : 'password';
|
||||
|
||||
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 $pocketbase.collection('ports').create(port);
|
||||
device.ports = [...device.ports, result.id];
|
||||
} else {
|
||||
await $pocketbase.collection('ports').update(port.id, port);
|
||||
}
|
||||
}
|
||||
|
||||
// create or update device
|
||||
if (mode === 'add') {
|
||||
await $pocketbase.collection('devices').create(device);
|
||||
} else {
|
||||
await $pocketbase.collection('devices').update(device.id, device);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
await $pocketbase.collection('devices').delete(device.id);
|
||||
device.ports.forEach(async (port) => {
|
||||
await $pocketbase.collection('ports').delete(port);
|
||||
});
|
||||
delete $devices[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 $pocketbase.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 onPasswordInput(event) {
|
||||
device.password = event.target.value;
|
||||
}
|
||||
|
||||
function toggleGroup(grpId) {
|
||||
const i = device.groups.indexOf(grpId);
|
||||
if (i !== -1) {
|
||||
device.groups.splice(i, 1);
|
||||
device.groups = device.groups;
|
||||
} else {
|
||||
device.groups = [...device.groups, grpId];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="m-0 mt-4 p-4 shadow-sm">
|
||||
<h3 class="mb-3 text-body-emphasis">{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"
|
||||
min="0"
|
||||
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-1">
|
||||
<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"
|
||||
min="0"
|
||||
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-1">
|
||||
<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 class="input-group-text form-switch">
|
||||
<label class="form-check-label" for="wake-cron-enabled">Enable</label>
|
||||
<input
|
||||
id="wake-cron-enabled"
|
||||
class="form-check-input mt-0 ms-1"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
aria-label="Enable/Disable wake cron"
|
||||
bind:checked={device.wake_cron_enabled}
|
||||
/>
|
||||
</div>
|
||||
</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 class="input-group-text form-switch">
|
||||
<label class="form-check-label" for="wake-shutdown-enabled">Enable</label>
|
||||
<input
|
||||
id="wake-shutdown-enabled"
|
||||
class="form-check-input mt-0 ms-1"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
aria-label="Enable/Disable wake cron"
|
||||
bind:checked={device.shutdown_cron_enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group mb-1">
|
||||
<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>
|
||||
{#if groups}
|
||||
<h5 class="mt-4">Groups:</h5>
|
||||
<div class="input-group mb-3">
|
||||
{#each $groups as grp}
|
||||
<span
|
||||
class="badge rounded-pill fs-6 cursor-pointer me-2 {device?.groups?.includes(
|
||||
grp.id
|
||||
)
|
||||
? 'text-bg-success'
|
||||
: 'text-bg-secondary text-white-50'} "
|
||||
role="none"
|
||||
on:click={() => toggleGroup(grp.id)}
|
||||
on:keydown={() => toggleGroup(grp.id)}>{grp.name}</span
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<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) Learn more about the correct syntax for cron on
|
||||
<a href="https://en.wikipedia.org/wiki/Cron" target="_blank" rel="noreferrer"
|
||||
>wikipedia</a
|
||||
>
|
||||
or refer to the
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://pkg.go.dev/github.com/robfig/cron">package documentation</a
|
||||
>
|
||||
</p>
|
||||
<p class="m-0">
|
||||
(2) Shell command to be executed. "net rpc", "sshpass" and "curl" are available.
|
||||
e.g.:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Windows: <code>net rpc shutdown -I 192.168.1.13 -U "user%password"</code>
|
||||
</li>
|
||||
<li>
|
||||
Linux: <code
|
||||
>sshpass -p your_password ssh -o "StrictHostKeyChecking=no"
|
||||
user@hostname "sudo poweroff"</code
|
||||
>
|
||||
</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,237 +0,0 @@
|
||||
<script>
|
||||
import { dev } from '$app/environment';
|
||||
import { page } from '$app/stores';
|
||||
import { pocketbase, authorizedStore, settings_public } from '@stores/pocketbase';
|
||||
|
||||
let username;
|
||||
let password;
|
||||
let passwordConfirm;
|
||||
let isAdmin;
|
||||
let callout = {
|
||||
hidden: true,
|
||||
level: 'danger',
|
||||
title: '',
|
||||
msg: ''
|
||||
};
|
||||
|
||||
$: if ($settings_public.setup_completed === false) {
|
||||
callout.title = '';
|
||||
callout.msg = 'No admin account has been set up. Please create one.';
|
||||
callout.level = 'info';
|
||||
callout.hidden = false;
|
||||
}
|
||||
|
||||
async function loginUserPass() {
|
||||
if (isAdmin) {
|
||||
$pocketbase.admins
|
||||
.authWithPassword(username, password)
|
||||
.then(() => {
|
||||
authorizedStore.set(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
authorizedStore.set(false);
|
||||
callout.level = 'danger';
|
||||
callout.title = err.status;
|
||||
callout.msg = err.message;
|
||||
callout.hidden = false;
|
||||
});
|
||||
} else {
|
||||
$pocketbase
|
||||
.collection('users')
|
||||
.authWithPassword(username, password)
|
||||
.then(() => {
|
||||
authorizedStore.set(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
authorizedStore.set(false);
|
||||
callout.level = 'danger';
|
||||
callout.title = err.status;
|
||||
callout.msg = err.message;
|
||||
callout.hidden = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function loginOIDC() {
|
||||
$pocketbase
|
||||
.collection('users')
|
||||
.authWithOAuth2({ provider: 'oidc' })
|
||||
.then(() => {
|
||||
authorizedStore.set(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
authorizedStore.set(false);
|
||||
callout.level = 'danger';
|
||||
callout.title = err.status;
|
||||
callout.msg = err.message;
|
||||
callout.hidden = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function register() {
|
||||
if (password !== passwordConfirm) {
|
||||
callout.level = 'danger';
|
||||
callout.title = "Passwords don't match";
|
||||
callout.msg = 'Please try again';
|
||||
callout.hidden = false;
|
||||
return;
|
||||
} else {
|
||||
callout.hidden = true;
|
||||
}
|
||||
$pocketbase.admins
|
||||
.create({
|
||||
email: username,
|
||||
password: password,
|
||||
passwordConfirm: passwordConfirm
|
||||
})
|
||||
.then(() => {
|
||||
$pocketbase.admins
|
||||
.authWithPassword(username, password)
|
||||
.then(() => {
|
||||
authorizedStore.set(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
return err;
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
authorizedStore.set(false);
|
||||
callout.level = 'danger';
|
||||
callout.title = err.status;
|
||||
callout.msg = err.message;
|
||||
callout.hidden = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container text-dark-emphasis">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col text-center p-3 p-md-5">
|
||||
<img
|
||||
src={$settings_public.favicon !== ''
|
||||
? `${dev ? $pocketbase.baseUrl : ''}/api/files/settings_public/${
|
||||
$settings_public.id
|
||||
}/${$settings_public.favicon}`
|
||||
: '/gopher.svg'}
|
||||
alt={$settings_public.website_title ? $settings_public.website_title : 'UpSnap'}
|
||||
class="logo pe-none user-select-none"
|
||||
/>
|
||||
<h2 class="text-dark-emphasis fw-bold text-center mt-3">
|
||||
{$settings_public.setup_completed === true ? 'Login' : 'Register'}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center align-items-center p-2">
|
||||
<div class="col-md-6 col-lg-5 p-4 login rounded-5 shadow-sm">
|
||||
<div class="callout callout-{callout.level} mt-0" class:d-none={callout.hidden}>
|
||||
{#if callout.title !== ''}
|
||||
<h5>{callout.title}</h5>
|
||||
{/if}
|
||||
{#if callout.msg !== ''}
|
||||
<p class="m-0">{callout.msg}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $settings_public.setup_completed === true}
|
||||
<form class="w-100" on:submit|preventDefault={loginUserPass}>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username or email</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="username"
|
||||
placeholder="user@example.com"
|
||||
bind:value={username}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
placeholder="secret-password"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="flexSwitchCheckChecked"
|
||||
bind:checked={isAdmin}
|
||||
/>
|
||||
<label class="form-check-label" for="flexSwitchCheckChecked"
|
||||
>Admin login</label
|
||||
>
|
||||
</div>
|
||||
<button class="btn btn-secondary w-100" type="submit">Login</button>
|
||||
</form>
|
||||
<p class="text-center m-0 mt-3">
|
||||
<!-- hacky way of linking a non existing route in svelte -->
|
||||
<a href="/_/" rel="external" class="btn btn-outline-secondary btn-sm"
|
||||
>Manage users</a
|
||||
>
|
||||
<button
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
data-toggle="tooltip"
|
||||
title="You can setup OIDC at {$page.url.host}/_/#/settings/auth-providers"
|
||||
on:click={() => loginOIDC()}
|
||||
on:keydown={() => loginOIDC()}>Login with OIDC</button
|
||||
>
|
||||
</p>
|
||||
{:else}
|
||||
<form class="w-100" on:submit|preventDefault={register}>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="username"
|
||||
placeholder="user@example.com"
|
||||
bind:value={username}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label"
|
||||
>Password (min. 10 characters)</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
placeholder="secret-password"
|
||||
minlength="10"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="passwordConfirm" class="form-label">Password confirm</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="passwordConfirm"
|
||||
placeholder="secret-password"
|
||||
minlength="10"
|
||||
bind:value={passwordConfirm}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button class="btn btn-secondary w-100" type="submit">Create and login</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.logo {
|
||||
width: 40%;
|
||||
min-width: 50px;
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,146 +0,0 @@
|
||||
<script>
|
||||
import { dev } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { theme } from '@stores/theme';
|
||||
import { pocketbase, authorizedStore, settings_public } from '@stores/pocketbase';
|
||||
import Fa from 'svelte-fa/src/fa.svelte';
|
||||
import {
|
||||
faSun,
|
||||
faMoon,
|
||||
faCircleHalfStroke,
|
||||
faBrush,
|
||||
faRightFromBracket
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
let userInfo = {
|
||||
usernameOrEmail: '',
|
||||
role: ''
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($pocketbase.authStore.baseModel?.collectionName === 'users') {
|
||||
userInfo.role = 'user';
|
||||
} else {
|
||||
userInfo.role = 'admin';
|
||||
}
|
||||
if ($pocketbase.authStore.baseModel?.username) {
|
||||
userInfo.usernameOrEmail = $pocketbase.authStore.baseModel.username;
|
||||
} else {
|
||||
userInfo.usernameOrEmail = $pocketbase.authStore.baseModel.email;
|
||||
}
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
$pocketbase.authStore.clear();
|
||||
authorizedStore.set(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="navbar navbar-expand-sm pt-0">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img
|
||||
src={$settings_public.favicon !== ''
|
||||
? `${dev ? $pocketbase.baseUrl : ''}/api/files/settings_public/${
|
||||
$settings_public.id
|
||||
}/${$settings_public.favicon}`
|
||||
: '/gopher.svg'}
|
||||
alt={$settings_public.website_title ? $settings_public.website_title : 'UpSnap'}
|
||||
width="45"
|
||||
height="45"
|
||||
class:me-2={$settings_public.website_title !== ''}
|
||||
/>{$settings_public.website_title ? $settings_public.website_title : ''}
|
||||
</a>
|
||||
<button
|
||||
class="navbar-toggler border-0"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon" />
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav">
|
||||
{#if userInfo.role !== 'user'}
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
class:active={$page.url.pathname === '/' ? true : false}
|
||||
href="/">Home</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item me-3">
|
||||
<a
|
||||
class="nav-link"
|
||||
class:active={$page.url.pathname === '/settings/' ? true : false}
|
||||
href="/settings/">Settings</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
<li class="nav-item dropdown">
|
||||
<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 shadow-sm mb-2">
|
||||
<li>
|
||||
<div
|
||||
class="dropdown-item"
|
||||
class:active={$theme === 'light'}
|
||||
role="none"
|
||||
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'}
|
||||
role="none"
|
||||
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'}
|
||||
role="none"
|
||||
on:click={() => ($theme = 'auto')}
|
||||
on:keydown={() => ($theme = 'auto')}
|
||||
>
|
||||
<Fa icon={faCircleHalfStroke} class="me-2" />
|
||||
Auto
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="ms-auto d-flex">
|
||||
<button
|
||||
class="text-dark-emphasis nav-link active border-0"
|
||||
data-toggle="tooltip"
|
||||
title="Logged in as {userInfo.role} "{userInfo.usernameOrEmail}""
|
||||
on:click={() => logout()}
|
||||
>
|
||||
<Fa icon={faRightFromBracket} class="me-2" />Logout ({userInfo.usernameOrEmail})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,10 +0,0 @@
|
||||
<script>
|
||||
import { fly } from 'svelte/transition';
|
||||
export let url;
|
||||
</script>
|
||||
|
||||
{#key url}
|
||||
<div in:fly={{ y: 50, duration: 300 }}>
|
||||
<slot />
|
||||
</div>
|
||||
{/key}
|
||||
@@ -1,16 +0,0 @@
|
||||
<script>
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
|
||||
let isUser = false;
|
||||
$: isUser = $pocketbase.authStore.baseModel?.collectionName === 'users';
|
||||
</script>
|
||||
|
||||
{#if isUser}
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col-auto">
|
||||
<div class="callout callout-danger m-0">
|
||||
Logged in as read only user. To make changes, please log in as admin.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
55
frontend/src/lib/components/Navbar.svelte
Normal file
55
frontend/src/lib/components/Navbar.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { pocketbase } from '$lib/stores/pocketbase';
|
||||
import { settingsPub } from '$lib/stores/settings';
|
||||
|
||||
$: console.log($pocketbase);
|
||||
$: avatar = $pocketbase.authStore?.model?.avatar ? $pocketbase.authStore?.model.avatar : '0';
|
||||
|
||||
function logout() {
|
||||
$pocketbase.authStore.clear();
|
||||
goto('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="navbar bg-base-100">
|
||||
<div class="flex-1">
|
||||
<a class="btn btn-ghost normal-case text-xl" href="/">
|
||||
<img
|
||||
src={$settingsPub?.collectionId && $settingsPub?.favicon
|
||||
? `/api/files/settings_public/${$settingsPub?.collectionId}/${$settingsPub?.favicon}`
|
||||
: '/gopher.svg'}
|
||||
alt={$settingsPub?.website_title ? $settingsPub?.website_title : 'UpSnap'}
|
||||
width="45"
|
||||
height="45"
|
||||
/>{$settingsPub?.website_title ? $settingsPub?.website_title : ''}
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
{#if $pocketbase.authStore?.model !== null}
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="-1" class="btn btn-ghost btn-circle avatar" for="avatar">
|
||||
<div class="w-10 rounded-full" id="avatar">
|
||||
<img
|
||||
src="{$pocketbase.baseUrl}/_/images/avatars/avatar{avatar}.svg"
|
||||
alt="Profile pic"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-40"
|
||||
>
|
||||
<li>
|
||||
<a class="justify-between" href="/profile/{$pocketbase.authStore.model?.id}"
|
||||
>Edit profile</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<div on:click={() => logout()} on:keydown={() => logout()} role="none">Logout</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
7
frontend/src/lib/helpers/forms.ts
Normal file
7
frontend/src/lib/helpers/forms.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function toggleVisibility(el: HTMLInputElement) {
|
||||
if (el.type === 'password') {
|
||||
el.type = 'text';
|
||||
} else {
|
||||
el.type = 'password';
|
||||
}
|
||||
}
|
||||
13
frontend/src/lib/stores/pocketbase.ts
Normal file
13
frontend/src/lib/stores/pocketbase.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import PocketBase from 'pocketbase';
|
||||
import type AdminAuthResponse from 'pocketbase';
|
||||
|
||||
// set backend url based on environment
|
||||
export const backendUrl = import.meta.env.DEV ? 'http://127.0.0.1:8090' : '';
|
||||
|
||||
// connect to backend
|
||||
const pb = new PocketBase(backendUrl);
|
||||
pb.autoCancellation(false);
|
||||
|
||||
// export stores
|
||||
export const pocketbase = writable(pb);
|
||||
5
frontend/src/lib/stores/settings.ts
Normal file
5
frontend/src/lib/stores/settings.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { SettingsPublic, SettingsPrivate } from '$lib/types/settings';
|
||||
|
||||
export const settingsPub = writable<SettingsPublic>();
|
||||
export const settingsPriv = writable<SettingsPrivate>();
|
||||
11
frontend/src/lib/types/settings.ts
Normal file
11
frontend/src/lib/types/settings.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type SettingsPublic = {
|
||||
collectionId: string;
|
||||
favicon: string;
|
||||
setup_completed: boolean;
|
||||
website_title: string;
|
||||
};
|
||||
|
||||
export type SettingsPrivate = {
|
||||
interval: number;
|
||||
scan_range: string;
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export const prerender = true;
|
||||
export const trailingSlash = 'always';
|
||||
@@ -1,148 +1,67 @@
|
||||
<script>
|
||||
import { dev } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import Navbar from '@components/Navbar.svelte';
|
||||
import Login from '@components/Login.svelte';
|
||||
import Transition from '@components/Transition.svelte';
|
||||
import { theme } from '@stores/theme';
|
||||
import {
|
||||
pocketbase,
|
||||
authorizedStore,
|
||||
devices,
|
||||
groups,
|
||||
settings_private,
|
||||
settings_public
|
||||
} from '@stores/pocketbase';
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { pocketbase } from '$lib/stores/pocketbase';
|
||||
import { settingsPub } from '$lib/stores/settings';
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
|
||||
let favicon;
|
||||
let preferesDark;
|
||||
let isAuth = false;
|
||||
onMount(async () => {
|
||||
// set settingsPub store on load
|
||||
if (!$settingsPub) {
|
||||
const res = await $pocketbase.collection('settings_public').getList(1, 1);
|
||||
settingsPub.set({
|
||||
collectionId: res.items[0].collectionId,
|
||||
favicon: res.items[0].favicon,
|
||||
setup_completed: res.items[0].setup_completed,
|
||||
website_title: res.items[0].website_title
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// import bootstrap js
|
||||
import('bootstrap/js/dist/dropdown');
|
||||
import('bootstrap/js/dist/collapse');
|
||||
// redirect to welcome page if setup is not completed
|
||||
if (!$settingsPub?.setup_completed && $page.url.pathname !== '/welcome') {
|
||||
goto('/welcome');
|
||||
}
|
||||
|
||||
// set dark mode
|
||||
preferesDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
preferesDark.addEventListener('change', (e) => {
|
||||
if ($theme === 'auto') {
|
||||
document.documentElement.setAttribute(
|
||||
'data-bs-theme',
|
||||
e.matches ? 'dark' : 'light'
|
||||
);
|
||||
}
|
||||
});
|
||||
// load auth from localstorage
|
||||
const pbCookie = localStorage.getItem('pocketbase_auth');
|
||||
if (!pbCookie) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
theme.subscribe((t) => {
|
||||
localStorage.setItem('theme', t);
|
||||
if (t === 'auto') {
|
||||
t = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
document.documentElement.setAttribute('data-bs-theme', t);
|
||||
});
|
||||
$pocketbase.authStore.loadFromCookie('pb_auth=' + pbCookie);
|
||||
if (!$pocketbase.authStore.isValid) {
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
authorizedStore.subscribe((state) => {
|
||||
isAuth = state;
|
||||
});
|
||||
|
||||
// load public settings and subscribe to changes and change favicon
|
||||
const settingsPublicRes = await $pocketbase.collection('settings_public').getList(1, 1);
|
||||
settings_public.set(settingsPublicRes.items[0]);
|
||||
updateSettingsPublic();
|
||||
$pocketbase.collection('settings_public').subscribe('*', function (e) {
|
||||
settings_public.set(e.record);
|
||||
updateSettingsPublic();
|
||||
});
|
||||
|
||||
// load auth from localstorage
|
||||
const pbCookie = localStorage.getItem('pocketbase_auth');
|
||||
if (pbCookie) {
|
||||
$pocketbase.authStore.loadFromCookie('pb_auth=' + pbCookie);
|
||||
|
||||
// try to refresh auth token if valid
|
||||
if ($pocketbase.authStore.isValid) {
|
||||
if ($pocketbase.authStore.model?.collectionName === 'users') {
|
||||
await $pocketbase.collection('users').authRefresh();
|
||||
} else {
|
||||
await $pocketbase.admins.authRefresh();
|
||||
}
|
||||
}
|
||||
authorizedStore.set($pocketbase.authStore.isValid);
|
||||
}
|
||||
});
|
||||
|
||||
function updateSettingsPublic() {
|
||||
favicon.href =
|
||||
$settings_public.favicon === ''
|
||||
? '/gopher.svg'
|
||||
: `${dev ? $pocketbase.baseUrl : ''}/api/files/settings_public/${
|
||||
$settings_public.id
|
||||
}/${$settings_public.favicon}`;
|
||||
document.title =
|
||||
$settings_public.website_title === '' ? 'UpSnap' : $settings_public.website_title;
|
||||
}
|
||||
|
||||
async function getSettingsPrivateAndDevices() {
|
||||
if ($pocketbase.authStore.model?.collectionName !== 'users') {
|
||||
const settingsPrivateRes = await $pocketbase
|
||||
.collection('settings_private')
|
||||
.getList(1, 1);
|
||||
settings_private.set(settingsPrivateRes.items[0]);
|
||||
}
|
||||
|
||||
// get all devices from pb and save in svelte store
|
||||
let tempDevices = {};
|
||||
const devicesRes = await $pocketbase.collection('devices').getFullList(-1, {
|
||||
sort: 'name',
|
||||
expand: 'ports,groups'
|
||||
});
|
||||
devicesRes.forEach((device) => {
|
||||
tempDevices[device.id] = device;
|
||||
});
|
||||
devices.set(tempDevices);
|
||||
|
||||
// get all groups from pb and save in svelte store
|
||||
const groupsRes = await $pocketbase.collection('groups').getFullList(-1, {
|
||||
sort: 'name'
|
||||
});
|
||||
groups.set(groupsRes);
|
||||
}
|
||||
|
||||
$: if (isAuth) {
|
||||
getSettingsPrivateAndDevices();
|
||||
}
|
||||
if ($pocketbase.authStore.model?.collectionName === 'users') {
|
||||
$pocketbase
|
||||
.collection('users')
|
||||
.authRefresh()
|
||||
.catch(() => {
|
||||
$pocketbase.authStore.clear();
|
||||
});
|
||||
} else {
|
||||
$pocketbase.admins.authRefresh().catch(() => {
|
||||
$pocketbase.authStore.clear();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="shortcut icon" href="/gopher.svg" bind:this={favicon} />
|
||||
<script>
|
||||
if (document) {
|
||||
preferesDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
let t = localStorage.getItem('theme');
|
||||
if (!t) {
|
||||
localStorage.setItem('theme', 'auto');
|
||||
t = preferesDark;
|
||||
} else if (t === 'auto') {
|
||||
t = preferesDark;
|
||||
}
|
||||
document.documentElement.setAttribute('data-bs-theme', t);
|
||||
}
|
||||
</script>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href={$settingsPub?.collectionId && $settingsPub?.favicon
|
||||
? `/api/files/settings_public/${$settingsPub?.collectionId}/${$settingsPub?.favicon}`
|
||||
: '/gopher.svg'}
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
{#if isAuth}
|
||||
<Navbar />
|
||||
<Transition url={$page.url}>
|
||||
<slot />
|
||||
</Transition>
|
||||
{:else}
|
||||
<Login />
|
||||
{#if $pocketbase.authStore.isValid}
|
||||
<Navbar />
|
||||
{/if}
|
||||
|
||||
<style lang="scss" global>
|
||||
@import '../scss/main.scss';
|
||||
</style>
|
||||
<slot />
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import DeviceCard from '@components/DeviceCard.svelte';
|
||||
import { pocketbase, devices } from '@stores/pocketbase';
|
||||
import { sortDevices, sortGroups } from '../sorts';
|
||||
|
||||
onMount(async () => {
|
||||
// subscribe to database events
|
||||
$pocketbase.collection('devices').subscribe('*', async (e) => {
|
||||
if (e.action === 'create') {
|
||||
$devices[e.record.id] = e.record;
|
||||
} else if (e.action === 'update') {
|
||||
const device = await $pocketbase.collection('devices').getOne(e.record.id, {
|
||||
expand: 'ports,groups'
|
||||
});
|
||||
$devices[device.id] = device;
|
||||
} else if (e.action === 'delete') {
|
||||
delete $devices[e.record.id];
|
||||
}
|
||||
});
|
||||
$pocketbase.collection('ports').subscribe('*', async (e) => {
|
||||
if (e.action === 'update') {
|
||||
const device = Object.values($devices).find((dev) =>
|
||||
dev.ports.includes(e.record.id)
|
||||
);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
// replace device.expand.ports with updated record
|
||||
const portIdx = device.expand.ports.findIndex((port) => port.id === e.record.id);
|
||||
$devices[device.id].expand.ports[portIdx] = e.record;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let devicesWithoutGroups = [];
|
||||
let devicesWithGroup = {};
|
||||
|
||||
devices.subscribe((d) => {
|
||||
// sort devices into their groups
|
||||
devicesWithoutGroups = [];
|
||||
devicesWithGroup = {};
|
||||
Object.values(d).forEach((device) => {
|
||||
if (device.groups.length === 0) {
|
||||
devicesWithoutGroups = [...devicesWithoutGroups, device];
|
||||
return;
|
||||
}
|
||||
device.expand.groups.forEach((grp) => {
|
||||
if (!devicesWithGroup.hasOwnProperty(grp.name)) {
|
||||
devicesWithGroup[grp.name] = [];
|
||||
}
|
||||
devicesWithGroup[grp.name] = [...devicesWithGroup[grp.name], device];
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// update device date
|
||||
let now = Date.now();
|
||||
let interval;
|
||||
$: {
|
||||
clearInterval(interval);
|
||||
interval = setInterval(() => {
|
||||
now = Date.now();
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container text-body-emphasis mb-4">
|
||||
{#if Object.keys($devices).length > 0}
|
||||
{#if devicesWithoutGroups.length > 0}
|
||||
<div class="row">
|
||||
{#each devicesWithoutGroups.sort(sortDevices) as device}
|
||||
<DeviceCard {device} {now} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each Object.keys(devicesWithGroup).sort(sortGroups) as grp}
|
||||
<h2 class="mt-4 mb-0">{grp}</h2>
|
||||
<div class="row">
|
||||
{#each devicesWithGroup[grp] as device}
|
||||
<DeviceCard {device} {now} />
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-center">
|
||||
<h4 class="text-muted">No devices</h4>
|
||||
<p>
|
||||
<a class="text-muted" href="/settings">Go to settings to add devices...</a>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export function load({ params }) {
|
||||
return { params };
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<script>
|
||||
import { devices } from '@stores/pocketbase';
|
||||
import DeviceForm from '@components/DeviceForm.svelte';
|
||||
import UnauthorizedMsg from '@components/UnauthorizedMsg.svelte';
|
||||
|
||||
export let data;
|
||||
let device;
|
||||
|
||||
$: {
|
||||
device = $devices[data.params.id];
|
||||
if (device && !device?.expand?.ports) {
|
||||
device.expand.ports = [];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if device}
|
||||
<div class="container">
|
||||
<UnauthorizedMsg />
|
||||
<DeviceForm bind:device mode="edit" />
|
||||
</div>
|
||||
{/if}
|
||||
101
frontend/src/routes/login/+page.svelte
Normal file
101
frontend/src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { pocketbase } from '$lib/stores/pocketbase';
|
||||
import Fa from 'svelte-fa';
|
||||
import { faLockOpen, faEye } from '@fortawesome/free-solid-svg-icons';
|
||||
import { toggleVisibility } from '$lib/helpers/forms';
|
||||
|
||||
let inputPassword: HTMLInputElement;
|
||||
let form = {
|
||||
email: '',
|
||||
password: ''
|
||||
};
|
||||
let errorMsg = '';
|
||||
|
||||
function tryAdminThenUser() {
|
||||
errorMsg = '';
|
||||
$pocketbase.admins
|
||||
.authWithPassword(form.email, form.password)
|
||||
.then(() => {
|
||||
goto('/');
|
||||
})
|
||||
.catch(() => {
|
||||
$pocketbase
|
||||
.collection('users')
|
||||
.authWithPassword(form.email, form.password)
|
||||
.then(() => {
|
||||
goto('/');
|
||||
})
|
||||
.catch((err) => {
|
||||
errorMsg = err;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen">
|
||||
<div class="m-auto">
|
||||
<div class="flex flex-col gap-16 w-screen max-w-lg my-4">
|
||||
<div class="card bg-base-300 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-row gap-4">
|
||||
<figure class="w-16"><img src="/gopher.svg" alt="Gopher" /></figure>
|
||||
<h2 class="card-title">Welcome</h2>
|
||||
</div>
|
||||
<form class="form-control w-full" on:submit|preventDefault={tryAdminThenUser}>
|
||||
<label class="label" for="email">
|
||||
<span class="label-text">Email or Username:</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={form.email}
|
||||
/>
|
||||
<label class="label" for="password">
|
||||
<span class="label-text">Password:</span>
|
||||
</label>
|
||||
<label class="relative block">
|
||||
<div
|
||||
class="absolute top-1/2 -translate-y-1/2 right-4 cursor-pointer"
|
||||
role="none"
|
||||
on:click={() => toggleVisibility(inputPassword)}
|
||||
on:keydown={() => toggleVisibility(inputPassword)}
|
||||
>
|
||||
<Fa icon={faEye} />
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
class="input input-bordered w-full"
|
||||
maxlength="72"
|
||||
bind:value={form.password}
|
||||
bind:this={inputPassword}
|
||||
/>
|
||||
</label>
|
||||
{#if errorMsg !== ''}
|
||||
<div class="alert alert-error mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/></svg
|
||||
>
|
||||
<span>{errorMsg}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button class="btn btn-primary" type="submit">Login <Fa icon={faLockOpen} /></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,478 +0,0 @@
|
||||
<script>
|
||||
import { dev } from '$app/environment';
|
||||
import {
|
||||
pocketbase,
|
||||
settings_private,
|
||||
settings_public,
|
||||
devices,
|
||||
groups
|
||||
} from '@stores/pocketbase';
|
||||
import UnauthorizedMsg from '@components/UnauthorizedMsg.svelte';
|
||||
import DeviceForm from '@components/DeviceForm.svelte';
|
||||
import Fa from 'svelte-fa/src/fa.svelte';
|
||||
import { faPlus, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
let version = import.meta.env.UPSNAP_VERSION;
|
||||
|
||||
let iconFiles = [];
|
||||
let iconPreview;
|
||||
let timeout;
|
||||
let buttons = {
|
||||
settings: {
|
||||
state: 'none',
|
||||
error: ''
|
||||
},
|
||||
scan: {
|
||||
state: 'none',
|
||||
error: ''
|
||||
},
|
||||
addAllDevices: {
|
||||
state: 'none',
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
let newDevice = {
|
||||
name: '',
|
||||
ip: '',
|
||||
mac: '',
|
||||
netmask: '255.255.255.0',
|
||||
expand: {
|
||||
ports: []
|
||||
},
|
||||
ports: [],
|
||||
link: '',
|
||||
wake_cron: '',
|
||||
wake_cron_enabled: false,
|
||||
shutdown_cron: '',
|
||||
shutdown_cron_enabled: false,
|
||||
shutdown_cmd: '',
|
||||
password: '',
|
||||
groups: []
|
||||
};
|
||||
let newGroup = '';
|
||||
let scannedDevices = {};
|
||||
|
||||
$: if (iconPreview && iconFiles.length > 0) {
|
||||
iconPreview.src = URL.createObjectURL(iconFiles[0]);
|
||||
} else if (iconPreview && $settings_public.favicon !== '') {
|
||||
iconPreview.src = `${dev ? $pocketbase.baseUrl : ''}/api/files/settings_public/${
|
||||
$settings_public.id
|
||||
}/${$settings_public.favicon}`;
|
||||
} else if (iconPreview) {
|
||||
iconPreview.src = '/gopher.svg';
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
buttons.settings.state = 'waiting';
|
||||
try {
|
||||
// update settings private
|
||||
if ($settings_private.interval === '') {
|
||||
$settings_private.interval = '@every 3s';
|
||||
}
|
||||
await $pocketbase
|
||||
.collection('settings_private')
|
||||
.update($settings_private.id, $settings_private);
|
||||
|
||||
// update settings public
|
||||
if (iconFiles.length > 0) {
|
||||
const formData = new FormData();
|
||||
formData.append('favicon', iconFiles[0]);
|
||||
await $pocketbase
|
||||
.collection('settings_public')
|
||||
.update($settings_public.id, formData);
|
||||
}
|
||||
await $pocketbase
|
||||
.collection('settings_public')
|
||||
.update($settings_public.id, $settings_public);
|
||||
|
||||
//set button
|
||||
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 scanDevices() {
|
||||
buttons.scan.state = 'waiting';
|
||||
await $pocketbase
|
||||
.collection('settings_private')
|
||||
.update($settings_private.id, $settings_private)
|
||||
.catch((err) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
buttons.scan.error = '';
|
||||
buttons.scan.state = 'none';
|
||||
}, 3000);
|
||||
buttons.scan.error = err;
|
||||
buttons.scan.state = 'failed';
|
||||
});
|
||||
|
||||
fetch(`${dev ? 'http://localhost:8090' : ''}/api/upsnap/scan`, {
|
||||
headers: {
|
||||
Authorization: $pocketbase.authStore.baseToken
|
||||
}
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.message) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
buttons.scan.error = '';
|
||||
buttons.scan.state = 'none';
|
||||
}, 3000);
|
||||
buttons.scan.error = data.message;
|
||||
buttons.scan.state = 'failed';
|
||||
} else {
|
||||
scannedDevices = data;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
buttons.scan.state = 'none';
|
||||
}, 3000);
|
||||
buttons.scan.state = 'success';
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
buttons.scan.error = '';
|
||||
buttons.scan.state = 'none';
|
||||
}, 3000);
|
||||
buttons.scan.error = error;
|
||||
buttons.scan.state = 'failed';
|
||||
});
|
||||
}
|
||||
|
||||
async function addAllDevices() {
|
||||
buttons.addAllDevices.state = 'waiting';
|
||||
scannedDevices.devices.forEach(async (device) => {
|
||||
device.netmask = scannedDevices.netmask;
|
||||
await addDevice(device);
|
||||
});
|
||||
buttons.addAllDevices.state = 'success';
|
||||
}
|
||||
|
||||
async function addDevice(device) {
|
||||
await $pocketbase.collection('devices').create(device);
|
||||
}
|
||||
|
||||
async function addGroup() {
|
||||
const res = await $pocketbase.collection('groups').create({
|
||||
name: newGroup
|
||||
});
|
||||
$groups = [...$groups, res];
|
||||
newGroup = '';
|
||||
}
|
||||
|
||||
async function deleteGroup(id) {
|
||||
await $pocketbase.collection('groups').delete(id);
|
||||
const ids = $groups.map((grp) => grp.id);
|
||||
const i = ids.indexOf(id);
|
||||
if (i > -1) {
|
||||
$groups.splice(i, 1);
|
||||
$groups = $groups;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<UnauthorizedMsg />
|
||||
<section class="m-0 mt-4 p-4 shadow-sm">
|
||||
<form on:submit|preventDefault={saveSettings}>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3 class="mb-3 text-body-emphasis">Ping interval</h3>
|
||||
<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_private.interval}
|
||||
/>
|
||||
</div>
|
||||
<h3 class="my-3 text-body-emphasis">Title</h3>
|
||||
<p>Sets the title of the website and in the browser tab.</p>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="website-title">Title</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="e.g. 'UpSnap'"
|
||||
aria-label="Website title"
|
||||
aria-describedby="website-title"
|
||||
bind:value={$settings_public.website_title}
|
||||
/>
|
||||
</div>
|
||||
<h3 class="my-3 text-body-emphasis">Icon</h3>
|
||||
<img
|
||||
src=""
|
||||
alt=""
|
||||
class="img-fluid icon-preview mb-3"
|
||||
bind:this={iconPreview}
|
||||
/>
|
||||
<div class="mb-3">
|
||||
<label for="iconInput" class="form-label">Set a custom favicon.</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
class="form-control"
|
||||
type="file"
|
||||
id="iconInput"
|
||||
accept=".ico,.png,.svg,.gif,.jpg,.jpeg"
|
||||
bind:files={iconFiles}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-danger"
|
||||
on:click={() => {
|
||||
iconFiles = [];
|
||||
$settings_public.favicon = '';
|
||||
}}>Back to Gopher</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info m-0">
|
||||
<h5>Ping interval:</h5>
|
||||
<p>
|
||||
Leave blank to use default value of <span class="fw-bold"
|
||||
>"@every 3s"</span
|
||||
>. Learn more about the correct syntax for cron on
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/Cron"
|
||||
target="_blank"
|
||||
rel="noreferrer">wikipedia</a
|
||||
>
|
||||
or refer to the
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://pkg.go.dev/github.com/robfig/cron"
|
||||
>package documentation</a
|
||||
>
|
||||
</p>
|
||||
<h5>Icon:</h5>
|
||||
<p class="m-0">
|
||||
Supported file types are
|
||||
<span class="badge text-bg-secondary">.ico</span>,
|
||||
<span class="badge text-bg-secondary">.png</span>,
|
||||
<span class="badge text-bg-secondary">.svg</span>,
|
||||
<span class="badge text-bg-secondary">.gif</span> and
|
||||
<span class="badge text-bg-secondary">.jpg/.jpeg</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary"
|
||||
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>
|
||||
<DeviceForm bind:device={newDevice} mode="add" />
|
||||
<section class="m-0 my-4 p-4 shadow-sm">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3 class="mb-3 text-body-emphasis">Manage Groups</h3>
|
||||
<p>
|
||||
{#each $groups as grp}
|
||||
<div class="badge rounded-pill fs-6 text-bg-secondary me-2">
|
||||
{grp.name}
|
||||
<span
|
||||
class="px-1"
|
||||
on:click={() => deleteGroup(grp.id)}
|
||||
on:keydown={() => deleteGroup(grp.id)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<Fa icon={faXmark} />
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</p>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Add new group</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Name for your new group"
|
||||
aria-label="New Group"
|
||||
type="text"
|
||||
bind:value={newGroup}
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
on:click={() => addGroup()}
|
||||
on:keydown={() => addGroup()}
|
||||
>
|
||||
<Fa icon={faPlus} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="m-0 my-4 p-4 shadow-sm">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3 class="mb-3 text-body-emphasis">Network scan</h3>
|
||||
<p>Set the network address to scan.</p>
|
||||
<form on:submit|preventDefault={scanDevices}>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="ip-range">IP range</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="e.g. '192.168.1.0/24'"
|
||||
aria-label="ip-range"
|
||||
aria-describedby="ip-range"
|
||||
bind:value={$settings_private.scan_range}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary"
|
||||
class:btn-success={buttons.scan.state === 'success' ? true : false}
|
||||
class:btn-warning={buttons.scan.state === 'waiting' ? true : false}
|
||||
class:btn-danger={buttons.scan.state === 'failed' ? true : false}
|
||||
disabled={$settings_private.scan_range === '' ||
|
||||
buttons.scan.state !== 'none'
|
||||
? true
|
||||
: false}
|
||||
>
|
||||
{#if buttons.scan.state === 'none'}
|
||||
Scan
|
||||
{:else if buttons.scan.state === 'success'}
|
||||
Scanned
|
||||
{:else if buttons.scan.state === 'waiting'}
|
||||
Scan running (might take some seconds)
|
||||
{:else if buttons.scan.state === 'failed'}
|
||||
Failed: {buttons.scan.error}
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info mb-0">
|
||||
<p class="m-0">
|
||||
For network scan to work, you need to run UpSnap as root/admin and have nmap
|
||||
installed and available in your $PATH. (For docker users, thats already the
|
||||
case and you don't need to do anything.)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if scannedDevices.devices}
|
||||
<div class="mt-3">
|
||||
<div class="row align-items-center my-1">
|
||||
<div class="col-lg-3 fw-bold">Name</div>
|
||||
<div class="col-lg-2 fw-bold">IP</div>
|
||||
<div class="col-lg-2 fw-bold">MAC</div>
|
||||
<div class="col-lg-2 fw-bold">MAC Vendor</div>
|
||||
<div class="col-lg-2 fw-bold">Netmask</div>
|
||||
<div class="col-lg-1 fw-bold">Add</div>
|
||||
</div>
|
||||
{#each scannedDevices?.devices as device}
|
||||
<hr class="my-1" />
|
||||
<div class="row align-items-center my-1">
|
||||
<div class="col-lg-3">
|
||||
{device.name}
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
{device.ip}
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
{device.mac}
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
{device.mac_vendor}
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
{scannedDevices.netmask}
|
||||
</div>
|
||||
<div class="col-lg-1">
|
||||
<button
|
||||
class="btn btn-sm btn-secondary py-0"
|
||||
on:click={async (e) => {
|
||||
e.target.disabled = true;
|
||||
device.netmask = scannedDevices.netmask;
|
||||
await addDevice(device);
|
||||
}}
|
||||
on:keydown={async (e) => {
|
||||
e.target.disabled = true;
|
||||
device.netmask = scannedDevices.netmask;
|
||||
await addDevice(device);
|
||||
}}
|
||||
>
|
||||
Add <Fa icon={faPlus} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-secondary mt-3"
|
||||
class:btn-success={buttons.addAllDevices.state === 'success' ? true : false}
|
||||
class:btn-warning={buttons.addAllDevices.state === 'waiting' ? true : false}
|
||||
on:click={async (e) => {
|
||||
e.target.disabled = true;
|
||||
await addAllDevices();
|
||||
}}
|
||||
on:keydown={async (e) => {
|
||||
e.target.disabled = true;
|
||||
await addAllDevices();
|
||||
}}
|
||||
>
|
||||
{#if buttons.addAllDevices.state === 'success'}
|
||||
Added
|
||||
{:else if buttons.addAllDevices.state === 'waiting'}
|
||||
Adding all...
|
||||
{:else}
|
||||
Add all <Fa icon={faPlus} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</section>
|
||||
<p class="m-0 my-4 p-4 text-center text-muted">
|
||||
{#if version !== undefined}
|
||||
UpSnap version: <a
|
||||
href="https://github.com/seriousm4x/UpSnap/releases/tag/{version}"
|
||||
class="text-reset">{version}</a
|
||||
>
|
||||
{:else}
|
||||
UpSnap version: (untracked)
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.icon-preview {
|
||||
height: 130px;
|
||||
}
|
||||
</style>
|
||||
181
frontend/src/routes/welcome/+page.svelte
Normal file
181
frontend/src/routes/welcome/+page.svelte
Normal file
@@ -0,0 +1,181 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { pocketbase } from '$lib/stores/pocketbase';
|
||||
import { settingsPub } from '$lib/stores/settings';
|
||||
import { toggleVisibility } from '$lib/helpers/forms';
|
||||
import Fa from 'svelte-fa';
|
||||
import { faArrowRight, faEye } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
let stepsCompleted = 0;
|
||||
let inputPassword: HTMLInputElement;
|
||||
let inputConfirm: HTMLInputElement;
|
||||
let form = {
|
||||
email: '',
|
||||
password: '',
|
||||
confirm: ''
|
||||
};
|
||||
let errorMsg = '';
|
||||
|
||||
async function register() {
|
||||
errorMsg = '';
|
||||
$pocketbase.admins
|
||||
.create({
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
passwordConfirm: form.confirm
|
||||
})
|
||||
.then(() => {
|
||||
$pocketbase.admins
|
||||
.authWithPassword(form.email, form.password)
|
||||
.then(() => {
|
||||
stepsCompleted = 2;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.data?.data?.identity?.message) {
|
||||
errorMsg = err.data.data.identity.message;
|
||||
return;
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.data?.data?.passwordConfirm?.message) {
|
||||
errorMsg = "Passwords don't match.";
|
||||
} else if (err.data?.data?.email?.message) {
|
||||
errorMsg = err.data.data.email.message;
|
||||
} else {
|
||||
errorMsg = err;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen">
|
||||
<div class="m-auto">
|
||||
<div class="flex flex-col gap-16 w-screen max-w-lg my-4">
|
||||
<div class="card bg-base-300 shadow-xl">
|
||||
{#if $settingsPub?.setup_completed}
|
||||
<figure class="w-72 mx-auto pt-6"><img src="/gopher.svg" alt="Gopher" /></figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">I didn't expect you here! 🧐</h2>
|
||||
<p>You are already done with the setup! Nothing to do.</p>
|
||||
<div class="card-actions justify-end">
|
||||
<button class="btn btn-primary" on:click={() => goto('/')}>Take me back</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if stepsCompleted === 0}
|
||||
<figure class="w-72 mx-auto pt-6"><img src="/gopher.svg" alt="Gopher" /></figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Welcome to UpSnap 🥳</h2>
|
||||
<p>Please complete the following steps to finish the setup.</p>
|
||||
<div class="card-actions justify-end">
|
||||
<button class="btn btn-primary" on:click={() => (stepsCompleted = 1)}
|
||||
>Next <Fa icon={faArrowRight} /></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if stepsCompleted === 1}
|
||||
<div class="card-body">
|
||||
<div class="flex flex-row gap-4">
|
||||
<figure class="w-16"><img src="/gopher.svg" alt="Gopher" /></figure>
|
||||
<h2 class="card-title">Create an admin account</h2>
|
||||
</div>
|
||||
<form class="form-control w-full" on:submit|preventDefault={register}>
|
||||
<label class="label" for="email">
|
||||
<span class="label-text">Email:</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={form.email}
|
||||
/>
|
||||
<label class="label" for="password">
|
||||
<span class="label-text">Password:</span>
|
||||
<span class="label-text-alt">min. 10 characters</span>
|
||||
</label>
|
||||
<label class="relative block">
|
||||
<div
|
||||
class="absolute top-1/2 -translate-y-1/2 right-4 cursor-pointer"
|
||||
role="none"
|
||||
on:click={() => toggleVisibility(inputPassword)}
|
||||
on:keydown={() => toggleVisibility(inputPassword)}
|
||||
>
|
||||
<Fa icon={faEye} />
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
class="input input-bordered w-full"
|
||||
minlength="10"
|
||||
maxlength="72"
|
||||
bind:value={form.password}
|
||||
bind:this={inputPassword}
|
||||
/>
|
||||
</label>
|
||||
<label class="label" for="passwordConfirm">
|
||||
<span class="label-text">Password confirm:</span>
|
||||
</label>
|
||||
<label class="relative block">
|
||||
<div
|
||||
class="absolute top-1/2 -translate-y-1/2 right-4 cursor-pointer"
|
||||
role="none"
|
||||
on:click={() => toggleVisibility(inputConfirm)}
|
||||
on:keydown={() => toggleVisibility(inputConfirm)}
|
||||
>
|
||||
<Fa icon={faEye} />
|
||||
</div>
|
||||
<input
|
||||
id="confirm"
|
||||
type="password"
|
||||
class="input input-bordered w-full"
|
||||
minlength="10"
|
||||
maxlength="72"
|
||||
bind:value={form.confirm}
|
||||
bind:this={inputConfirm}
|
||||
/>
|
||||
</label>
|
||||
{#if errorMsg !== ''}
|
||||
<div class="alert alert-error mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/></svg
|
||||
>
|
||||
<span>{errorMsg}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button class="btn btn-primary" type="submit"
|
||||
>Create <Fa icon={faArrowRight} /></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{:else if stepsCompleted === 2}
|
||||
<figure class="w-72 mx-auto pt-6"><img src="/gopher.svg" alt="Gopher" /></figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">You are all set! 🎉</h2>
|
||||
<p>Go ahead and add some devices to your dashboard.</p>
|
||||
<div class="card-actions justify-end">
|
||||
<button class="btn btn-success" on:click={() => goto('/')}>Lets go!</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !$settingsPub?.setup_completed}
|
||||
<ul class="steps steps-horizontal">
|
||||
<li class="step step-primary">Welcome</li>
|
||||
<li class="step" class:step-primary={stepsCompleted > 0}>Create account</li>
|
||||
<li class="step" class:step-primary={stepsCompleted > 1}>Done</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,162 +0,0 @@
|
||||
// import bootstrap functions and variables
|
||||
@import '../../node_modules/bootstrap/scss/functions';
|
||||
@import '../../node_modules/bootstrap/scss/variables';
|
||||
@import '../../node_modules/bootstrap/scss/variables-dark';
|
||||
|
||||
// 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
|
||||
) !default;
|
||||
|
||||
$body-bg-dark: $gray-1000;
|
||||
$dropdown-link-active-bg: $gray-1000;
|
||||
$dropdown-min-width: 0;
|
||||
$form-check-input-checked-bg-color: $orange-300;
|
||||
$form-check-input-checked-border-color: $orange-500;
|
||||
$input-focus-border-color: $gray-500;
|
||||
$input-focus-box-shadow: $gray-900;
|
||||
|
||||
// import rest
|
||||
@import '../../node_modules/bootstrap/scss/maps';
|
||||
@import '../../node_modules/bootstrap/scss/mixins';
|
||||
@import '../../node_modules/bootstrap/scss/root';
|
||||
@import '../../node_modules/bootstrap/scss/utilities';
|
||||
@import '../../node_modules/bootstrap/scss/reboot';
|
||||
@import '../../node_modules/bootstrap/scss/type';
|
||||
@import '../../node_modules/bootstrap/scss/images';
|
||||
@import '../../node_modules/bootstrap/scss/containers';
|
||||
@import '../../node_modules/bootstrap/scss/grid';
|
||||
@import '../../node_modules/bootstrap/scss/helpers';
|
||||
@import '../../node_modules/bootstrap/scss/card';
|
||||
@import '../../node_modules/bootstrap/scss/nav';
|
||||
@import '../../node_modules/bootstrap/scss/navbar';
|
||||
@import '../../node_modules/bootstrap/scss/forms';
|
||||
@import '../../node_modules/bootstrap/scss/dropdown';
|
||||
@import '../../node_modules/bootstrap/scss/buttons';
|
||||
@import '../../node_modules/bootstrap/scss/spinners';
|
||||
@import '../../node_modules/bootstrap/scss/badge';
|
||||
@import '../../node_modules/bootstrap/scss/transitions';
|
||||
|
||||
@import '../../node_modules/bootstrap/scss/utilities/api';
|
||||
|
||||
section {
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 0.3rem 0.6rem !important;
|
||||
border-radius: 0.4rem;
|
||||
|
||||
&.active {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.text-success,
|
||||
.port-up {
|
||||
color: $teal-500 !important;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: $yellow !important;
|
||||
}
|
||||
|
||||
.text-danger,
|
||||
.port-down {
|
||||
color: #ef476f !important;
|
||||
}
|
||||
|
||||
.callout {
|
||||
padding: 1rem;
|
||||
border-radius: 0.3rem;
|
||||
margin: 1rem 0;
|
||||
|
||||
&.callout-info {
|
||||
background-color: #1177b21f;
|
||||
border-left: 0.5rem solid #1177b21f;
|
||||
}
|
||||
|
||||
&.callout-danger {
|
||||
background-color: #ef476f1f;
|
||||
border-left: 0.5rem solid #ef476f1f;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.power-hover {
|
||||
&:hover {
|
||||
filter: drop-shadow(0px 0px 10px rgb(155, 155, 155));
|
||||
transition: all 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
width: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-bs-theme='light'] {
|
||||
html,
|
||||
body {
|
||||
background: #f0f1f2;
|
||||
}
|
||||
|
||||
section,
|
||||
.card,
|
||||
.login {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-bs-theme='dark'] {
|
||||
html,
|
||||
body {
|
||||
background: #131316;
|
||||
}
|
||||
|
||||
section,
|
||||
.card,
|
||||
.login {
|
||||
background: #25252b;
|
||||
}
|
||||
|
||||
.nav-link,
|
||||
.dropdown-menu {
|
||||
&.active {
|
||||
background-color: $gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background-color: $gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export function sortDevices(a, b) {
|
||||
return a.name.localeCompare(b.name, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
});
|
||||
}
|
||||
|
||||
export function sortGroups(a, b) {
|
||||
return a.localeCompare(b, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
});
|
||||
}
|
||||
|
||||
export function sortPorts(a, b) {
|
||||
return a.number - b.number;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
// set backend url based on environment
|
||||
let backend_url = import.meta.env.DEV ? 'http://127.0.0.1:8090' : '/';
|
||||
|
||||
// get default values for stores
|
||||
const pb = new PocketBase(backend_url);
|
||||
pb.autoCancellation(false);
|
||||
|
||||
// export stores
|
||||
export const pocketbase = writable(pb);
|
||||
export const settings_private = writable({
|
||||
interal: '',
|
||||
scan_range: ''
|
||||
});
|
||||
export const settings_public = writable({
|
||||
website_title: '',
|
||||
favicon: ''
|
||||
});
|
||||
export const devices = writable({});
|
||||
export const groups = writable([]);
|
||||
export const authorizedStore = writable(false);
|
||||
@@ -1,5 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const theme = writable();
|
||||
if (typeof window !== 'undefined')
|
||||
[theme.set(localStorage.getItem('theme') ? localStorage.getItem('theme') : 'auto')];
|
||||
@@ -1,25 +1,18 @@
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import preprocess from 'svelte-preprocess';
|
||||
import path from 'path';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: preprocess(),
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
fallback: 'index.html'
|
||||
}),
|
||||
alias: {
|
||||
'@components': path.resolve('./src/components'),
|
||||
'@stores': path.resolve('./src/stores')
|
||||
},
|
||||
prerender: {
|
||||
entries: ['/device/[id]', '/settings', '/']
|
||||
}
|
||||
}
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
9
frontend/tailwind.config.js
Normal file
9
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: [require('daisyui')],
|
||||
daisyui: {}
|
||||
};
|
||||
17
frontend/tsconfig.json
Normal file
17
frontend/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
@@ -2,6 +2,5 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
envPrefix: 'UPSNAP'
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
Reference in New Issue
Block a user