mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-03-31 06:24:11 -04:00
397 lines
16 KiB
Svelte
397 lines
16 KiB
Svelte
<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, 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 {
|
|
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 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-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) Read more about valid cron syntax on <a
|
|
href="https://en.wikipedia.org/wiki/Cron"
|
|
target="_blank"
|
|
rel="noreferrer">wikipedia</a
|
|
>
|
|
or the package documentation
|
|
<a
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
href="https://pkg.go.dev/github.com/robfig/cron"
|
|
>pkg.go.dev/github.com/robfig/cron</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>
|