mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-07-30 01:13:16 -04:00
device settings page and other improvements
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -199,3 +199,4 @@ frontend/vite.config.ts.timestamp-*
|
||||
|
||||
backend/pb_data
|
||||
backend/pb_public/*
|
||||
!backend/pb_public/.gitkeep
|
||||
|
||||
@@ -32,7 +32,7 @@ Open up [http://localhost:5173/](http://localhost:5173/)
|
||||
|
||||
- [x] ~~add rest of settings page~~
|
||||
- [x] ~~form for adding new devices~~
|
||||
- [ ] add per device settings
|
||||
- [x] ~~add per device settings~~
|
||||
- [x] ~~theme toggle button~~
|
||||
- [x] ~~add device ports to cards~~
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ func RunCron(app *pocketbase.PocketBase) {
|
||||
// init cronjob
|
||||
Jobs = cron.New()
|
||||
Jobs.AddFunc(settingsRecords[0].GetString("interval"), func() {
|
||||
// skip cron if no realtime clients connected
|
||||
realtimeClients := len(app.SubscriptionsBroker().Clients())
|
||||
if realtimeClients == 0 {
|
||||
return
|
||||
}
|
||||
// expand ports field
|
||||
expandFetchFunc := func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
||||
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
||||
|
||||
0
backend/pb_public/.gitkeep
Normal file
0
backend/pb_public/.gitkeep
Normal file
BIN
backend/upsnap
Executable file
BIN
backend/upsnap
Executable file
Binary file not shown.
@@ -4,6 +4,7 @@
|
||||
<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">
|
||||
|
||||
@@ -61,7 +61,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-auto fs-5">
|
||||
<Fa icon={faEllipsisVertical} />
|
||||
<a href="/device/{device.id}" class="text-reset text-center ellipsis power-hover">
|
||||
<Fa icon={faEllipsisVertical} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{#if device.link}
|
||||
|
||||
350
frontend/src/components/DeviceForm.svelte
Normal file
350
frontend/src/components/DeviceForm.svelte
Normal file
@@ -0,0 +1,350 @@
|
||||
<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';
|
||||
|
||||
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';
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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 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 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>
|
||||
</section>
|
||||
@@ -8,7 +8,7 @@
|
||||
<nav class="navbar navbar-expand">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="favicon.png" alt="UpSnap" width="30" height="30" />
|
||||
<img src="/favicon.png" alt="UpSnap" width="30" height="30" />
|
||||
</a>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
@@ -31,8 +31,8 @@
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<Fa icon={faBrush} class="me-2" />
|
||||
Theme
|
||||
<Fa icon={faBrush} class="me-2" />
|
||||
Theme
|
||||
</div>
|
||||
<ul class="dropdown-menu border-0 p-1">
|
||||
<li>
|
||||
|
||||
6
frontend/src/routes/device/[id]/+page.js
Normal file
6
frontend/src/routes/device/[id]/+page.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const prerender = true;
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export function load({ params }) {
|
||||
return { params }
|
||||
}
|
||||
27
frontend/src/routes/device/[id]/+page.svelte
Normal file
27
frontend/src/routes/device/[id]/+page.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script>
|
||||
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
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
pocketbase.subscribe((conn) => {
|
||||
pb = conn;
|
||||
});
|
||||
|
||||
const result = await pb.collection('devices').getOne(id, {
|
||||
expand: 'ports'
|
||||
});
|
||||
device = result;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<DeviceForm bind:device mode="edit" />
|
||||
</div>
|
||||
@@ -1,18 +1,14 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
import DeviceForm from '@components/DeviceForm.svelte';
|
||||
|
||||
let pb;
|
||||
let files;
|
||||
let settings = {};
|
||||
let timeout;
|
||||
let buttons = {
|
||||
add_device: {
|
||||
state: 'none',
|
||||
error: ''
|
||||
},
|
||||
saved: {
|
||||
settings: {
|
||||
state: 'none',
|
||||
error: ''
|
||||
},
|
||||
@@ -26,6 +22,10 @@
|
||||
ip: '',
|
||||
mac: '',
|
||||
netmask: '255.255.255.0',
|
||||
expand: {
|
||||
ports: []
|
||||
},
|
||||
ports: [],
|
||||
link: '',
|
||||
wake_cron: '',
|
||||
shutdown_cron: '',
|
||||
@@ -33,9 +33,6 @@
|
||||
password: ''
|
||||
};
|
||||
|
||||
let passwordShow = false;
|
||||
$: passwordType = passwordShow ? 'text' : 'password';
|
||||
|
||||
onMount(async () => {
|
||||
pocketbase.subscribe((conn) => {
|
||||
pb = conn;
|
||||
@@ -45,53 +42,29 @@
|
||||
settings = result.items[0];
|
||||
});
|
||||
|
||||
async function addDevice() {
|
||||
buttons.add_device.state = 'waiting';
|
||||
try {
|
||||
if (
|
||||
newDevice.password.length != 0 &&
|
||||
newDevice.password.length != 4 &&
|
||||
newDevice.password.length != 6
|
||||
) {
|
||||
throw 'Password must be 0, 4 or 6 characters long';
|
||||
}
|
||||
await pb.collection('devices').create(newDevice, { $autoCancel: false });
|
||||
setTimeout(() => {
|
||||
buttons.add_device.state = 'none';
|
||||
}, 3000);
|
||||
buttons.add_device.state = 'success';
|
||||
} catch (error) {
|
||||
setTimeout(() => {
|
||||
buttons.add_device.error = '';
|
||||
buttons.add_device.state = 'none';
|
||||
}, 3000);
|
||||
buttons.add_device.error = error;
|
||||
buttons.add_device.state = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
buttons.saved.state = 'waiting';
|
||||
buttons.settings.state = 'waiting';
|
||||
try {
|
||||
await pb.collection('settings').update(
|
||||
settings.id,
|
||||
{
|
||||
interval: settings.interval,
|
||||
notifications: settings.notifications
|
||||
},
|
||||
{ $autoCancel: false }
|
||||
);
|
||||
setTimeout(() => {
|
||||
buttons.saved.state = 'none';
|
||||
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.saved.state = 'success';
|
||||
buttons.settings.state = 'success';
|
||||
} catch (error) {
|
||||
setTimeout(() => {
|
||||
buttons.saved.error = '';
|
||||
buttons.saved.state = 'none';
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
buttons.settings.error = '';
|
||||
buttons.settings.state = 'none';
|
||||
}, 3000);
|
||||
buttons.saved.error = error;
|
||||
buttons.saved.state = 'failed';
|
||||
buttons.settings.error = error;
|
||||
buttons.settings.state = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,30 +100,24 @@
|
||||
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
|
||||
},
|
||||
{ $autoCancel: false }
|
||||
);
|
||||
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
|
||||
},
|
||||
{ $autoCancel: false }
|
||||
);
|
||||
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(() => {
|
||||
@@ -166,171 +133,10 @@
|
||||
buttons.restore.state = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
function onPasswordInput(event) {
|
||||
newDevice.password = event.target.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<section class="m-0 mt-4 p-4 shadow-sm">
|
||||
<h3 class="mb-3">Add new device</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form on:submit|preventDefault={addDevice}>
|
||||
<h5>Required:</h5>
|
||||
<div class="input-group mb-3">
|
||||
<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={newDevice.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<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={newDevice.ip}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<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={newDevice.mac}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<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={newDevice.netmask}
|
||||
/>
|
||||
</div>
|
||||
<h5 class="mt-4">Optional:</h5>
|
||||
<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={newDevice.link}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<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={newDevice.wake_cron}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<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={newDevice.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={newDevice.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={newDevice.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={buttons.add_device.state === 'success' ? true : false}
|
||||
class:btn-warning={buttons.add_device.state === 'waiting' ? true : false}
|
||||
class:btn-danger={buttons.add_device.state === 'failed' ? true : false}
|
||||
>
|
||||
{#if buttons.add_device.state === 'none'}
|
||||
Add device
|
||||
{:else if buttons.add_device.state === 'success'}
|
||||
Added successfully
|
||||
{:else if buttons.add_device.state === 'waiting'}
|
||||
Waiting
|
||||
{:else if buttons.add_device.state === 'failed'}
|
||||
Failed: {buttons.add_device.error}
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="callout callout-info mb-0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
@@ -380,18 +186,19 @@
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary mt-3"
|
||||
class:btn-success={buttons.saved.state === 'success' ? true : false}
|
||||
class:btn-warning={buttons.saved.state === 'waiting' ? true : false}
|
||||
class:btn-danger={buttons.saved.state === 'failed' ? true : false}
|
||||
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.saved.state === 'none'}
|
||||
{#if buttons.settings.state === 'none'}
|
||||
Save
|
||||
{:else if buttons.saved.state === 'success'}
|
||||
{:else if buttons.settings.state === 'success'}
|
||||
Saved
|
||||
{:else if buttons.saved.state === 'waiting'}
|
||||
{:else if buttons.settings.state === 'waiting'}
|
||||
Waiting
|
||||
{:else if buttons.saved.state === 'failed'}
|
||||
Failed: {buttons.add_device.error}
|
||||
{:else if buttons.settings.state === 'failed'}
|
||||
Failed: {buttons.settings.error}
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -101,6 +101,11 @@ section {
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
width: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-bs-theme='light'] {
|
||||
html,
|
||||
body {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export let pocketbase = writable(new PocketBase('http://127.0.0.1:8090'));
|
||||
const pb = new PocketBase('http://127.0.0.1:8090')
|
||||
pb.autoCancellation(false)
|
||||
|
||||
export let pocketbase = writable(pb);
|
||||
|
||||
@@ -13,6 +13,9 @@ const config = {
|
||||
alias: {
|
||||
'@components': path.resolve('./src/components'),
|
||||
'@stores': path.resolve('./src/stores')
|
||||
},
|
||||
prerender: {
|
||||
entries: ['/device/[id]']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user