working nice. docker uses bun
@@ -55,6 +55,7 @@ trigger:
|
||||
- pull_request
|
||||
branch:
|
||||
- main
|
||||
- update
|
||||
|
||||
depends_on:
|
||||
- Build
|
||||
@@ -117,6 +118,7 @@ trigger:
|
||||
- pull_request
|
||||
branch:
|
||||
- main
|
||||
- update
|
||||
|
||||
depends_on:
|
||||
- Build
|
||||
@@ -127,6 +129,6 @@ volumes:
|
||||
temp: {}
|
||||
---
|
||||
kind: signature
|
||||
hmac: bea117f5e4b51c4fc215ae86962a8cfb24993d9e9b7db3498ab5940b10c70d69
|
||||
hmac: 83b1ec6458ceddef038000faac2b391d192530ceb681b790ad79c90456f12cca
|
||||
|
||||
...
|
||||
|
||||
@@ -21,32 +21,9 @@ spec:
|
||||
- image: ${IMAGE}
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: infra-map
|
||||
env:
|
||||
- name: HOMEASSISTANT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: secret-env-values
|
||||
key: HOMEASSISTANT_TOKEN
|
||||
- name: HOMEASSISTANT_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret-env-values
|
||||
key: HOMEASSISTANT_URL
|
||||
- name: PROXMOX_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret-env-values
|
||||
key: PROXMOX_TOKEN
|
||||
- name: PROXMOX_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret-env-values
|
||||
key: PROXMOX_URL
|
||||
- name: TRAEFIK_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret-env-values
|
||||
key: TRAEFIK_URL
|
||||
resources:
|
||||
limits:
|
||||
cpu: 900m
|
||||
|
||||
@@ -20,4 +20,4 @@ RUN yarn --production
|
||||
EXPOSE 3000
|
||||
ENV NODE_ENV=production
|
||||
|
||||
CMD [ "node", "build" ]
|
||||
CMD [ "node", "build/index.js" ]
|
||||
|
||||
60
README.md
@@ -1,19 +1,61 @@
|
||||
# sv
|
||||
# infra-map
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
## Configuration setup
|
||||
|
||||
## Creating a project
|
||||
set the following env variables during runtime or update `.env` file.
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
### Kubernetes
|
||||
|
||||
Required parameters:
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
KUBERNETES_SERVICE_HOST=https://IP_ADDRESS:16443
|
||||
KUBERNETES_CA_CART_PATH=kube-ca.crt
|
||||
KUBERNETES_SA_TOKEN=LKdgk34l...
|
||||
```
|
||||
|
||||
The `KUBERNETES_SERVICE_HOST` is the api server, [microk8s documentation](https://microk8s.io/docs/services-and-ports#services-binding-to-the-default-host-interface) describes that API server is running at port `16443`. Runtime in a pod this will be set by kubernetes.
|
||||
|
||||
Get ca_cert from the any running pod at: `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.
|
||||
|
||||
### Proxmox
|
||||
|
||||
Required parameters:
|
||||
|
||||
```bash
|
||||
PROXMOX_URL=https://apollo.schleppe:8086/api2/json/
|
||||
PROXMOX_TOKEN=PVEAPITOKEN=USER@pve!USER=TOKEN_VALUE
|
||||
```
|
||||
|
||||
`PROXMOX_TOKEN` should contain the sub-variable `PVEAPITOKEN` that describes a proxmox API token.
|
||||
|
||||
Create api token:
|
||||
|
||||
- Create Users
|
||||
- user name: infra-map
|
||||
- realm: pve
|
||||
- expire: never
|
||||
- Add API tokens
|
||||
- user: infra-map@pve
|
||||
- token ID: infra-map
|
||||
- Permissions
|
||||
- add: API Token permissions
|
||||
- path: /
|
||||
- api-token: infra-map@pve!infra-map
|
||||
- role: Administrator
|
||||
- propagate: true
|
||||
|
||||
## Home Assistant
|
||||
|
||||
Required parameters:
|
||||
|
||||
```bash
|
||||
HOMEASSISTANT_URL=http://homeassistant.schleppe:8123/api/states
|
||||
HOMEASSISTANT_TOKEN=
|
||||
```
|
||||
|
||||
Follow hass documentation on generating a api token: https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token.
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^1.1.0",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@zerodevx/svelte-json-view": "^1.0.11",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"sveltekit-sse": "^0.13.16"
|
||||
}
|
||||
}
|
||||
|
||||
88
src/lib/components/ColorInput.svelte
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
export let label: string;
|
||||
export let value = '#000000';
|
||||
export let placeholder: string | null = null;
|
||||
export let required = false;
|
||||
export let icon: unknown = null;
|
||||
|
||||
let focus = false;
|
||||
</script>
|
||||
|
||||
<div class="label-input">
|
||||
{#if label?.length > 0}
|
||||
<label for={label}>{label}</label>
|
||||
{/if}
|
||||
|
||||
<div class="input" class:focus style={`--bg: ${value}`}>
|
||||
<div class="color-preview"></div>
|
||||
|
||||
<input
|
||||
id={label}
|
||||
{placeholder}
|
||||
name={label}
|
||||
style={`background-color: ${value}30`}
|
||||
bind:value
|
||||
{required}
|
||||
on:focus={() => (focus = true)}
|
||||
on:blur={() => (focus = false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.label-input {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background-color: var(--bg);
|
||||
border-radius: inherit;
|
||||
border-bottom-left-radius: unset;
|
||||
border-bottom-right-radius: unset;
|
||||
}
|
||||
|
||||
.input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 4rem;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 1px solid #a68b85;
|
||||
border-color: var(--bg);
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s;
|
||||
outline: none;
|
||||
|
||||
&.focus {
|
||||
box-shadow: 0px 0px 0px 4px #7d66654d;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
touch-action: manipulation;
|
||||
background: transparent;
|
||||
color: var(--color);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
--padding: 1rem;
|
||||
width: calc(100% - var(--padding));
|
||||
padding-left: var(--padding);
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,11 +8,12 @@
|
||||
|
||||
const healthy =
|
||||
status?.desiredNumberScheduled && status?.desiredNumberScheduled === status?.numberReady;
|
||||
const daemonUrl = `/cluster/daemonset/${metadata?.uid}`;
|
||||
</script>
|
||||
|
||||
<div class="card-container">
|
||||
<div class="namespace">
|
||||
<h2>{pods?.length} of {metadata?.name} in {metadata?.namespace}</h2>
|
||||
<h2>{pods?.length} of <a href={daemonUrl}>{metadata?.name}</a> in {metadata?.namespace}</h2>
|
||||
</div>
|
||||
|
||||
<p>heatlthy: {healthy}</p>
|
||||
@@ -28,7 +29,7 @@
|
||||
.card-container {
|
||||
background-color: #cab2aa40;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
width: calc(100% - 1.5rem);
|
||||
padding: 0.75rem;
|
||||
|
||||
.namespace {
|
||||
@@ -38,8 +39,8 @@
|
||||
|
||||
.card-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
grid-template-columns: var(--grid-tmpl-cols, repeat(3, 1fr));
|
||||
gap: var(--grid-gap, 2rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,16 @@
|
||||
export let deploy: V1Deployment;
|
||||
|
||||
let { metadata, pods } = deploy;
|
||||
|
||||
const deploymentUrl = `/cluster/deployment/${metadata?.uid}`;
|
||||
</script>
|
||||
|
||||
<div class="card-container">
|
||||
<div class="namespace">
|
||||
<h2>{metadata?.name} in {metadata?.namespace}</h2>
|
||||
<h2>
|
||||
<a href={deploymentUrl}>{metadata?.name}</a> in
|
||||
{metadata?.namespace}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-wrapper">
|
||||
@@ -34,8 +39,8 @@
|
||||
|
||||
.card-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
grid-template-columns: var(--grid-tmpl-cols, repeat(3, 1fr));
|
||||
gap: var(--grid-gap, 2rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
174
src/lib/components/Dialog.svelte
Normal file
@@ -0,0 +1,174 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
||||
import { clickOutside } from '$lib/utils/mouseEvents';
|
||||
|
||||
export let title: string;
|
||||
export let description: string | null = null;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const close = () => dispatch('close');
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
const element = document.getElementsByClassName('dialog')?.[0]?.children[0];
|
||||
|
||||
if (clickOutside(event, element) === false) return;
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
setTimeout(() => window.addEventListener('click', handleClick), 100);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
window.removeEventListener('click', handleClick);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="dialog"
|
||||
aria-labelledby="dialog-title"
|
||||
aria-describedby="dialog-description"
|
||||
class="dialog"
|
||||
>
|
||||
<div tabindex="-1" id="dialog-title" class="title">
|
||||
<header>
|
||||
<button on:click={close} aria-disabled="false" aria-label="Close" type="button" tabindex="0"
|
||||
><svg viewBox="0 0 24 24" aria-hidden="true" tabindex="-1" height="100%" width="100%"
|
||||
><path
|
||||
d="M6.909 5.636a.9.9 0 1 0-1.273 1.273l5.091 5.09-5.091 5.092a.9.9 0 0 0 1.273 1.273L12 13.273l5.091 5.09a.9.9 0 1 0 1.273-1.272L13.273 12l5.09-5.091a.9.9 0 1 0-1.272-1.273L12 10.727z"
|
||||
></path></svg
|
||||
>
|
||||
</button>
|
||||
<h5>{title}</h5>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="dialog-description">
|
||||
{#if description}
|
||||
{@html description}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="alerts">
|
||||
<Success>There are no applications created yet.</Success>
|
||||
<Warning>There are no applications created yet.</Warning>
|
||||
<Error>There are no applications created yet.</Error>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.alerts > *) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2006;
|
||||
width: 100%;
|
||||
pointer-events: all;
|
||||
background-color: #7d666580;
|
||||
transition:
|
||||
opacity 0.4s ease,
|
||||
visibility 0.4s ease;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
align-items: center;
|
||||
|
||||
> div {
|
||||
max-width: 880px;
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
--padding: 1rem;
|
||||
position: relative;
|
||||
background-color: #ffffff;
|
||||
background-clip: padding-box;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
transform: translate(0, 2rem);
|
||||
transition:
|
||||
transform 0.4s ease,
|
||||
opacity 0.4s ease;
|
||||
box-shadow:
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
pointer-events: auto;
|
||||
max-height: 90vh;
|
||||
padding: var(--padding);
|
||||
width: calc(880px - calc(--padding * 2));
|
||||
z-index: 2008;
|
||||
max-width: 100%;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
|
||||
header {
|
||||
padding: 24px;
|
||||
padding: 24px 16px;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row-reverse;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
flex: unset;
|
||||
border: none;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
fill: orange;
|
||||
fill: #0a0a0a;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0 auto 0 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
main #dialog-description {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
main > * {
|
||||
padding-top: 1rem;
|
||||
padding-top: 0rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
114
src/lib/components/Dropdown.svelte
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import { clickOutside } from '$lib/utils/mouseEvents';
|
||||
|
||||
export let options = ['Today', 'Yesterday', 'Last 7 Days', 'Last 30 Days', 'All time'];
|
||||
export let selected;
|
||||
export let placeholder = '';
|
||||
export let label = '';
|
||||
export let icon = undefined;
|
||||
export let required = false;
|
||||
|
||||
let dropdown: Element;
|
||||
let open = false;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function select(option: string) {
|
||||
selected = option;
|
||||
open = false;
|
||||
|
||||
dispatch('value', option);
|
||||
}
|
||||
|
||||
function handleEnter(event: KeyboardEvent): boolean {
|
||||
if (!(event.code === 'Enter' || event.code === 'Space')) return false;
|
||||
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
console.log('dropdown element:', dropdown);
|
||||
const outside = clickOutside(event, dropdown);
|
||||
console.log('click outside:', outside);
|
||||
if (outside === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
open = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('click', handleClick);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('click', handleClick);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="dropdown" bind:this={dropdown}>
|
||||
<span role="button" class="trigger" on:click={() => (open = !open)}>
|
||||
<Input
|
||||
{icon}
|
||||
{placeholder}
|
||||
{label}
|
||||
value={selected}
|
||||
{required}
|
||||
on:blur={() => (open = false)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{#if open}
|
||||
<ul class="menu">
|
||||
{#each options as option (option)}
|
||||
<li>
|
||||
<span
|
||||
tabindex="0"
|
||||
class:active={selected === option}
|
||||
on:click={() => select(option)}
|
||||
on:keydown={(event) => handleEnter(event) && select(option)}
|
||||
role="button">{option}</span
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
.arrow {
|
||||
margin-left: auto;
|
||||
}
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background: var(--bg);
|
||||
color: var(--color);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
list-style: none;
|
||||
margin: 0.2rem 0 0 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.menu li span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.menu li span:hover,
|
||||
.menu li span:active {
|
||||
background: #333;
|
||||
background: var(--color);
|
||||
color: var(--bg);
|
||||
}
|
||||
</style>
|
||||
23
src/lib/components/Error.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<div type="error" class="negative">
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.negative {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
color: #1c1819;
|
||||
background: linear-gradient(90deg, #ff5449 4px, #fed8d0 4px);
|
||||
|
||||
> div {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import User from '$lib/icons/user.svelte';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
// Create a derived store to extract breadcrumb data
|
||||
@@ -40,6 +41,7 @@
|
||||
|
||||
<div class="right">
|
||||
<span>User profile</span>
|
||||
<User />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,4 +114,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.right svg) {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
fill: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
91
src/lib/components/Input.svelte
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
export let label: string;
|
||||
export let value: string;
|
||||
export let placeholder: string;
|
||||
export let required = false;
|
||||
export let icon: unknown;
|
||||
|
||||
let focus = false;
|
||||
</script>
|
||||
|
||||
<div class="label-input">
|
||||
{#if label?.length > 0}
|
||||
<label for={label}>{label}</label>
|
||||
{/if}
|
||||
|
||||
<div class="input" class:focus>
|
||||
{#if icon}
|
||||
<i class="icon">
|
||||
<svelte:component this={icon} />
|
||||
</i>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
id={label}
|
||||
{placeholder}
|
||||
name={label}
|
||||
bind:value
|
||||
{required}
|
||||
on:focus={() => (focus = true)}
|
||||
on:blur={() => (focus = false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.label-input {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.input i {
|
||||
display: block;
|
||||
--icon-size: 1.2rem;
|
||||
height: var(--icon-size);
|
||||
width: var(--icon-size);
|
||||
fill: #4c4243;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
--padding: 0.75rem;
|
||||
width: calc(100% - (var(--padding) * 2));
|
||||
height: 2.5rem;
|
||||
background: #ffffff;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid #a68b85;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px var(--padding);
|
||||
|
||||
&.focus {
|
||||
box-shadow: 0px 0px 0px 4px #7d66654d;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
touch-action: manipulation;
|
||||
background: transparent;
|
||||
color: var(--color);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
src/lib/components/JsonViewer.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { JsonView } from '@zerodevx/svelte-json-view';
|
||||
|
||||
export let json: any;
|
||||
</script>
|
||||
|
||||
<div class="parent">
|
||||
<div class="code">
|
||||
<JsonView {json} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.parent {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
background-color: var(--color);
|
||||
max-height: calc(90vh - 10rem);
|
||||
color: white;
|
||||
border-radius: var(--border-radius, 1rem);
|
||||
padding: 1rem;
|
||||
overflow: scroll;
|
||||
|
||||
--jsonBorderLeft: 1px dotted var(--highlight);
|
||||
--jsonKeyColor: #87afff;
|
||||
--jsonValStringColor: #afaf87;
|
||||
--jsonValBooleanColor: #ff8787;
|
||||
--jsonValNumberColor: #5fd787;
|
||||
}
|
||||
</style>
|
||||
98
src/lib/components/LiveImage.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { grey400x225 } from '$lib/utils/staticImageSource';
|
||||
import Dialog from './Dialog.svelte';
|
||||
|
||||
const IMAGE_PROXY_URL = 'http://localhost:6081';
|
||||
const IMAGE_REFRESH_INTERVAL = 300;
|
||||
|
||||
let { imageUrl }: { imageUrl: string } = $props();
|
||||
let lastUpdated = new Date();
|
||||
let timestamp = $state(0);
|
||||
let fullscreen = $state(false);
|
||||
|
||||
let imageSource: string | ArrayBuffer = grey400x225;
|
||||
let lastCacheSize: string;
|
||||
|
||||
function loadBlob(blob: Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
imageSource = reader.result || '';
|
||||
const img = document.getElementById('live-image') as HTMLImageElement;
|
||||
if (!img) return;
|
||||
|
||||
// set imageSource to image element
|
||||
img.src = `data:image/jpeg;base64; ${imageSource}`;
|
||||
lastUpdated = new Date();
|
||||
};
|
||||
|
||||
// load blob into FileReader
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
|
||||
function refetchImage() {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(`${IMAGE_PROXY_URL}/image/${imageUrl}`);
|
||||
} catch {
|
||||
console.log('url not valid, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'If-None-Match': lastCacheSize
|
||||
}
|
||||
};
|
||||
|
||||
fetch(url.href, options)
|
||||
.then((resp) => {
|
||||
if (resp.status === 304) throw Error('image exists');
|
||||
lastCacheSize = resp.headers.get('Content-Length') || '';
|
||||
return resp;
|
||||
})
|
||||
.then((resp) => resp.blob())
|
||||
.then((blob) => loadBlob(blob))
|
||||
.catch(() => {}); // suppress all exceptions
|
||||
}
|
||||
|
||||
function timeDiff(d: Date) {
|
||||
const seconds = d.getTime();
|
||||
const v = seconds - lastUpdated.getTime();
|
||||
return Math.floor(v / 100) / 10;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
refetchImage();
|
||||
const imageInterval = setInterval(refetchImage, IMAGE_REFRESH_INTERVAL);
|
||||
const timerInterval = setInterval(() => (timestamp = timeDiff(new Date())), 80);
|
||||
|
||||
return () => Promise.all([clearInterval(imageInterval), clearInterval(timerInterval)]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if !fullscreen}
|
||||
<img on:click={() => (fullscreen = !fullscreen)} src={String(imageSource)} id="live-image" />
|
||||
{:else}
|
||||
<Dialog title="Live stream of printer" on:close={() => (fullscreen = false)}>
|
||||
<img style="width: 100%;" src={String(imageSource)} id="live-image" />
|
||||
</Dialog>
|
||||
|
||||
<img src={String(grey400x225)} />
|
||||
{/if}
|
||||
<span>Last update {timestamp}s ago</span>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
img {
|
||||
width: 400px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
113
src/lib/components/PageElement.svelte
Normal file
@@ -0,0 +1,113 @@
|
||||
<script lang="ts">
|
||||
export let bgColor: string;
|
||||
export let color = 'black';
|
||||
export let title = '';
|
||||
export let description = '';
|
||||
export let header = '';
|
||||
export let link = '/';
|
||||
export let icon = '';
|
||||
</script>
|
||||
|
||||
<a href={link} class="shortcut" style={`--bg: ${bgColor}; --color: ${color}`}>
|
||||
<span class="header">{header}</span>
|
||||
<i class="icon">{icon}</i>
|
||||
<h2>{title}</h2>
|
||||
|
||||
<span class="description">{description}</span>
|
||||
<button>Utforsk</button>
|
||||
</a>
|
||||
|
||||
<style lang="scss">
|
||||
.shortcut {
|
||||
aspect-ratio: 1.5 / 1;
|
||||
border-radius: 0.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--color);
|
||||
background-color: var(--bg);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
padding: 2rem 1.25rem;
|
||||
|
||||
@media (min-width: 750px) {
|
||||
aspect-ratio: 1 / 1;
|
||||
padding: 2.5rem 1.875rem 3.125rem;
|
||||
|
||||
.header {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
padding: 3.125rem 3.75rem 3.75rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px) translateX(-4px);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 4.5rem;
|
||||
letter-spacing: 4.4px;
|
||||
font-family: 'Stop';
|
||||
margin: 0;
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
flex: unset;
|
||||
color: var(--color);
|
||||
font-size: 1rem;
|
||||
border: 2px solid var(--color);
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 0 1.25rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover button {
|
||||
color: var(--bg);
|
||||
background-color: var(--color);
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
// padding: 0 3rem;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
button::after {
|
||||
position: absolute;
|
||||
transition: inherit;
|
||||
width: fit-content;
|
||||
left: 2rem;
|
||||
top: 12px;
|
||||
opacity: 0;
|
||||
content: '→';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
import Network from '$lib/icons/network.svelte';
|
||||
import Layers from '$lib/icons/layers.svelte';
|
||||
import Clock from '$lib/icons/clock.svelte';
|
||||
import Sync from '$lib/icons/sync.svelte';
|
||||
import { formatDuration } from '$lib/utils/conversion';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
@@ -67,6 +68,12 @@
|
||||
</div>
|
||||
<span>{i + 1} of {replicas}</span>
|
||||
|
||||
<div class="title">
|
||||
<Sync />
|
||||
<span>Restarts</span>
|
||||
</div>
|
||||
<span>{status?.containerStatuses?.[0].restartCount}</span>
|
||||
|
||||
<div class="title">
|
||||
<Connection />
|
||||
<span>Running on Node</span>
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
|
||||
<article class="main-container">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<h2>{title}</h2>
|
||||
<slot name="top-left" />
|
||||
</div>
|
||||
<label>{description}</label>
|
||||
</div>
|
||||
|
||||
@@ -25,5 +28,10 @@
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,10 +11,21 @@
|
||||
import { formatBytes, formatDuration } from '$lib/utils/conversion';
|
||||
import type { Node } from '$lib/interfaces/proxmox';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Speed from '$lib/icons/speed.svelte';
|
||||
import Fingerprint from '$lib/icons/fingerprint.svelte';
|
||||
|
||||
export let node: Node;
|
||||
|
||||
const buttons = ['View logs', 'Web terminal', 'graphs'];
|
||||
const buttons = [
|
||||
{ name: 'View logs', link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:25::::::` },
|
||||
{
|
||||
name: 'Terminal',
|
||||
link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:=jsconsole::::::`
|
||||
},
|
||||
{ name: 'Graphs', link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:5::::::` },
|
||||
{ name: 'Web', link: `https://${node.ip}:8006/` }
|
||||
];
|
||||
|
||||
let { cpuinfo, memory, uptime, loadavg } = node.info;
|
||||
|
||||
@@ -23,6 +34,9 @@
|
||||
const lxcsRunning = node.lxcs.filter((l) => l?.template !== 1 && l.status === 'running');
|
||||
const lxcsTotal = node.lxcs.filter((l) => l?.template !== 1);
|
||||
|
||||
const t = cpuinfo.model.match(/(\w+\(\w+\)) (\w+\(\w+\)) (.*)/);
|
||||
const cpu = t[3].replaceAll(' ', ' ');
|
||||
|
||||
onMount(() => {
|
||||
setInterval(() => (uptime += 1), 1000);
|
||||
});
|
||||
@@ -51,6 +65,18 @@
|
||||
>{cpuinfo.cpus} Cores on {cpuinfo.sockets} {cpuinfo.sockets > 1 ? 'Sockets' : 'Socket'}</span
|
||||
>
|
||||
|
||||
<div class="title">
|
||||
<Fingerprint />
|
||||
<span>Model</span>
|
||||
</div>
|
||||
<span>{cpu}</span>
|
||||
|
||||
<div class="title">
|
||||
<Speed />
|
||||
<span>Turbo speed</span>
|
||||
</div>
|
||||
<span>{Math.floor(node.info.cpuinfo.mhz) / 1000} GHz</span>
|
||||
|
||||
<div class="title">
|
||||
<Shield />
|
||||
<span>DDoS protection</span>
|
||||
@@ -90,9 +116,11 @@
|
||||
|
||||
<div class="footer">
|
||||
{#each buttons as btn (btn)}
|
||||
<button on:click={() => console.log(node)}>
|
||||
<span>{btn}</span>
|
||||
<a href={btn.link} target="_blank" rel="noopener noreferrer">
|
||||
<button>
|
||||
<span>{btn.name}</span>
|
||||
</button>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,7 +214,7 @@
|
||||
background-color: var(--bg);
|
||||
|
||||
row-gap: 6px;
|
||||
column-gap: 20px;
|
||||
max-width: 330px;
|
||||
|
||||
> div,
|
||||
span {
|
||||
@@ -203,6 +231,7 @@
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
|
||||
|
||||
@@ -59,8 +59,12 @@
|
||||
min-width: var(--nav-width);
|
||||
margin-right: 1rem;
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
--nav-width: 100px;
|
||||
@media screen and (max-width: 1460px) {
|
||||
--nav-width: 220px;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@media screen and (max-width: 1200px) {
|
||||
--nav-width: 140px;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
23
src/lib/components/Success.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<div type="warning" class="warning">
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.warning {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
color: #091d14;
|
||||
background: linear-gradient(90deg, #02a73a 4px, #62fe67 4px);
|
||||
|
||||
> div {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Dialog from './Dialog.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Certificate from '$lib/icons/certificate.svelte';
|
||||
import { daysUntil } from '$lib/utils/conversion';
|
||||
import JsonViewer from './JsonViewer.svelte';
|
||||
|
||||
export let title = '';
|
||||
export let description = '';
|
||||
@@ -22,7 +26,7 @@
|
||||
<div class="description">{description}</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<slot></slot>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -38,23 +42,8 @@
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data as row, i (row)}
|
||||
<tr on:click={() => hasLinks && goto(links[i])} class={hasLinks ? 'link' : ''}>
|
||||
{#each columns as column (column)}
|
||||
{#if column === 'Link'}
|
||||
<td><a href={row[column]}>Link</a></td>
|
||||
{:else if column === 'Hex'}
|
||||
<td><span class="color" style={`background: ${row[column]}`} /></td>
|
||||
{:else if Array.isArray(row[column])}
|
||||
<td>{row[column].join(', ')}</td>
|
||||
{:else}
|
||||
<td>{row[column]}</td>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
<slot name="tbody"></slot>
|
||||
</table>
|
||||
|
||||
{#if footer?.length}
|
||||
@@ -77,59 +66,6 @@
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: sans-serif;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 500;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
font-stretch: 2px;
|
||||
border-bottom: 1px solid #eaddd5;
|
||||
}
|
||||
|
||||
tr {
|
||||
&:not(&:last-of-type) {
|
||||
border-bottom: 1px solid #eaddd5;
|
||||
}
|
||||
|
||||
&:hover > td {
|
||||
background-color: var(--highlight);
|
||||
background-color: #f5ede9;
|
||||
}
|
||||
|
||||
&.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.color {
|
||||
--size: 2rem;
|
||||
display: block;
|
||||
width: calc(var(--size) * 2);
|
||||
height: var(--size);
|
||||
margin-top: -calc(var(--size / 2));
|
||||
margin-bottom: -calc(var(--size / 2));
|
||||
border-radius: var(--border-radius, 1rem);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
0
src/lib/components/TableWrapper.svelte
Normal file
126
src/lib/components/ThumbnailButton.svelte
Normal file
@@ -0,0 +1,126 @@
|
||||
<script lang="ts">
|
||||
import External from '$lib/icons/external.svelte';
|
||||
|
||||
interface Site {
|
||||
title: string;
|
||||
image: string;
|
||||
link: string;
|
||||
background?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { title, image, background, color, link }: Site = $props();
|
||||
|
||||
let colors = [
|
||||
['#401C26', '#f6cfdd'],
|
||||
['#213726', '#BDCBB2'],
|
||||
['#EED7CD', '#262221'],
|
||||
['#262221', '#F3BFA2'],
|
||||
['#f6cfdd', '#401C26'],
|
||||
['#BDCBB2', '#213726'],
|
||||
['#FF8FAB', '#401C26'],
|
||||
['#9381FF', '#262221']
|
||||
];
|
||||
|
||||
if (!background && !color) {
|
||||
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
background = randomColor[0];
|
||||
color = randomColor[1];
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
href={link}
|
||||
style={`--background: ${background}; --color: ${color}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div class="image" style={`background-image: url("${image}")`}></div>
|
||||
<hr />
|
||||
|
||||
<div class="title">
|
||||
<h2>
|
||||
{title || 'Grafana'}
|
||||
<span class="link"><External /></span>
|
||||
</h2>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style lang="scss">
|
||||
a {
|
||||
border-radius: 0.8rem;
|
||||
color: white;
|
||||
text-align: justify;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
cursor: pointer;
|
||||
background-color: var(--background);
|
||||
color: var(--color);
|
||||
|
||||
&,
|
||||
h2,
|
||||
.link,
|
||||
.title {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.title {
|
||||
h2 {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
margin: 1.2rem auto;
|
||||
font-size: 2rem;
|
||||
justify-content: center;
|
||||
text-transform: lowercase;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2px;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: unset;
|
||||
border: none;
|
||||
width: 96%;
|
||||
margin-left: 2%;
|
||||
height: 1px;
|
||||
background-color: var(--color);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.image {
|
||||
height: 8rem;
|
||||
width: 100%;
|
||||
margin: 1.2rem 0;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
position: absolute;
|
||||
transition: inherit;
|
||||
width: fit-content;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
left: calc(100% - 2rem);
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px) translateX(-4px);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.8rem;
|
||||
|
||||
.title h2 {
|
||||
padding-right: 2rem;
|
||||
|
||||
.link {
|
||||
opacity: 1;
|
||||
left: calc(100% - 1rem);
|
||||
fill: var(--color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
23
src/lib/components/Warning.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<div type="warning" class="warning">
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.warning {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
color: #1c1819;
|
||||
background: linear-gradient(90deg, #db7700 4px, #fedd6c 4px);
|
||||
|
||||
> div {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
82
src/lib/components/forms/FormFilament.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import ColorInput from '$lib/components/ColorInput.svelte';
|
||||
import Dropdown from '$lib/components/Dropdown.svelte';
|
||||
import Flower from '$lib/icons/flower.svelte';
|
||||
import Weight from '$lib/icons/weight.svelte';
|
||||
import Link from '$lib/icons/link.svelte';
|
||||
import PencilRuler from '$lib/icons/pencil-ruler.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const close = () => dispatch('close');
|
||||
|
||||
const materialOptions = ['PLA Matte', 'PLA Basic', 'PLA-CF', 'PET-G'];
|
||||
const weightOptions = ['0.5 kg', '1 kg', '2 kg'];
|
||||
let process = $state('');
|
||||
let port = $state('');
|
||||
</script>
|
||||
|
||||
<form method="POST" action="/printer/filament">
|
||||
<div class="wrapper">
|
||||
<ColorInput label="Hex" required />
|
||||
<Input label="Color name" icon={Flower} placeholder="Infinity orange" required />
|
||||
<Dropdown
|
||||
placeholder="Plastic material name"
|
||||
label="Material"
|
||||
required={true}
|
||||
icon={PencilRuler}
|
||||
options={materialOptions}
|
||||
/>
|
||||
<Dropdown
|
||||
placeholder="Spool weight"
|
||||
label="Weight"
|
||||
icon={Weight}
|
||||
required={true}
|
||||
options={weightOptions}
|
||||
/>
|
||||
<Input label="Link" icon={Link} placeholder="https://store.shop/item" required />
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button on:click={close} aria-disabled="false" type="button" tabindex="0"
|
||||
><span tabindex="-1">Cancel</span></button
|
||||
>
|
||||
<button class="affirmative" type="submit" tabindex="-1">
|
||||
<span tabindex="-1">Add connection</span>
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
form {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 0.75rem 1.5rem;
|
||||
max-width: 100%;
|
||||
height: 2.5rem;
|
||||
width: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
button {
|
||||
flex: unset;
|
||||
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(form .wrapper div) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
98
src/lib/components/kube-describe/DaemonSet.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Tab from '$lib/components/navigation/Tab.svelte';
|
||||
import Tabs from '$lib/components/navigation/Tabs.svelte';
|
||||
import TabList from '$lib/components/navigation/TabList.svelte';
|
||||
import TabView from '$lib/components/navigation/TabView.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import JsonViewer from '$lib/components/JsonViewer.svelte';
|
||||
import type { V1DaemonSet } from '@kubernetes/client-node';
|
||||
|
||||
const { daemonset }: { daemonset: V1DaemonSet } = $props();
|
||||
const { status, spec } = daemonset || {};
|
||||
|
||||
let uptime = writable(new Date().getTime() - new Date(status?.startTime || 0).getTime());
|
||||
|
||||
onMount(() => {
|
||||
setInterval(() => uptime.update((n) => n + 1000), 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Details</Tab>
|
||||
<Tab>Metadata</Tab>
|
||||
<Tab>Spec</Tab>
|
||||
<Tab>Status</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabView>
|
||||
<div class="section-wrapper">
|
||||
<Section title="Status" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Pods scheduled</label>
|
||||
<span>{status?.currentNumberScheduled}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Pods available</label>
|
||||
<span>{status?.numberAvailable}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Pods ready</label>
|
||||
<span>{status?.numberReady}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Pods misscheduled</label>
|
||||
<span>{status?.numberMisscheduled}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Spec" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Number of containers</label>
|
||||
<span>{spec?.template?.spec?.containers.length}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Number of volumes</label>
|
||||
<span>{spec?.template?.spec?.volumes?.length}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Restart policy</label>
|
||||
<span>{spec?.template?.spec?.restartPolicy}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Host network</label>
|
||||
<span>{spec?.template?.spec?.hostNetwork ? 'yes' : 'no'}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>DNS policy</label>
|
||||
<span>{spec?.dnsPolicy}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={daemonset.metadata} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={daemonset.spec} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={daemonset.status} />
|
||||
</TabView>
|
||||
</Tabs>
|
||||
109
src/lib/components/kube-describe/Deployment.svelte
Normal file
@@ -0,0 +1,109 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Tab from '$lib/components/navigation/Tab.svelte';
|
||||
import Tabs from '$lib/components/navigation/Tabs.svelte';
|
||||
import TabList from '$lib/components/navigation/TabList.svelte';
|
||||
import TabView from '$lib/components/navigation/TabView.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import JsonViewer from '$lib/components/JsonViewer.svelte';
|
||||
import type { V1Deployment } from '@kubernetes/client-node';
|
||||
|
||||
const { deployment }: { deployment: V1Deployment } = $props();
|
||||
const { status, metadata, spec } = deployment || {};
|
||||
|
||||
let uptime = writable(new Date().getTime() - new Date(status?.startTime || 0).getTime());
|
||||
|
||||
onMount(() => {
|
||||
setInterval(() => uptime.update((n) => n + 1000), 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Details</Tab>
|
||||
<Tab>Metadata</Tab>
|
||||
<Tab>Spec</Tab>
|
||||
<Tab>Status</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabView>
|
||||
<div class="section-wrapper">
|
||||
<Section title="Status" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Pods ready</label>
|
||||
<span>{status?.readyReplicas}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Pods available</label>
|
||||
<span>{status?.availableReplicas}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Replicas</label>
|
||||
<span>{status?.replicas}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Metadata" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Namespace</label>
|
||||
<span>{metadata?.namespace}</span>
|
||||
</div>
|
||||
|
||||
{#if metadata?.ownerReferences?.length || 0 > 0}
|
||||
<div class="section-element">
|
||||
<label>Parent pod</label>
|
||||
<a
|
||||
href={`/cluster/${metadata?.ownerReferences?.[0].kind.toLowerCase()}/${
|
||||
metadata?.ownerReferences?.[0].uid
|
||||
}`}
|
||||
sveltekit:reload><span>{metadata?.ownerReferences?.[0].kind}</span></a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Spec" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Number of containers</label>
|
||||
<span>{spec?.template?.spec?.containers.length}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Scheduler</label>
|
||||
<span>{spec?.schedulerName}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Host network</label>
|
||||
<span>{spec?.hostNetwork ? 'yes' : 'no'}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>DNS policy</label>
|
||||
<span>{spec?.dnsPolicy}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={deployment.metadata} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={deployment.spec} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={deployment.status} />
|
||||
</TabView>
|
||||
</Tabs>
|
||||
@@ -1,39 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
import type { PageData } from './$types';
|
||||
import type { V1Pod } from '@kubernetes/client-node';
|
||||
import { formatDuration } from '$lib/utils/conversion';
|
||||
import Tab from '$lib/components/navigation/Tab.svelte';
|
||||
import Tabs from '$lib/components/navigation/Tabs.svelte';
|
||||
import TabList from '$lib/components/navigation/TabList.svelte';
|
||||
import TabView from '$lib/components/navigation/TabView.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import { formatDuration } from '$lib/utils/conversion';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Logs from '$lib/components/Logs.svelte';
|
||||
import JsonViewer from '$lib/components/JsonViewer.svelte';
|
||||
import { source } from 'sveltekit-sse';
|
||||
import type { V1Pod } from '@kubernetes/client-node';
|
||||
|
||||
const { pod }: { pod: V1Pod } = $props();
|
||||
const { status, metadata, spec } = pod || {};
|
||||
|
||||
let value = 'no data :(';
|
||||
if (browser) {
|
||||
console.log('setting up sse', window.location.pathname);
|
||||
value = source(window.location.pathname + '/logs').select('message');
|
||||
console.log(value);
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const { pod }: { pod: V1Pod | undefined } = data;
|
||||
const { status, metadata, spec } = pod || {};
|
||||
// console.log(pod);
|
||||
|
||||
let uptime = writable(new Date().getTime() - new Date(status?.startTime || 0).getTime());
|
||||
|
||||
let logs = writable([]);
|
||||
let eventSource: EventSource;
|
||||
|
||||
const parentUrl = `/cluster/${metadata?.ownerReferences?.[0].kind.toLowerCase()}/${metadata?.ownerReferences?.[0].uid}`;
|
||||
|
||||
function setupWS() {
|
||||
const url = new URL(`${window.location.origin}/cluster/pod/${pod?.metadata?.uid}/logs`);
|
||||
if (pod?.metadata) {
|
||||
if (pod?.metadata === undefined) {
|
||||
console.error('missing pod info. not enough metadata to setup WS connection.');
|
||||
return;
|
||||
}
|
||||
@@ -54,7 +48,13 @@
|
||||
|
||||
onMount(() => {
|
||||
setInterval(() => uptime.update((n) => n + 1000), 1000);
|
||||
|
||||
if (browser) {
|
||||
console.log('setting up sse', window.location.pathname);
|
||||
value = source(window.location.pathname + '/logs').select('message');
|
||||
console.log(value);
|
||||
return setupWS();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -65,13 +65,13 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<PageHeader>Pod: {pod?.metadata?.name}</PageHeader>
|
||||
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Details</Tab>
|
||||
<Tab>Logs</Tab>
|
||||
<Tab>Metadata</Tab>
|
||||
<Tab>Spec</Tab>
|
||||
<Tab>Status</Tab>
|
||||
<Tab>Deployment logs</Tab>
|
||||
</TabList>
|
||||
|
||||
@@ -108,9 +108,11 @@
|
||||
<span>{metadata?.namespace}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Parent resource</label>
|
||||
<span>{metadata?.ownerReferences?.[0].kind}</span>
|
||||
<div class="section-element" data-sveltekit-preload-data="false">
|
||||
<label>Parent pod</label>
|
||||
<a href={parentUrl} sveltekit:reload
|
||||
><span>{metadata?.ownerReferences?.[0].kind}</span></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
@@ -156,10 +158,18 @@
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<Logs logs={JSON.stringify(metadata, null, 2).split('\n')} lineNumbers={false} />
|
||||
<JsonViewer json={pod.metadata} lineNumbers={false} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<Logs lineNumbers={false} />
|
||||
<JsonViewer json={pod.spec} lineNumbers={false} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={pod.status} lineNumbers={false} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<Logs logs="" lineNumbers={false} />
|
||||
</TabView>
|
||||
</Tabs>
|
||||
14
src/lib/icons/Length.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M752 512h-304v-64h48c8.8 0 16-7.2 16-16v-96c0-8.8-7.2-16-16-16h-57.2c-10.5-35.3-29.6-67.6-56.4-94.4-42.3-42.3-98.6-65.6-158.4-65.6s-116.1 23.3-158.4 65.6c-42.3 42.3-65.6 98.6-65.6 158.4v160c0 35.3 28.7 64 64 64h320c35.3 0 64-28.7 64-64h288v64h32v-80c0-8.8-7.2-16-16-16zM480 352v64h-32v-32c0-10.8-0.8-21.5-2.3-32h34.3zM384 544h-320v-160c0-88.2 71.8-160 160-160s160 71.8 160 160v160z"
|
||||
></path>
|
||||
<path
|
||||
d="M224 288c-52.9 0-96 43.1-96 96s43.1 96 96 96 96-43.1 96-96-43.1-96-96-96zM224 448c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64c0 35.3-28.7 64-64 64z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 699 B |
30
src/lib/icons/barcore.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path d="M0 160h32v352h-32v-352z"></path>
|
||||
<path d="M64 160h64v352h-64v-352z"></path>
|
||||
<path d="M160 160h32v352h-32v-352z"></path>
|
||||
<path d="M224 160h32v352h-32v-352z"></path>
|
||||
<path d="M288 160h64v352h-64v-352z"></path>
|
||||
<path d="M736 160h32v352h-32v-352z"></path>
|
||||
<path d="M576 160h64v352h-64v-352z"></path>
|
||||
<path d="M672 160h32v352h-32v-352z"></path>
|
||||
<path d="M512 160h32v352h-32v-352z"></path>
|
||||
<path d="M448 160h32v352h-32v-352z"></path>
|
||||
<path d="M384 160h32v352h-32v-352z"></path>
|
||||
<path d="M0 544h32v64h-32v-64z"></path>
|
||||
<path d="M64 544h64v64h-64v-64z"></path>
|
||||
<path d="M160 544h32v64h-32v-64z"></path>
|
||||
<path d="M224 544h32v64h-32v-64z"></path>
|
||||
<path d="M288 544h64v64h-64v-64z"></path>
|
||||
<path d="M736 544h32v64h-32v-64z"></path>
|
||||
<path d="M576 544h64v64h-64v-64z"></path>
|
||||
<path d="M672 544h32v64h-32v-64z"></path>
|
||||
<path d="M512 544h32v64h-32v-64z"></path>
|
||||
<path d="M448 544h32v64h-32v-64z"></path>
|
||||
<path d="M384 544h32v64h-32v-64z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
23
src/lib/icons/certificate.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path d="M192 128h160v32h-160v-32z"></path>
|
||||
<path d="M192 192h384v32h-384v-32z"></path>
|
||||
<path d="M192 256h352v32h-352v-32z"></path>
|
||||
<path d="M192 320h384v32h-384v-32z"></path>
|
||||
<path d="M192 384h128v32h-128v-32z"></path>
|
||||
<path d="M192 448h64v32h-64v-32z"></path>
|
||||
<path d="M192 512h96v32h-96v-32z"></path>
|
||||
<path
|
||||
d="M521 542.3c14.3-16.8 23-38.5 23-62.3 0-52.9-43.1-96-96-96s-96 43.1-96 96c0 23.7 8.7 45.5 23 62.3l-85.3 170.6c-3.1 6.2-1.9 13.6 3 18.5s12.3 6.1 18.5 3l49.7-24.8 24.8 49.7c2.7 5.5 8.3 8.8 14.3 8.8 0.6 0 1.3 0 1.9-0.1 6.7-0.8 12.2-5.7 13.7-12.3l32.4-140.5 32.4 140.4c1.5 6.6 7 11.5 13.7 12.3 0.6 0.1 1.3 0.1 1.9 0.1 6 0 11.6-3.4 14.3-8.8l24.8-49.7 49.7 24.8c6.2 3.1 13.6 1.9 18.5-3s6.1-12.3 3-18.5l-85.3-170.5zM448 448c17.6 0 32 14.4 32 32s-14.4 32-32 32-32-14.4-32-32 14.4-32 32-32zM394.4 705.1l-12.1-24.2c-4-7.9-13.6-11.1-21.5-7.2l-21.1 10.5 60.5-121c7.6 4.4 15.9 7.7 24.6 9.9l-30.4 132zM535.2 673.7c-7.9-4-17.5-0.7-21.5 7.2l-12.1 24.2-30.4-131.9c8.7-2.2 17-5.5 24.6-9.9l60.5 121-21.1-10.6z"
|
||||
></path>
|
||||
<path
|
||||
d="M608 0h-448c-35.3 0-64 28.7-64 64v544c0 35.3 28.7 64 64 64h128v-64h-128v-544h448v544c0 0 0 0 0 0v64c35.3 0 64-28.7 64-64v-544c0-35.3-28.7-64-64-64z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
14
src/lib/icons/external.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M736 0h-256v64h178.7l-401.3 401.4 45.3 45.3 401.3-401.4v178.7h64v-256c0-17.7-14.3-32-32-32z"
|
||||
></path>
|
||||
<path
|
||||
d="M544 736h-512v-512h256v-32h-272c-8.8 0-16 7.2-16 16v544c0 8.8 7.2 16 16 16h544c8.8 0 16-7.2 16-16v-272h-32v256z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
15
src/lib/icons/fingerprint.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M672 368c0-6.4-0.3-12.7-0.9-19-4.6-92.3-27.6-171.9-67.2-231.6-50.9-76.8-127-117.4-219.9-117.4s-169 40.6-219.9 117.4c-44.6 67.2-68.1 159.3-68.1 266.6 0 88.7 5.7 189.6 48.1 264.6 21.9 38.7 52.4 68.4 90.7 88.1 40.7 21 89.5 31.2 149.2 31.2s108.5-10.2 149.2-31.2c38.3-19.8 68.8-49.4 90.7-88.1 42.4-75 48.1-175.9 48.1-264.6 0-5.4-0.1-10.7-0.2-16h0.2zM217.4 152.8c39.1-58.9 95.1-88.8 166.6-88.8 44.3 0 82.7 11.5 114.7 34.2-11.5-1.4-23.1-2.2-34.7-2.2-50 0-121.8 9.2-179.7 53.3-61.2 46.5-92.3 120.1-92.3 218.7 0 79.4 64.6 144 144 144s144-64.6 144-144v-16h-32v16c0 61.8-50.2 112-112 112s-112-50.2-112-112c0-208.8 150.4-240 240-240 26.6 0 53.2 4.5 78.3 13.1 2.8 3.8 5.6 7.6 8.3 11.7 7.6 11.4 14.4 23.7 20.5 37-31.3-18.9-68-29.8-107.1-29.8-114.7 0-208 93.3-208 208 0 44.1 35.9 80 80 80s80-35.9 80-80c0-26.5 21.5-48 48-48s48 21.5 48 48c0 97-79 176-176 176-95.2 0-173-76-175.9-170.6 1.4-90 21.2-166.1 57.3-220.6zM568.1 617.2c-33.5 59.2-92 86.8-184.1 86.8s-150.7-27.6-184.1-86.8c-1.3-2.3-2.5-4.6-3.7-6.9 37.9 19.4 80.1 29.7 123.8 29.7v-32c-51.8 0-101.4-16.5-142.5-46.8-6.6-24-10.8-49.8-13.4-76.2 37.5 54.9 100.5 91 171.9 91 114.7 0 208-93.3 208-208 0-44.1-35.9-80-80-80s-80 35.9-80 80c0 26.5-21.5 48-48 48s-48-21.5-48-48c0-97 79-176 176-176 50.1 0 95.4 21 127.5 54.8 10.9 40.7 16.5 86.9 16.5 137.2 0 80.4-4.7 171-39.9 233.2z"
|
||||
></path>
|
||||
<path d="M320 368v16h32v-16c0-61.8 50.2-112 112-112h16v-32h-16c-79.4 0-144 64.6-144 144z"></path>
|
||||
<path
|
||||
d="M512 224v32c15.1 0 30.6 12.4 42.6 34 13.8 24.9 21.4 58.3 21.4 94 0 123.3-70.1 256-224 256v32c80.1 0 147.1-32 193.7-92.5 20.4-26.5 36.2-57.6 47-92.3 10.2-33 15.3-67.6 15.3-103.1 0-89.8-42.2-160.1-96-160.1z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
13
src/lib/icons/flower.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
d="M768.2 384c0-57.1-37.6-105.6-89.3-122 9.4-18 14.4-38.2 14.4-59.1 0-34.2-13.3-66.3-37.5-90.5s-56.3-37.5-90.5-37.5c-20.9 0-41 5-59.1 14.4-16.4-51.7-64.9-89.3-122-89.3-57.2-0.1-105.7 37.5-122.2 89.3-48.2-25-109.2-17.3-149.6 23.1s-48.1 101.4-23.1 149.6c-51.7 16.4-89.3 64.9-89.3 122s37.6 105.6 89.4 122.1c-25 48.2-17.3 109.2 23.1 149.6s101.3 48.1 149.6 23.1c16.4 51.8 64.9 89.4 122.1 89.4 57.1 0 105.6-37.6 122-89.3 18 9.4 38.2 14.4 59.1 14.4 34.2 0 66.3-13.3 90.5-37.5s37.5-56.3 37.5-90.5c0-20.9-5-41-14.4-59.1 51.7-16.6 89.3-65.1 89.3-122.2zM506.7 167.9c0.6-0.4 1.3-0.9 1.8-1.2 3.1-2.1 7.4-5 11.4-9 25-25 65.6-25 90.5 0 25 25 25 65.6 0 90.5-4 4-6.7 8-8.9 11.3-0.4 0.6-1 1.4-1.4 2-6.3 5.2-10.1 12.5-11.2 20.3l-93.8 38.9c-11.4-19.9-27.9-36.5-47.8-47.8l38.9-93.7c7.9-1.1 15.2-5 20.5-11.3zM384 448c-35.3 0-64-28.7-64-64s28.7-64 64-64c35.3 0 64 28.7 64 64s-28.7 64-64 64zM318.4 142.4c0.7-3.7 1.7-8.8 1.7-14.5 0-35.3 28.7-64 64-64s64 28.7 64 64c0 5.7 1 10.7 1.7 14.4 0.1 0.7 0.3 1.6 0.4 2.3-0.7 8.1 1.7 16 6.4 22.3l-38.9 93.7c-10.8-3-22.1-4.5-33.8-4.5s-23 1.6-33.8 4.5l-38.7-93.6c4.8-6.3 7.3-14.2 6.6-22.4 0.1-0.7 0.3-1.5 0.4-2.2zM157.7 157.6c25-25 65.6-25 90.5 0 4 4 8 6.7 11.3 8.9 0.6 0.4 1.4 1 2 1.4 5.2 6.2 12.5 10.1 20.2 11.2l38.9 93.7c-19.9 11.4-36.5 27.9-47.8 47.8l-93.6-38.8c-1-7.8-4.9-15.2-11.2-20.4-0.4-0.6-0.9-1.2-1.2-1.8-2.1-3.1-5-7.4-9-11.4-25-25-25-65.6-0.1-90.6zM64 384c0-35.3 28.7-64 64-64 5.7 0 10.7-1 14.4-1.7 0.7-0.1 1.6-0.3 2.3-0.4 8.1 0.7 16.1-1.7 22.3-6.5l93.6 38.8c-3 10.8-4.5 22.1-4.5 33.8s1.6 23 4.5 33.8l-93.6 38.8c-6.2-4.8-14.1-7.2-22.3-6.5-0.7-0.1-1.5-0.3-2.2-0.4-3.7-0.7-8.8-1.7-14.5-1.7-35.3 0-64-28.7-64-64zM261.5 600.1c-0.6 0.4-1.2 0.9-1.8 1.2-3.1 2.1-7.4 5-11.4 9-25 25-65.6 25-90.5 0-25-25-25-65.6 0-90.5 4-4 6.7-8 8.9-11.3 0.4-0.6 1-1.4 1.4-2 6.3-5.2 10.1-12.6 11.2-20.4l93.6-38.8c11.4 19.9 27.9 36.5 47.8 47.8l-38.9 93.7c-7.8 1.2-15.1 5.1-20.3 11.3zM449.8 625.6c-0.7 3.7-1.7 8.8-1.7 14.5 0 35.3-28.7 64-64 64s-64-28.7-64-64c0-5.7-1-10.7-1.7-14.4-0.1-0.7-0.3-1.6-0.4-2.3 0.7-8.2-1.7-16.2-6.6-22.4l38.8-93.6c10.8 3 22.1 4.5 33.8 4.5s23-1.6 33.8-4.5l38.9 93.7c-4.7 6.2-7.2 14.1-6.5 22.2-0.1 0.8-0.3 1.6-0.4 2.3zM610.5 610.4c-25 25-65.6 25-90.5 0-4-4-8-6.7-11.3-8.9-0.6-0.4-1.4-1-2-1.4-5.3-6.3-12.6-10.2-20.4-11.2l-38.9-93.7c19.9-11.4 36.5-27.9 47.8-47.8l93.8 38.9c1.1 7.8 4.9 15.1 11.2 20.3 0.4 0.6 0.9 1.3 1.2 1.8 2.1 3.1 5 7.4 9 11.4 25 25 25 65.6 0.1 90.6zM640.2 448c-5.7 0-10.7 1-14.4 1.7-0.7 0.1-1.6 0.3-2.3 0.4-8.2-0.7-16.1 1.7-22.4 6.5l-93.7-38.9c3-10.8 4.5-22.1 4.5-33.8s-1.6-23-4.5-33.8l93.7-38.9c6.2 4.8 14.2 7.3 22.3 6.5 0.7 0.1 1.5 0.3 2.2 0.4 3.7 0.7 8.8 1.7 14.5 1.7 35.3 0 64 28.7 64 64 0.1 35.5-28.6 64.2-63.9 64.2z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
14
src/lib/icons/helm.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M384 256c-70.6 0-128 57.4-128 128s57.4 128 128 128c70.6 0 128-57.4 128-128s-57.4-128-128-128zM384 448c-35.3 0-64-28.7-64-64s28.7-64 64-64c35.3 0 64 28.7 64 64s-28.7 64-64 64z"
|
||||
></path>
|
||||
<path
|
||||
d="M768 416v-64h-97.7c-6-54.3-27.1-105.2-61.2-147.8l69.1-69.1-45.3-45.3-69.1 69.2c-42.6-34.2-93.5-55.3-147.8-61.2v-97.8h-64v97.7c-54.3 6-105.2 27.1-147.8 61.2l-69.1-69.1-45.3 45.3 69.1 69.1c-34.2 42.6-55.3 93.5-61.2 147.8h-97.7v64h97.7c6 54.3 27.1 105.2 61.2 147.8l-69.1 69.1 45.3 45.3 69.1-69.1c42.6 34.2 93.5 55.3 147.8 61.2v97.7h64v-97.7c54.3-6 105.2-27.1 147.8-61.2l69.1 69.1 45.3-45.3-69.2-69.1c34.2-42.6 55.3-93.5 61.2-147.8h97.8zM638 352h-64.7c-4.9-29.1-16.4-56.1-32.9-79.2l45.7-45.7c27.6 35.4 46.1 78.2 51.9 124.9zM384 544c-88.2 0-160-71.8-160-160s71.8-160 160-160c88.2 0 160 71.8 160 160s-71.8 160-160 160zM540.9 181.8l-45.7 45.7c-23.1-16.5-50.1-28-79.2-32.9v-64.6c46.7 5.8 89.5 24.3 124.9 51.8zM352 130v64.7c-29.1 4.9-56.1 16.4-79.2 32.9l-45.7-45.7c35.4-27.6 78.2-46.1 124.9-51.9zM181.8 227.1l45.7 45.7c-16.5 23.1-28 50.1-32.9 79.2h-64.6c5.8-46.7 24.3-89.5 51.8-124.9zM130 416h64.7c4.9 29.1 16.4 56.1 32.9 79.2l-45.7 45.7c-27.6-35.4-46.1-78.2-51.9-124.9zM227.1 586.2l45.7-45.7c23.1 16.5 50.1 28 79.2 32.9v64.6c-46.7-5.8-89.5-24.3-124.9-51.8zM416 638v-64.7c29.1-4.9 56.1-16.4 79.2-32.9l45.7 45.7c-35.4 27.6-78.2 46.1-124.9 51.9zM586.2 540.9l-45.7-45.7c16.5-23.1 28-50.1 32.9-79.2h64.6c-5.8 46.7-24.3 89.5-51.8 124.9z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -2,8 +2,8 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="768"
|
||||
height="768"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
|
||||
|
Before Width: | Height: | Size: 852 B After Width: | Height: | Size: 854 B |
17
src/lib/icons/pencil-ruler.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M748.2 2.4c-12-5-25.7-2.2-34.9 6.9l-704 704c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8h672c35.3 0 64-28.7 64-64v-672c0-12.9-7.8-24.6-19.8-29.6zM640 704v-32h-32v32h-64v-32h-32v32h-64v-32h-32v32h-64v-32h-32v32h-64v-32h-32v32h-114.7l594.7-594.7v114.7h-32v32h32v64h-32v32h32v64h-32v32h32v64h-32v32h32v64h-32v32h32v64h-64z"
|
||||
></path>
|
||||
<path
|
||||
d="M598.1 385.2c-6-2.5-12.9-1.1-17.4 3.5l-192 192c-4.6 4.6-5.9 11.5-3.5 17.4s8.3 9.9 14.8 9.9h192c8.8 0 16-7.2 16-16v-192c0-6.5-3.9-12.3-9.9-14.8zM576 576h-137.4l137.4-137.4v137.4z"
|
||||
></path>
|
||||
<path
|
||||
d="M16 480c1.3 0 2.6-0.2 3.9-0.5l128-32c2.8-0.7 5.4-2.2 7.4-4.2l300-300c32.7-32.7 32.7-85.9 0-118.6s-85.9-32.7-118.6 0l-300 300c-2.1 2.1-3.5 4.6-4.2 7.4l-32 128c-1.4 5.5 0.2 11.2 4.2 15.2 3 3.1 7.1 4.7 11.3 4.7zM432.7 47.3c20.2 20.2 20.2 53.1 0 73.4l-16.7 16.7-73.4-73.4 16.7-16.7c20.2-20.2 53.2-20.2 73.4 0zM62.4 344.2l257.6-257.6 73.4 73.4-257.6 257.6-97.8 24.4 24.4-97.8z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
8
src/lib/icons/search.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg height="100%" width="100%" viewBox="0 0 24 24" aria-hidden="true"
|
||||
><path
|
||||
fill="inherit"
|
||||
fill-rule="evenodd"
|
||||
d="M10 2a8 8 0 1 0 4.24 14.785l4.687 4.688a1.8 1.8 0 0 0 2.546-2.546l-4.688-4.687A8 8 0 0 0 10 2m-6.2 8a6.2 6.2 0 1 1 12.4 0 6.2 6.2 0 0 1-12.4 0"
|
||||
clip-rule="evenodd"
|
||||
></path></svg
|
||||
>
|
||||
|
After Width: | Height: | Size: 306 B |
17
src/lib/icons/speed.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="90%"
|
||||
height="90%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
d="M345.9 499.3c11.5 8.6 25 12.7 38.3 12.7 19.6 0 38.9-8.9 51.5-25.7 0 0 0-0.1 0.1-0.1 2.7-3.6 4.9-7.3 6.5-11l66.7-141.5c6.5-13.7 2.4-30.2-9.8-39.2-12.1-9.1-29.1-8.4-40.5 1.7l-117.2 104.1c0 0-0.1 0.1-0.1 0.1-3 2.7-5.8 5.8-8.4 9.1 0 0.1-0.1 0.1-0.1 0.2-10.3 13.7-14.6 30.6-12.1 47.5 2.4 17 11.3 32 25.1 42.1zM358.3 429.1c1.4-1.8 2.9-3.4 4.5-4.9l117.1-104.2c0 0 0 0 0 0l-66.7 141.6-0.1 0.3c-0.8 1.7-1.8 3.5-3.2 5.3-10.6 14.1-30.7 17-45 6.4-6.9-5.1-11.3-12.5-12.6-20.9-1.2-8.4 0.9-16.8 6-23.6z"
|
||||
></path>
|
||||
<path d="M256 576h256v32h-256v-32z"></path>
|
||||
<path
|
||||
d="M737.8 298.5c-19.3-45.7-47-86.8-82.3-122.1s-76.3-62.9-122-82.3c-47.4-19.9-97.7-30.1-149.5-30.1s-102.1 10.2-149.5 30.2c-45.7 19.3-86.8 47-122 82.3s-62.9 76.3-82.3 122.1c-20 47.3-30.2 97.6-30.2 149.4 0 45.4 7.8 89.8 23.3 132.1 14.9 40.8 36.6 78.6 64.4 112.3 6.1 7.4 15.1 11.6 24.7 11.6h543.2c9.6 0 18.6-4.3 24.7-11.6 27.8-33.7 49.4-71.5 64.4-112.3 15.5-42.3 23.3-86.7 23.3-132.1 0-51.8-10.2-102.1-30.2-149.5zM640.1 640h-512.2c-34.9-46.6-56.5-102.2-62.3-160h62.4v-64h-62.4c6.4-64.3 31.9-123 70.7-170.4l41 41 45.3-45.3-41-41c47.4-38.8 106.1-64.3 170.3-70.7v62.4h64v-62.4c64.3 6.4 123.1 31.9 170.5 70.8l-41 41 45.3 45.3 41-41c38.8 47.3 64.3 106 70.7 170.3h-62.4v64h62.4c-5.8 57.8-27.4 113.4-62.3 160z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
15
src/lib/icons/sync.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2c2.703 0 5.272 1.1 7.141 3h-4.141v2h6c0.553 0 1-0.447 1-1v-6h-2v3.059c-0.725-0.647-1.525-1.209-2.384-1.666-1.716-0.912-3.659-1.394-5.616-1.394-1.619 0-3.191 0.319-4.672 0.944-1.428 0.603-2.712 1.469-3.813 2.572s-1.966 2.384-2.572 3.813c-0.625 1.481-0.944 3.053-0.944 4.672h2c0-5.512 4.488-10 10-10z"
|
||||
></path>
|
||||
<path
|
||||
d="M22 12c0 5.513-4.488 10-10 10-2.703 0-5.272-1.1-7.141-3h4.141v-2h-6c-0.553 0-1 0.447-1 1v6h2v-3.059c0.725 0.647 1.525 1.209 2.384 1.666 1.719 0.912 3.659 1.394 5.616 1.394 1.619 0 3.191-0.319 4.672-0.944 1.428-0.603 2.712-1.469 3.813-2.572s1.966-2.384 2.572-3.813c0.625-1.481 0.944-3.053 0.944-4.672h-2z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 802 B |
14
src/lib/icons/time.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M651 271.4c0.8-0.7 1.5-1.4 2.3-2.2 25-25 25-65.6 0-90.5-12.1-12-28.2-18.7-45.3-18.7s-33.2 6.7-45.3 18.7c-0.7 0.7-1.5 1.5-2.2 2.3-43.2-28.7-92.5-46.3-144.6-51.4v-33.6h16c26.5 0 48-21.5 48-48s-21.4-48-47.9-48h-96c-26.5 0-48 21.5-48 48s21.5 48 48 48h16v33.6c-73.4 7.2-141.4 39.3-194.3 92.2-60.4 60.4-93.7 140.7-93.7 226.2s33.3 165.8 93.7 226.3c60.5 60.4 140.8 93.7 226.3 93.7s165.8-33.3 226.3-93.7c60.4-60.5 93.7-140.8 93.7-226.3 0-63.8-18.5-124.7-53-176.6zM320 48c0-8.8 7.2-16 16-16h96c8.8 0 16 7.2 16 16s-7.2 16-16 16h-96c-8.8 0-16-7.2-16-16zM608 192c8.5 0 16.6 3.3 22.6 9.4 12.1 12.1 12.5 31.5 1.1 44.1-6.7-8.2-13.9-16.1-21.5-23.7s-15.5-14.8-23.7-21.5c6-5.4 13.5-8.3 21.5-8.3zM384 704c-141.2 0-256-114.8-256-256s114.8-256 256-256c141.2 0 256 114.8 256 256s-114.8 256-256 256z"
|
||||
></path>
|
||||
<path
|
||||
d="M447 436.3l-31.5-185.8v-0.3c-2.8-15.2-16.1-26.3-31.5-26.3s-28.6 10.9-31.4 26l-31.6 186.4c-0.7 4-1 7.9-1 11.7 0 35.3 28.7 64 64 64s64-28.7 64-64c0-3.7-0.4-7.6-1-11.7v0zM384 480c-17.6 0-32-14.4-32-32 0-2 0.2-4.1 0.6-6.3l31.3-185.7c0 0 0 0 0 0.1l31.4 185.2h-0.1c0.5 2.4 0.7 4.7 0.7 6.7 0.1 17.6-14.3 32-31.9 32z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
9
src/lib/icons/tuning-fork.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 768 768">
|
||||
</g>
|
||||
<path d="M721.4 97.4l-368 368c-21.9 21.9-42.8 16.6-55.1 4.3s-17.6-33.2 4.3-55.1l368-368-45.2-45.2-368 368c-28.3 28.3-33.9 57.7-33.6 77.4 0.2 14.7 3.8 29 10.3 42l-153.5 153.4c-5.3-1.4-10.9-2.2-16.6-2.2-17.1 0-33.2 6.7-45.3 18.7-25 25-25 65.6 0 90.5 12.1 12.1 28.2 18.8 45.3 18.8s33.2-6.7 45.3-18.7c16.7-16.7 22.2-40.4 16.6-61.8l153.4-153.5c13 6.5 27.3 10.1 42 10.3 0.5 0 1 0 1.5 0 19.6 0 48.3-6 75.9-33.6l368-368-45.3-45.3zM86.6 726.6c-6 6-14.1 9.4-22.6 9.4s-16.6-3.3-22.6-9.4c-12.5-12.5-12.5-32.8 0-45.3 0 0 0 0 0 0 6-6 14.1-9.4 22.6-9.4s16.6 3.3 22.6 9.4c12.5 12.5 12.5 32.8 0 45.3z"></path>
|
||||
<path d="M448 544v32c68.4 0 132.7-26.6 181-75s75-112.6 75-181h-32c0 123.5-100.5 224-224 224z"></path>
|
||||
<path d="M224 320c0-123.5 100.5-224 224-224v-32c-68.4 0-132.7 26.6-181 75s-75 112.6-75 181h32z"></path>
|
||||
<path d="M736 320c0 158.8-129.2 288-288 288v32c85.5 0 165.8-33.3 226.3-93.7 60.4-60.5 93.7-140.8 93.7-226.3h-32z"></path>
|
||||
<path d="M448 32v-32c-85.5 0-165.8 33.3-226.3 93.7-60.4 60.5-93.7 140.8-93.7 226.3h32c0-158.8 129.2-288 288-288z"></path>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
16
src/lib/icons/user.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
d="M384 448c105.9 0 192-86.1 192-192s-86.1-192-192-192-192 86.1-192 192 86.1 192 192 192zM384 128c70.6 0 128 57.4 128 128s-57.4 128-128 128c-70.6 0-128-57.4-128-128s57.4-128 128-128z"
|
||||
></path>
|
||||
<path
|
||||
d="M630.7 546.2c-44.2-44.6-124.9-66.2-246.7-66.2s-202.5 21.6-246.7 66.2c-41.3 41.6-41.3 92.3-41.3 125.8v16c0 8.8 7.2 16 16 16h544c8.8 0 16-7.2 16-16v-16c0-33.5 0-84.2-41.3-125.8zM160 672c0-28.4 0-57.9 22.7-80.7 13.5-13.6 34.3-24.4 62-32.2 35.4-10 82.3-15.1 139.3-15.1s103.9 5.1 139.3 15.1c27.7 7.8 48.5 18.6 62 32.2 22.7 22.8 22.7 52.3 22.7 80.7h-448z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 755 B |
22
src/lib/icons/weight.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path d="M160 576h160v32h-160v-32z"></path>
|
||||
<path d="M160 448h160v32h-160v-32z"></path>
|
||||
<path d="M160 384h160v32h-160v-32z"></path>
|
||||
<path d="M448 576h160v32h-160v-32z"></path>
|
||||
<path d="M448 448h160v32h-160v-32z"></path>
|
||||
<path d="M448 384h160v32h-160v-32z"></path>
|
||||
<path d="M160 512h160v32h-160v-32z"></path>
|
||||
<path d="M448 512h160v32h-160v-32z"></path>
|
||||
<path
|
||||
d="M292 314.5c3 3.5 7.4 5.5 12 5.5h160c4.6 0 9-2 12-5.5l64-73c2.8-3.3 4.3-7.5 3.9-11.8-0.3-4.3-2.4-8.3-5.7-11-48.4-40-97.4-58.7-154.2-58.7s-105.8 18.7-154.2 58.7c-3.3 2.8-5.4 6.7-5.7 11s1.1 8.6 3.9 11.8l64 73zM384 192c44.6 0 82.4 12.8 120.8 41.2l-48 54.8h-46l6.9-62.1-31.8-3.5-7.3 65.7h-67.3l-48-54.8c38.3-28.5 76.1-41.3 120.7-41.3z"
|
||||
></path>
|
||||
<path
|
||||
d="M608 32h-448c-70.6 0-128 57.4-128 128v448c0 70.6 57.4 128 128 128h448c70.6 0 128-57.4 128-128v-448c0-70.6-57.4-128-128-128zM672 608c0 35.3-28.7 64-64 64h-448c-35.3 0-64-28.7-64-64v-448c0-35.3 28.7-64 64-64h448c35.3 0 64 28.7 64 64v448z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
11
src/lib/interfaces/health.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export enum HEALTH_STATUS {
|
||||
LIVE = 'live',
|
||||
UNKNOWN = 'unknown',
|
||||
DOWN = 'down'
|
||||
}
|
||||
|
||||
export interface HttpEndpoint {
|
||||
domain: string;
|
||||
code: number;
|
||||
status: HEALTH_STATUS.DOWN;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
export interface Filament {
|
||||
Hex: string;
|
||||
Color: string;
|
||||
Material: string;
|
||||
Weight: string;
|
||||
Count: number;
|
||||
Link: string;
|
||||
hex: string;
|
||||
color: string;
|
||||
material: string;
|
||||
weight: string;
|
||||
count: number;
|
||||
link: string;
|
||||
created: number;
|
||||
updated: number;
|
||||
}
|
||||
|
||||
135
src/lib/server/database.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { currentFilament } from './filament';
|
||||
|
||||
import { open } from 'sqlite';
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import type { Filament } from '$lib/interfaces/printer';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const dbPath = resolve(__dirname, '../../../db.sqlite');
|
||||
|
||||
let db;
|
||||
|
||||
async function initDb() {
|
||||
const db = await open({
|
||||
filename: dbPath,
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
|
||||
// Transaction to run schemas
|
||||
await db.exec('BEGIN TRANSACTION');
|
||||
try {
|
||||
for (const stmt of schemas) {
|
||||
await db.run(stmt);
|
||||
}
|
||||
await db.exec('COMMIT');
|
||||
} catch (err) {
|
||||
console.error('Failed to create tables:', err.message);
|
||||
await db.exec('ROLLBACK');
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
const schemas = [
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS filament (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hex TEXT NOT NULL,
|
||||
color TEXT NOT NULL,
|
||||
material TEXT,
|
||||
weight REAL,
|
||||
link TEXT,
|
||||
added INTEGER, -- epoch seconds
|
||||
updated INTEGER -- epoch seconds
|
||||
)
|
||||
`
|
||||
];
|
||||
|
||||
async function seedData(db) {
|
||||
const baseTimestamp = Math.floor(new Date('2025-04-01T05:47:01+00:00').getTime() / 1000);
|
||||
const filaments = currentFilament();
|
||||
|
||||
const stmt = await db.prepare(`
|
||||
INSERT OR IGNORE INTO filament (hex, color, material, weight, link, added, updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
await db.exec('BEGIN TRANSACTION');
|
||||
try {
|
||||
for (const f of filaments) {
|
||||
const existing = await db.get('SELECT 1 FROM filament WHERE hex = ? AND updated = ?', [
|
||||
f.hex,
|
||||
baseTimestamp
|
||||
]);
|
||||
|
||||
if (!existing) {
|
||||
await db.run(
|
||||
`INSERT INTO filament (hex, color, material, weight, link, added, updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[f.hex, f.color, f.material, f.weight, f.link, baseTimestamp, baseTimestamp]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await db.exec('COMMIT');
|
||||
} catch (err) {
|
||||
console.error('Failed to seed data:', err.message);
|
||||
await db.exec('ROLLBACK');
|
||||
} finally {
|
||||
await stmt.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
// Export helper to use db elsewhere
|
||||
async function getDb() {
|
||||
if (db !== undefined) return db;
|
||||
|
||||
db = await initDb();
|
||||
await seedData(db);
|
||||
console.log('Database setup and seeding complete!');
|
||||
}
|
||||
|
||||
export async function getAllFilament(): Promise<Array<Filament>> {
|
||||
const db = await getDb();
|
||||
const result = await db?.all('SELECT * FROM filament');
|
||||
return result || [];
|
||||
}
|
||||
|
||||
export async function getFilamentByColor(name: string) {
|
||||
const db = await getDb();
|
||||
const result = await db?.get('SELECT * FROM filament WHERE LOWER(color) = ?', [name]);
|
||||
return result || undefined;
|
||||
}
|
||||
|
||||
export async function addFilament(
|
||||
hex: string,
|
||||
color: string,
|
||||
material: string,
|
||||
weight: number,
|
||||
link: string
|
||||
) {
|
||||
const timestamp = Math.floor(new Date().getTime() / 1000);
|
||||
|
||||
const db = await getDb();
|
||||
const result = await db.run(
|
||||
`INSERT INTO filament (hex, color, material, weight, link, added, updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[hex, color, material, weight, link, timestamp, timestamp]
|
||||
);
|
||||
return { id: result.lastID };
|
||||
}
|
||||
|
||||
export async function updatefilament({ id, make, model, year }) {
|
||||
const db = await getDb();
|
||||
await db.run(
|
||||
'UPDATE filaments SET make = ?, model = ?, year = ? WHERE id = ?',
|
||||
make,
|
||||
model,
|
||||
year,
|
||||
id
|
||||
);
|
||||
}
|
||||
54
src/lib/server/health_http.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { request, Agent } from 'https';
|
||||
import tls, { type PeerCertificate } from 'tls';
|
||||
|
||||
const SSL_WEBSERVER = '10.0.0.53';
|
||||
export async function getSSLInfo(url: string, port = 443) {
|
||||
if (new URL(url).protocol !== 'https:') return { raw: 'none' };
|
||||
const hostname = new URL(url).hostname;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = tls.connect(port, SSL_WEBSERVER, { servername: hostname }, () => {
|
||||
const cert = socket.getPeerCertificate(true);
|
||||
|
||||
if (!cert || Object.keys(cert).length === 0) {
|
||||
reject(new Error('No certificate found'));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({
|
||||
subject: cert.subject,
|
||||
issuer: cert.issuer,
|
||||
valid_from: cert.valid_from,
|
||||
valid_to: cert.valid_to,
|
||||
fingerprint: cert.fingerprint,
|
||||
fingerprint256: cert.fingerprint256,
|
||||
ca: cert.ca,
|
||||
nistCurve: cert.nistCurve,
|
||||
asn1Curve: cert.asn1Curve,
|
||||
serialNumber: cert.serialNumber,
|
||||
altNames: cert.subjectaltname,
|
||||
publicKey: cert?.pubkey?.toString('base64') || '',
|
||||
infoAccess: cert?.infoAccess || ''
|
||||
});
|
||||
|
||||
socket.end();
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function healthOk(url: string): Promise<number> {
|
||||
return fetch(url, { signal: AbortSignal.timeout(400) })
|
||||
.then((resp) => {
|
||||
return resp.status;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('got error from health endpoint for url:', url);
|
||||
console.log(error);
|
||||
|
||||
return 550;
|
||||
});
|
||||
}
|
||||
@@ -16,19 +16,70 @@ function buildHomeassistantRequest() {
|
||||
return { url, options };
|
||||
}
|
||||
|
||||
const attributes = {
|
||||
current_stage: null,
|
||||
print_status: null,
|
||||
bed_temperature: null,
|
||||
nozzle_temperature: null,
|
||||
total_usage: null,
|
||||
nozzle_type: null,
|
||||
nozzle_size: null,
|
||||
print_bed_type: null,
|
||||
current_layer: null,
|
||||
total_layer_count: null,
|
||||
print_progress: null,
|
||||
print_length: null,
|
||||
print_weight: null,
|
||||
sd_card_status: null,
|
||||
speed_profile: null,
|
||||
wi_fi_signal: null,
|
||||
end_time: null,
|
||||
cover_image: null,
|
||||
pick_image: null,
|
||||
camera: null
|
||||
};
|
||||
|
||||
interface PrinterState {
|
||||
[key: string]: {
|
||||
value: string;
|
||||
unit?: string;
|
||||
picture?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function printerState(data: object) {
|
||||
const state: PrinterState = {};
|
||||
|
||||
const keys = Object.keys(attributes);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const k = keys[i];
|
||||
const value = data?.filter((el) => el.entity_id.includes(k))[0];
|
||||
|
||||
if (!value) continue;
|
||||
state[k] = { value: value.state };
|
||||
|
||||
if (value?.attributes?.unit_of_measurement)
|
||||
state[k]['unit'] = value.attributes.unit_of_measurement;
|
||||
|
||||
if (value?.attributes?.entity_picture) state[k]['picture'] = value.attributes.entity_picture;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
async function fetchHassStates() {
|
||||
const { url, options } = buildHomeassistantRequest();
|
||||
return fetch(url, options).then((resp) => resp.json());
|
||||
}
|
||||
|
||||
export async function fetchP1P(): Promise<Entity[]> {
|
||||
export async function fetchP1P(): Promise<PrinterState> {
|
||||
try {
|
||||
let hassStates = await fetchHassStates();
|
||||
|
||||
hassStates = hassStates.filter(
|
||||
(el: Entity) => el.attributes.friendly_name?.includes('P1P') === true
|
||||
);
|
||||
return hassStates;
|
||||
return printerState(hassStates);
|
||||
} catch (error) {
|
||||
console.log('ERROR! from fetchP1P:', error);
|
||||
return Promise.reject(null);
|
||||
|
||||
@@ -1,9 +1,46 @@
|
||||
import * as k8s from '@kubernetes/client-node';
|
||||
import stream from 'stream';
|
||||
import { writable } from 'svelte/store';
|
||||
import fs from 'fs';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
/*
|
||||
const kubeCaPath =
|
||||
env.KUBERNETES_CA_CERT_PATH || '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt';
|
||||
const kubeCaCert = fs.readFileSync(kubeCaPath, 'utf8');
|
||||
|
||||
// const kubeSaTokenPath = env.KUBERNETES_SA_TOKEN_PATH || '/var/run/secrets/kubernetes.io/serviceaccount/token';
|
||||
const token = fs.readFileSync(kubeSaTokenPath, 'utf8');
|
||||
*/
|
||||
|
||||
const kubeConfig: k8s.KubeConfig = {
|
||||
clusters: [
|
||||
{
|
||||
name: 'kazan',
|
||||
server: env.KUBERNETES_SERVICE_HOST || 'https://kubernetes.default.svc',
|
||||
// caData: kubeCaCert,
|
||||
// skipTLSVerify: true
|
||||
skipTLSVerify: true
|
||||
}
|
||||
],
|
||||
users: [
|
||||
{
|
||||
name: 'pod-user',
|
||||
token: env.KUBERNETES_SA_TOKEN
|
||||
}
|
||||
],
|
||||
contexts: [
|
||||
{
|
||||
name: 'default-context',
|
||||
user: 'pod-user',
|
||||
cluster: 'kazan'
|
||||
}
|
||||
],
|
||||
currentContext: 'default-context'
|
||||
};
|
||||
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromDefault();
|
||||
kc.loadFromOptions(kubeConfig);
|
||||
|
||||
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
|
||||
const appsV1Api = kc.makeApiClient(k8s.AppsV1Api);
|
||||
@@ -92,12 +129,16 @@ export function createLogStream(podName: string, namespace: string, containerNam
|
||||
});
|
||||
|
||||
console.log('setting logAbortController, prev:', logAbortController);
|
||||
try {
|
||||
logAbortController = await k8sLog.log(namespace, podName, containerName, liveStream, {
|
||||
follow: true,
|
||||
timestamps: false,
|
||||
pretty: false,
|
||||
tailLines: maxLines
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('ERROR SETTING UP WS', error);
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { env } from '$env/dynamic/private';
|
||||
import type { Cluster, Node } from '$lib/interfaces/proxmox';
|
||||
|
||||
function buildProxmoxRequest() {
|
||||
const url = env.PROXMOX_URL || 'https://10.0.0.50:8006/api2/json/';
|
||||
const token = env.PROXMOX_TOKEN || 'REPLACE_WITH_PROXMOX_TOKEN';
|
||||
const url = env.PROXMOX_URL;
|
||||
const token = env.PROXMOX_TOKEN;
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { env } from '$env/dynamic/private';
|
||||
const TRAEFIK_HTTP_URL = '/api/http';
|
||||
|
||||
function buildTraefikRequest(path: string) {
|
||||
const baseURL = env.TRAEFIK_URL || 'http://localhost:9000';
|
||||
const baseURL = env.TRAEFIK_URL;
|
||||
const url = `${baseURL}${TRAEFIK_HTTP_URL}/${path}`;
|
||||
const options = {
|
||||
method: 'GET',
|
||||
@@ -12,6 +12,7 @@ function buildTraefikRequest(path: string) {
|
||||
}
|
||||
};
|
||||
|
||||
console.log('making traefik request', url);
|
||||
return { url, options };
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,20 @@ export function formatDuration(seconds: number) {
|
||||
return `${days} days ${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(Math.floor(seconds)).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function daysUntil(dateString: string) {
|
||||
const inputDate = new Date(dateString);
|
||||
const today = new Date();
|
||||
|
||||
// Clear time components for accurate day comparison
|
||||
inputDate.setHours(0, 0, 0, 0);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const diffTime = inputDate - today;
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
export function convertKiToHumanReadable(input: string) {
|
||||
const match = input.match(/^(\d+)(Ki)$/);
|
||||
if (!match) return 'Invalid input';
|
||||
@@ -44,3 +58,38 @@ export function convertKiToHumanReadable(input: string) {
|
||||
|
||||
return `${humanReadable.toFixed(2)} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
export function formatTimeLeft(seconds: number, short = false) {
|
||||
const units = [
|
||||
{ label: 'mo', value: 2592000 }, // 30 days as an average month
|
||||
{ label: 'd', value: 86400 },
|
||||
{ label: 'h', value: 3600 },
|
||||
{ label: 'm', value: 60 },
|
||||
{ label: 's', value: 1 }
|
||||
];
|
||||
|
||||
let remaining = seconds;
|
||||
const parts = [];
|
||||
|
||||
for (const unit of units) {
|
||||
if (remaining >= unit.value) {
|
||||
const amount = Math.floor(remaining / unit.value);
|
||||
remaining %= unit.value;
|
||||
parts.push(`${amount}${unit.label}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (short) return parts.slice(' ')[0];
|
||||
|
||||
// If 0 seconds, still return "0s"
|
||||
return parts.length > 0 ? parts.join(' ') : '0s';
|
||||
}
|
||||
|
||||
export function formatDateIntl(d: Date) {
|
||||
return new Intl.DateTimeFormat('nb-NO', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'short'
|
||||
}).format(d);
|
||||
}
|
||||
|
||||
22
src/lib/utils/hash.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const cyrb64 = (str: string, seed = 0) => {
|
||||
let h1 = 0xdeadbeef ^ seed,
|
||||
h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
||||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
||||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
// For a single 53-bit numeric return value we could return
|
||||
// 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
// but we instead return the full 64-bit value:
|
||||
return [h2 >>> 0, h1 >>> 0];
|
||||
};
|
||||
|
||||
export const digest = (str: string, seed = 0) => {
|
||||
const [h2, h1] = cyrb64(str, seed);
|
||||
return h2.toString(36).padStart(7, '0') + h1.toString(36).padStart(7, '0');
|
||||
};
|
||||
14
src/lib/utils/mouseEvents.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export function clickOutside(event: MouseEvent, element: Element | undefined) {
|
||||
if (!element) return false;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (!rect) return false;
|
||||
|
||||
const isClickOutside =
|
||||
event.clientX < rect.left ||
|
||||
event.clientX > rect.right ||
|
||||
event.clientY < rect.top ||
|
||||
event.clientY > rect.bottom;
|
||||
|
||||
return isClickOutside ? true : false;
|
||||
}
|
||||
2
src/lib/utils/staticImageSource.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const grey400x225 =
|
||||
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCADhAZADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3aiiipKCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z';
|
||||
@@ -30,8 +30,19 @@
|
||||
|
||||
main {
|
||||
/* mobile: 1rem */
|
||||
margin: 2rem;
|
||||
width: 100%;
|
||||
--margin: 2rem;
|
||||
margin: var(--margin);
|
||||
width: calc(100% - var(--margin) * 2);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
main {
|
||||
--margin: 1rem;
|
||||
}
|
||||
|
||||
:global(> .nav-wrapper) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,82 +1,124 @@
|
||||
<script lang="ts">
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import PageElement from '$lib/components/PageElement.svelte';
|
||||
|
||||
let elems = [];
|
||||
let counter = 0;
|
||||
let colors = [
|
||||
['#401C26', '#f6cfdd'],
|
||||
['#213726', '#BDCBB2'],
|
||||
['#EED7CD', '#262221'],
|
||||
['#262221', '#F3BFA2'],
|
||||
['#f6cfdd', '#401C26'],
|
||||
['#BDCBB2', '#213726'],
|
||||
['#FF8FAB', '#401C26'],
|
||||
['#9381FF', '#262221']
|
||||
];
|
||||
const descriptions = [
|
||||
'Røroshotellene, eid av Røros Hotell AS, er fire unike hoteller og spisesteder på Røros. Med røtter i lokalsamfunnet tilbyr vi autentiske opplevelser, enten du vil bo komfortabelt eller nyte lokal mat',
|
||||
'Faglige møter blir enda bedre med frisk høstluft og unike omgivelser. Kampanjepris fra 2115,-',
|
||||
'Planlegg et minneverdig bedriftsarrangement hos oss!',
|
||||
'Lei hele Erzscheidergården for en utforstyrret ramme til ditt neste møtested!'
|
||||
];
|
||||
|
||||
function createPageElement(title: string, description = '', header = '') {
|
||||
if (counter + 1 >= colors.length) counter = 1;
|
||||
else counter += 1;
|
||||
|
||||
console.log(counter);
|
||||
|
||||
return {
|
||||
bgColor: colors[counter - 1][0],
|
||||
color: colors[counter - 1][1],
|
||||
title,
|
||||
header: null,
|
||||
description: description ? description : '',
|
||||
link: title
|
||||
};
|
||||
}
|
||||
|
||||
elems = elems.concat(createPageElement('sites'));
|
||||
elems = elems.concat(createPageElement('servers', 'Overview of proxmox servers'));
|
||||
elems = elems.concat(
|
||||
createPageElement(
|
||||
'printer',
|
||||
'Realtime information on P1P printer and filament overview with current & historical rolls.'
|
||||
)
|
||||
);
|
||||
elems = elems.concat(
|
||||
createPageElement(
|
||||
'network',
|
||||
'View traefik configuration & all defined routes, services & middlewares.'
|
||||
)
|
||||
);
|
||||
elems = elems.concat(
|
||||
createPageElement(
|
||||
'cluster',
|
||||
'View running resources in Kubernetes cluster. View nodes, daemonset & deployments; and get a pods realtime logs, resource usage & view related kubernetes resources.'
|
||||
)
|
||||
);
|
||||
elems = elems.concat(createPageElement('health'));
|
||||
|
||||
elems = elems.concat(createPageElement('cluster '));
|
||||
elems = elems.concat(createPageElement('health '));
|
||||
</script>
|
||||
|
||||
<PageHeader>Welcome to SvelteKit</PageHeader>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<PageHeader>Welcome to schleppe.cloud infra overview</PageHeader>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
This site is a local-first dashboard for monitoring the state of digital and physical tools in a
|
||||
workshop environment. It currently tracks servers (IP, cores, memory, uptime), 3D printers
|
||||
(status, history, filament stock), and other connected devices. Each device or system has its own
|
||||
page with relevant real-time and historical information. More modules are planned, including
|
||||
general monitoring tools, IoT integrations, and project overviews.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
|
||||
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
|
||||
ipsum voluptatum sunt, atque magni minus.
|
||||
The system is intended for hybrid spaces where digital infrastructure coexists with hands-on work.
|
||||
Alongside real-time monitoring, Schleppe is expanding to reflect the broader physical
|
||||
workspace—covering areas like tool usage, material stocks, and workstations for welding,
|
||||
woodworking, electronics, and leathercraft. The goal is to make the state of the entire
|
||||
workshop—both virtual and physical—easily visible in one place.
|
||||
</p>
|
||||
|
||||
<div class="shortcut-grid">
|
||||
{#each elems as shortcut (shortcut.title)}
|
||||
<PageElement
|
||||
bgColor={shortcut.bgColor}
|
||||
color={shortcut.color}
|
||||
title={shortcut.title}
|
||||
header={shortcut.header}
|
||||
description={shortcut.description}
|
||||
link={shortcut.link}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.4;
|
||||
line-height: 1.7;
|
||||
max-width: 80%;
|
||||
color: #333;
|
||||
|
||||
background-color: #fafafa; /* Subtle background to separate it from the rest */
|
||||
padding: 2rem;
|
||||
border-radius: 1rem; /* Soft edges */
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); /* Light shadow for depth */
|
||||
}
|
||||
|
||||
.shortcut-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
||||
gap: 1.2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
:global(.shortcut-grid .shortcut:nth-of-type(odd) h2, .shortcut-grid a:nth-of-type(odd) h2) {
|
||||
font-weight: 600;
|
||||
letter-spacing: 2.6px;
|
||||
font-family: 'Norman' !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,18 +3,28 @@
|
||||
import Deploy from '$lib/components/Deploy.svelte';
|
||||
import Daemon from '$lib/components/Daemon.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import type { V1DaemonSet, V1Deployment, V1Node } from '@kubernetes/client-node';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let filterValue = $state('');
|
||||
|
||||
const deployments: V1Deployment[] = data?.deployments;
|
||||
const daemons: V1DaemonSet[] = data?.daemons;
|
||||
const nodes: V1Node[] = data?.nodes;
|
||||
const rawDeployments: V1Deployment[] = data?.deployments;
|
||||
const rawDaemons: V1DaemonSet[] = data?.daemons;
|
||||
const rawNodes: V1Node[] = data?.nodes;
|
||||
|
||||
let deployments = $derived(rawDeployments.filter((d) => d.metadata.name.includes(filterValue)));
|
||||
let daemons = $derived(rawDaemons.filter((d) => d.metadata.name.includes(filterValue)));
|
||||
let nodes = $derived(rawNodes.filter((n) => n.metadata.name.includes(filterValue)));
|
||||
</script>
|
||||
|
||||
<PageHeader>Cluster overview</PageHeader>
|
||||
|
||||
<div class="search-section">
|
||||
<Input label="Filter resources" placeholder="Search by name" bind:value={filterValue} />
|
||||
</div>
|
||||
|
||||
<details open>
|
||||
<summary>
|
||||
<h2>Cluster <span>{nodes.length} nodes</span></h2>
|
||||
@@ -65,12 +75,27 @@
|
||||
</details>
|
||||
|
||||
<style lang="scss">
|
||||
.search-section {
|
||||
padding: 1.714rem 0px;
|
||||
}
|
||||
|
||||
.server-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
--grid-tmpl-cols: repeat(2, 1fr);
|
||||
--grid-gap: 0.5rem;
|
||||
grid-template-columns: var(--grid-tmpl-cols, repeat(2, 1fr));
|
||||
gap: var(--grid-gap, 0.5rem);
|
||||
|
||||
margin-bottom: 2rem;
|
||||
|
||||
@media screen and (min-width: 750px) {
|
||||
--grid-tmpl-cols: repeat(2, 1fr);
|
||||
--grid-gap: 1.25rem;
|
||||
}
|
||||
@media screen and (min-width: 1200px) {
|
||||
--grid-tmpl-cols: repeat(3, 1fr);
|
||||
--grid-gap: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.server-list.deploys {
|
||||
@@ -88,7 +113,8 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h2 {
|
||||
:global(.server-list h2) {
|
||||
font-family: 'Reckless Neue';
|
||||
justify-content: unset !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
59
src/routes/cluster/[resource]/[uid]/+page.server.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getPods, getDaemons, getReplicas, getDeployments } from '$lib/server/kubernetes';
|
||||
import type { V1DaemonSet, V1Deployment, V1Pod } from '@kubernetes/client-node';
|
||||
|
||||
const AVAILABLE_RESOURCES = [
|
||||
'pod',
|
||||
'deployment',
|
||||
'daemonset',
|
||||
'cronjobs',
|
||||
'configmap',
|
||||
'replicaset'
|
||||
];
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const { resource, uid } = params;
|
||||
console.log('PARAMS:', params);
|
||||
|
||||
if (!AVAILABLE_RESOURCES.includes(resource)) {
|
||||
return {
|
||||
error: 'No resource ' + resource,
|
||||
resource: null
|
||||
};
|
||||
}
|
||||
console.log(uid);
|
||||
|
||||
let resources: V1Pod[];
|
||||
|
||||
switch (resource) {
|
||||
case 'pod':
|
||||
const podsResp: V1Pod[] = await getPods();
|
||||
resources = JSON.parse(JSON.stringify(podsResp));
|
||||
break;
|
||||
case 'daemonset':
|
||||
const daemonsResp: V1DaemonSet[] = await getDaemons();
|
||||
resources = JSON.parse(JSON.stringify(daemonsResp));
|
||||
break;
|
||||
case 'deployment':
|
||||
const deploymentResp: V1Deployment[] = await getDeployments();
|
||||
resources = JSON.parse(JSON.stringify(deploymentResp));
|
||||
break;
|
||||
case 'replicaset':
|
||||
console.log('replicas');
|
||||
const replicasResp: V1ReplicaSet[] = await getReplicas();
|
||||
console.log('replicas', replicasResp);
|
||||
resources = JSON.parse(JSON.stringify(replicasResp));
|
||||
break;
|
||||
default:
|
||||
console.log('no resources found');
|
||||
}
|
||||
|
||||
const singleResource = resources?.find((p) => p.metadata?.uid === uid);
|
||||
delete singleResource?.metadata?.managedFields;
|
||||
|
||||
return {
|
||||
resource: singleResource,
|
||||
kind: resource,
|
||||
error: null
|
||||
};
|
||||
};
|
||||
32
src/routes/cluster/[resource]/[uid]/+page.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import PodDescribe from '$lib/components/kube-describe/Pod.svelte';
|
||||
import DeploymentDescribe from '$lib/components/kube-describe/Deployment.svelte';
|
||||
import DaemonSetDescribe from '$lib/components/kube-describe/DaemonSet.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import type { V1Pod } from '@kubernetes/client-node';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const { error, kind } = data;
|
||||
const { resource }: { pod: V1Pod | undefined } = data;
|
||||
</script>
|
||||
|
||||
<PageHeader>{kind || 'Resource'}: {resource?.metadata?.name || 'not found'}</PageHeader>
|
||||
|
||||
{#if error}
|
||||
<p>{error}</p>
|
||||
{/if}
|
||||
|
||||
{#if resource}
|
||||
{#if kind == 'pod'}
|
||||
<PodDescribe pod={resource} />
|
||||
{:else if kind == 'deployment' || kind == 'replicaset'}
|
||||
<DeploymentDescribe deployment={resource} />
|
||||
{:else if kind == 'daemonset'}
|
||||
<DaemonSetDescribe daemonset={resource} />
|
||||
{:else}
|
||||
<PodDescribe pod={resource} />
|
||||
{/if}
|
||||
{:else}
|
||||
<h2>404. '{kind}' resource not found!</h2>
|
||||
{/if}
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getPods } from '$lib/server/kubernetes';
|
||||
import type { V1Pod } from '@kubernetes/client-node';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const { uid } = params;
|
||||
|
||||
console.log(uid);
|
||||
|
||||
const podsResp = await getPods();
|
||||
const pods: V1Pod[] = JSON.parse(JSON.stringify(podsResp));
|
||||
|
||||
const pod = pods.find((p) => p.metadata?.uid === uid);
|
||||
return {
|
||||
pod
|
||||
};
|
||||
};
|
||||
30
src/routes/health/+page.server.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { getSSLInfo, healthOk } from '$lib/server/health_http';
|
||||
import { HEALTH_STATUS, type HttpEndpoint } from '$lib/interfaces/health';
|
||||
|
||||
const ENDPOINTS: string[] = env?.HTTP_HEALTH_ENDPOINTS?.split(',');
|
||||
|
||||
export const load: PageServerLoad = async (): Promise<{ httpHealth: HttpEndpoint[] }> => {
|
||||
const statusPromises = ENDPOINTS?.map(async (endpointUrl) => {
|
||||
const status = await healthOk(endpointUrl);
|
||||
const ssl = await getSSLInfo(endpointUrl);
|
||||
|
||||
return { status, ssl };
|
||||
});
|
||||
const endpointStatuses = await Promise.all(statusPromises);
|
||||
|
||||
const httpHealth = ENDPOINTS.map((domain, i) => {
|
||||
return {
|
||||
domain: new URL(domain).hostname,
|
||||
code: endpointStatuses[i].status,
|
||||
ssl: endpointStatuses[i].ssl,
|
||||
status:
|
||||
String(endpointStatuses[i].status)[0] !== '5' ? HEALTH_STATUS.LIVE : HEALTH_STATUS.DOWN
|
||||
} as HttpEndpoint;
|
||||
});
|
||||
|
||||
return {
|
||||
httpHealth
|
||||
};
|
||||
};
|
||||
@@ -1,13 +1,17 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Dialog from '$lib/components/Dialog.svelte';
|
||||
import JsonViewer from '$lib/components/JsonViewer.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Table from '$lib/components/Table.svelte';
|
||||
import Certificate from '$lib/icons/certificate.svelte';
|
||||
import type { HttpEndpoint } from '$lib/interfaces/health';
|
||||
import { daysUntil } from '$lib/utils/conversion';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let columns = ['Domain', 'Status'];
|
||||
let data = [
|
||||
{ Domain: 'laravel-ucm1d.kinsta.app', Status: 'Live' },
|
||||
{ Domain: 'laravel-ucm1d.kinsta.app', Status: 'Live' },
|
||||
{ Domain: 'laravel-ucm1d.kinsta.app', Status: 'Live' }
|
||||
];
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const httpHealth: HttpEndpoint[] = data?.httpHealth;
|
||||
let selectedSSL = $state(null);
|
||||
</script>
|
||||
|
||||
<PageHeader>Health</PageHeader>
|
||||
@@ -15,6 +19,36 @@
|
||||
<Table
|
||||
title="Domains list"
|
||||
description="All of the verified domains below point to this application and are covered by free Cloudflare SSL certificates for a secure HTTPS connection. The DNS records for the domains must be set up correctly for them to work."
|
||||
{columns}
|
||||
{data}
|
||||
/>
|
||||
columns={['Domain', 'SSL', 'Status', 'Code']}
|
||||
>
|
||||
<tbody slot="tbody">
|
||||
{#each httpHealth as row, i (row)}
|
||||
<tr>
|
||||
<td>{row.domain}</td>
|
||||
<td>
|
||||
{#if row['ssl']['valid_to']}
|
||||
<div
|
||||
on:click={() => (selectedSSL = row['ssl'])}
|
||||
style="display: flex; cursor: pointer;"
|
||||
>
|
||||
<div style="height: 1.4rem; width: 1.4rem;">
|
||||
<Certificate />
|
||||
</div>
|
||||
<span>{daysUntil(row['ssl']['valid_to'])} days left</span>
|
||||
</div>
|
||||
{:else}
|
||||
<span>(none)</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>{row.status}</td>
|
||||
<td>{row.code}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
{#if selectedSSL !== null}
|
||||
<Dialog on:close={() => (selectedSSL = null)} title="SSL Certificate info">
|
||||
<JsonViewer json={selectedSSL} />
|
||||
</Dialog>
|
||||
{/if}
|
||||
|
||||
37
src/routes/image/[...catchall]/+server.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
async function fetchImage(src: string) {
|
||||
// const url = new URL(src);
|
||||
const remoteUrl = String(src.match(/http:\/\/[a-z0-9\\.]+:8123\/.*/));
|
||||
const url = new URL(remoteUrl);
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'image/jpeg'
|
||||
}
|
||||
};
|
||||
|
||||
if (url === null) {
|
||||
console.log('url is not valid');
|
||||
return null;
|
||||
}
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then((resp) => resp.blob())
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
console.log('GET');
|
||||
url.pathname = url.pathname.replace('/image/', '');
|
||||
|
||||
const res = await fetchImage(url.href);
|
||||
|
||||
// something went wrong
|
||||
if (res === null) {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
return new Response(res);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import Table from '$lib/components/Table.svelte';
|
||||
@@ -7,22 +8,11 @@
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const { routers } = data;
|
||||
const columns = {
|
||||
entryPoints: 'Entrypoints',
|
||||
name: 'Name',
|
||||
provider: 'Provider',
|
||||
rule: 'Rule',
|
||||
service: 'Service',
|
||||
status: 'Status'
|
||||
};
|
||||
const links: string[] = routers.map((router) => `/network/${router.service}`);
|
||||
|
||||
const providers = [
|
||||
...new Set(
|
||||
routers.map((item) => item.provider).filter((provider) => typeof provider === 'string')
|
||||
)
|
||||
];
|
||||
console.log(routers);
|
||||
</script>
|
||||
|
||||
<PageHeader>Network</PageHeader>
|
||||
@@ -42,7 +32,24 @@
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Table title="Routers" description="Traefik routers available" {columns} data={routers} {links} />
|
||||
<Table
|
||||
title="Routers"
|
||||
description="Traefik routers available"
|
||||
columns={['Entrypoints', 'Name', 'Provider', 'Rule', 'Service', 'Status']}
|
||||
>
|
||||
<tbody slot="tbody">
|
||||
{#each routers as route (route)}
|
||||
<tr on:click={() => goto(`/network/${route.service}`)} class="link">
|
||||
<td>{route.entryPoints}</td>
|
||||
<td>{route.name}</td>
|
||||
<td>{route.provider}</td>
|
||||
<td>{route.rule}</td>
|
||||
<td>{route.service}</td>
|
||||
<td>{route.status}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import JsonViewer from '$lib/components/JsonViewer.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
@@ -9,7 +10,4 @@
|
||||
|
||||
<PageHeader>Network: {router.service}</PageHeader>
|
||||
|
||||
<div>
|
||||
<p>router:</p>
|
||||
<pre><code>{JSON.stringify(router, null, 2)}</code></pre>
|
||||
</div>
|
||||
<JsonViewer json={router} />
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { fetchP1P } from '$lib/server/homeassistant';
|
||||
import { currentFilament } from '$lib/server/filament';
|
||||
import type { Entity } from '$lib/interfaces/homeassistant';
|
||||
import { getAllFilament } from '$lib/server/database';
|
||||
import type { Filament } from '$lib/interfaces/printer';
|
||||
|
||||
export const load: PageServerLoad = async (): Promise<{ p1p: Entity[]; filament: Filament[] }> => {
|
||||
const p1p = await fetchP1P();
|
||||
const filament = currentFilament();
|
||||
interface PrinterState {
|
||||
[key: string]: {
|
||||
value: string;
|
||||
unit?: string;
|
||||
picture?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async (): Promise<{
|
||||
p1p: PrinterState;
|
||||
filament: Filament[];
|
||||
}> => {
|
||||
let p1p;
|
||||
let filament: Filament[];
|
||||
|
||||
try {
|
||||
p1p = await fetchP1P();
|
||||
filament = await getAllFilament();
|
||||
} catch (error) {
|
||||
console.error('error while fetching printer server props');
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return { p1p, filament };
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { capitalizeFirstLetter } from '$lib/utils/string';
|
||||
import { formatTimeLeft } from '$lib/utils/conversion';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import Table from '$lib/components/Table.svelte';
|
||||
import Progress from '$lib/components/Progress.svelte';
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import Dialog from '$lib/components/Dialog.svelte';
|
||||
import FormFilament from '$lib/components/forms/FormFilament.svelte';
|
||||
|
||||
import Finished from '$lib/icons/finished.svelte';
|
||||
import Paused from '$lib/icons/paused.svelte';
|
||||
@@ -15,53 +18,55 @@
|
||||
import PrinterStopped from '$lib/icons/printer-stopped.svelte';
|
||||
import NozzleTemperature from '$lib/icons/temperature-nozzle.svelte';
|
||||
import BedTemperature from '$lib/icons/temperature-bed.svelte';
|
||||
import Search from '$lib/icons/search.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import type { Entity } from '$lib/interfaces/homeassistant';
|
||||
import type { Filament } from '$lib/interfaces/printer';
|
||||
import Weight from '$lib/icons/weight.svelte';
|
||||
import Speed from '$lib/icons/speed.svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import Time from '$lib/icons/time.svelte';
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import PrinterImage from './section_image.svelte';
|
||||
import PrinterAttributes from './section_printer_attributes.svelte';
|
||||
import { formatDateIntl } from '$lib/utils/conversion';
|
||||
import Length from '$lib/icons/Length.svelte';
|
||||
|
||||
interface PrinterState {
|
||||
[key: string]: {
|
||||
value: string;
|
||||
unit?: string;
|
||||
picture?: string;
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const p1p: Entity[] = data?.p1p;
|
||||
const filament: Filament[] = data?.filament;
|
||||
let printer: PrinterState = $state(data?.p1p);
|
||||
let filamentFilter = $state('');
|
||||
let secondsLeft = $state(0);
|
||||
let open = $state(false);
|
||||
let timeLeftInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
const currentStage = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_current_stage'
|
||||
)[0];
|
||||
const printStatus = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_print_status'
|
||||
)[0];
|
||||
const bedTemp = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_bed_temperature'
|
||||
)[0];
|
||||
const nozzleTemp = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_nozzle_temperature'
|
||||
)[0];
|
||||
const totalUsage = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_total_usage'
|
||||
)[0];
|
||||
const nozzleType = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_nozzle_type'
|
||||
)[0];
|
||||
const nozzleSize = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_nozzle_size'
|
||||
)[0];
|
||||
const bedType = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_print_bed_type'
|
||||
)[0];
|
||||
const currentLayer = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_current_layer'
|
||||
)[0];
|
||||
const totalLayer = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_total_layer_count'
|
||||
)[0];
|
||||
const progress = p1p.filter(
|
||||
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_print_progress'
|
||||
)[0];
|
||||
const rawFilament: Filament[] = data?.filament || [];
|
||||
let filament = $derived(
|
||||
rawFilament
|
||||
?.filter(
|
||||
(f: Filament) =>
|
||||
f.color.toLowerCase().includes(filamentFilter) ||
|
||||
f.material.toLowerCase().includes(filamentFilter)
|
||||
)
|
||||
.sort((a, b) => (a.updated > b.updated ? -1 : 1))
|
||||
);
|
||||
|
||||
console.log(p1p);
|
||||
function reloadProps() {
|
||||
invalidateAll().then(() => (printer = data?.p1p));
|
||||
}
|
||||
|
||||
let columns = ['Hex', 'Color', 'Material', 'Weight', 'Count', 'Link'];
|
||||
const links = filament.map((f) => `/printer/${f.Color.replaceAll(' ', '-').toLowerCase()}`);
|
||||
// console.log(p1p);
|
||||
|
||||
const filamentLink = (f: Filament) =>
|
||||
`/printer/filament/${f.color.replaceAll(' ', '-').toLowerCase()}`;
|
||||
|
||||
const iconDictState = { running: Printing, pause: Paused, failed: Stopped, finish: Finished };
|
||||
const iconDictStage = {
|
||||
idle: PrinterIdle,
|
||||
printing: PrinterPrinting,
|
||||
@@ -71,7 +76,16 @@
|
||||
cleaning_nozzle_tip: PrinterPrinting,
|
||||
homing_toolhead: PrinterPrinting
|
||||
};
|
||||
const iconDictState = { running: Printing, pause: Paused, failed: Stopped, finish: Finished };
|
||||
|
||||
function isObjKey<T>(key: PropertyKey, obj: T): key is keyof T {
|
||||
return key in obj;
|
||||
}
|
||||
|
||||
const stateToIcon = (key: string) => {
|
||||
if (!isObjKey(key, iconDictStage)) return;
|
||||
|
||||
return iconDictStage[key];
|
||||
};
|
||||
|
||||
interface FilamentUpdated {
|
||||
date: Date;
|
||||
@@ -88,6 +102,32 @@
|
||||
year: 'numeric'
|
||||
})
|
||||
.toLowerCase();
|
||||
|
||||
function updateTimeLeft() {
|
||||
if (secondsLeft <= 0) {
|
||||
clearInterval(timeLeftInterval);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = new Date(printer['end_time']?.value).getTime() - now.getTime();
|
||||
secondsLeft = Math.max(Math.floor(diffMs / 1000), 0);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// only poll status updates if not idle
|
||||
if (printer['print_status']?.value === 'idle') return;
|
||||
updateTimeLeft();
|
||||
|
||||
timeLeftInterval = setInterval(updateTimeLeft, 1000);
|
||||
const refreshStateInterval = setInterval(reloadProps, 5000);
|
||||
|
||||
return () =>
|
||||
Promise.all([clearInterval(timeLeftInterval), clearInterval(refreshStateInterval)]);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(timeLeftInterval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<PageHeader>Printer</PageHeader>
|
||||
@@ -97,110 +137,145 @@
|
||||
title="Printer status"
|
||||
description="Historical printer information, last prints and current status."
|
||||
>
|
||||
<div slot="top-left">
|
||||
<button on:click={reloadProps}><span>Reload</span></button>
|
||||
</div>
|
||||
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Current stage</label>
|
||||
<span
|
||||
><span class="icon">
|
||||
<svelte:component this={iconDictStage[currentStage.state]} />
|
||||
</span>{currentStage.state}</span
|
||||
<svelte:component this={stateToIcon(printer['current_stage']?.value || '')} />
|
||||
</span>{printer['current_stage']?.value}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Bed temperature</label>
|
||||
<label>Bed temp</label>
|
||||
<span
|
||||
><span class="icon"><BedTemperature /></span>{bedTemp.state}
|
||||
{bedTemp.attributes.unit_of_measurement}</span
|
||||
><span class="icon"><BedTemperature /></span>{printer['bed_temperature']?.value}
|
||||
{printer['bed_temperature']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Nozzle temperature</label>
|
||||
<label>Nozzle temp</label>
|
||||
<span
|
||||
><span class="icon"><NozzleTemperature /></span>{nozzleTemp.state}
|
||||
{nozzleTemp.attributes.unit_of_measurement}</span
|
||||
><span class="icon"><NozzleTemperature /></span>{printer['nozzle_temperature']?.value}
|
||||
{printer['nozzle_temperature']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Speed profile</label>
|
||||
<span
|
||||
><span class="icon"><Speed /></span>{printer['speed_profile']?.value}
|
||||
{printer['speed_profile']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Print weight</label>
|
||||
<span
|
||||
><span class="icon" style="--size: 1.8rem"><Weight /></span>{printer['print_weight']
|
||||
?.value}
|
||||
{printer['print_weight']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Print length</label>
|
||||
<span
|
||||
><span class="icon"><Length /></span>{printer['print_length']?.value}
|
||||
{printer['print_length']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Print status</label>
|
||||
<span>
|
||||
<span class={`icon ${printStatus?.state === 'running' ? 'spin' : ''}`}>
|
||||
<svelte:component this={iconDictState[printStatus.state]} /></span
|
||||
<span class={`icon ${printer['print_status']?.value === 'running' ? 'spin' : ''}`}>
|
||||
<svelte:component this={iconDictState[printer['print_status']?.value]} /></span
|
||||
>
|
||||
{printStatus.state}
|
||||
{printer['print_status']?.value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Time left</label>
|
||||
<span><span class="icon"><Time /></span>{formatTimeLeft(secondsLeft)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<Progress value={progress.state} />
|
||||
{#if currentLayer.state !== totalLayer.state}
|
||||
<span>Currently printing layer line {currentLayer.state} of {totalLayer.state}</span>
|
||||
<Progress value={printer['print_progress']?.value} />
|
||||
|
||||
{#if printer['current_layer']?.value !== printer['total_layer_count']?.value}
|
||||
<span
|
||||
>Currently printing layer line {printer['current_layer']?.value} of {printer[
|
||||
'total_layer_count'
|
||||
]?.value}</span
|
||||
>
|
||||
{:else}
|
||||
<span>Finished printing {currentLayer.state} of {totalLayer.state} layers!</span>
|
||||
<span
|
||||
>Finished printing {printer['current_layer']?.value} of {printer['total_layer_count']
|
||||
?.value} layers!</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Printer attributes"
|
||||
description="Historical printer information, last prints and current status."
|
||||
>
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Total print time</label>
|
||||
<span>
|
||||
{Math.floor(Number(totalUsage.state) * 10) / 10}
|
||||
<!-- {formatDuration(totalUsage.state * 3600)} -->
|
||||
{totalUsage.attributes.unit_of_measurement}</span
|
||||
>
|
||||
</div>
|
||||
<PrinterImage data={printer} />
|
||||
|
||||
<div class="section-element">
|
||||
<label>Nozzle Type</label>
|
||||
<span
|
||||
>{capitalizeFirstLetter(nozzleType.state.replaceAll('_', ' '))}
|
||||
{nozzleType.attributes.unit_of_measurement}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Nozzle Size</label>
|
||||
<span>{nozzleSize?.state} {nozzleSize.attributes.unit_of_measurement}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Bed type</label>
|
||||
<span
|
||||
>{capitalizeFirstLetter(bedType?.state?.replaceAll('_', ' ') || 'not found')}
|
||||
{bedType?.attributes.unit_of_measurement}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img src="/printer.png" />
|
||||
</Section>
|
||||
<PrinterAttributes data={printer} />
|
||||
|
||||
<Table
|
||||
title="Filaments"
|
||||
description={`${filament.length} colors are currently in stock. Overview of currently stocked filament.`}
|
||||
{columns}
|
||||
columns={['Color', 'Details', 'Last bought']}
|
||||
data={filament}
|
||||
{links}
|
||||
footer={`Last updated on ${lastUpdateFilament.title}`}
|
||||
/>
|
||||
>
|
||||
<div slot="actions" class="filament-table-inputs">
|
||||
<div>
|
||||
<Input placeholder="Filter filaments" icon={Search} bind:value={filamentFilter} />
|
||||
</div>
|
||||
|
||||
<button class="affirmative" on:click={() => (open = true)}><span>Add new</span></button>
|
||||
</div>
|
||||
|
||||
<tbody slot="tbody">
|
||||
{#each filament as row, i (row)}
|
||||
<tr class="link" on:click={() => goto(filamentLink(row))}>
|
||||
<td><span class="color" style={`background: ${row.hex}`} /></td>
|
||||
<td class="info">
|
||||
<h2>{row.material} in {row.color}</h2>
|
||||
<div class="meta">
|
||||
<span>Roll:	 {row.weight}</span>
|
||||
<span>Color:	 {row.hex}</span>
|
||||
<span>Last bought:	 {formatTimeLeft(row.updated / 1000)}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>{formatDateIntl(new Date(row.updated * 1000))}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
img {
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
top: 1.2rem;
|
||||
right: 1.2rem;
|
||||
}
|
||||
{#if open}
|
||||
<Dialog
|
||||
on:close={() => (open = false)}
|
||||
title="Add new filament"
|
||||
description="You can select anything deployed in <b>Belgium (europe-west1) datacenter</b> and create an internal connection with your service."
|
||||
>
|
||||
<FormFilament on:close={() => (open = false)} />
|
||||
</Dialog>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.section-element {
|
||||
.icon {
|
||||
display: inline-block;
|
||||
@@ -234,4 +309,58 @@
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.filament-table-inputs {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
> div {
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
> button {
|
||||
flex: unset;
|
||||
height: 2.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* filament table */
|
||||
tr td {
|
||||
&:first-of-type {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
&.info {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
padding-left: 25px;
|
||||
|
||||
h2 {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 0.45em;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 300;
|
||||
color: #1c1b1b;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
flex-direction: column;
|
||||
color: #6a6a6a;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
.color {
|
||||
--size: 4rem;
|
||||
display: block;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: var(--border-radius, 1rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { filamentByColor } from '$lib/server/filament';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
let { id } = params;
|
||||
if (id) {
|
||||
id = id.replaceAll('-', ' ');
|
||||
}
|
||||
|
||||
const filament = filamentByColor(id);
|
||||
return { id, filament };
|
||||
};
|
||||
13
src/routes/printer/filament/[id]/+page.server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getFilamentByColor } from '$lib/server/database';
|
||||
|
||||
export const load = async ({ params }: Parameters<PageServerLoad>[0]) => {
|
||||
let { id } = params;
|
||||
if (id) {
|
||||
id = id.replaceAll('-', ' ');
|
||||
}
|
||||
|
||||
const filament = await getFilamentByColor(id);
|
||||
console.log('fil:', filament);
|
||||
return { id, filament: filament };
|
||||
};
|
||||
@@ -6,11 +6,11 @@
|
||||
const filament = data?.filament;
|
||||
</script>
|
||||
|
||||
{#if filament !== null}
|
||||
<PageHeader>Filament: {filament?.Color}</PageHeader>
|
||||
{#if filament != null}
|
||||
<PageHeader>Filament: {filament?.color}</PageHeader>
|
||||
|
||||
<div class="page">
|
||||
<div class="color-block" style={`background: ${filament?.Hex}`}></div>
|
||||
<div class="color-block" style={`background: ${filament?.hex}`}></div>
|
||||
</div>
|
||||
{:else}
|
||||
<PageHeader>Filament not found!</PageHeader>
|
||||
39
src/routes/printer/section_image.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import LiveImage from '$lib/components/LiveImage.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
|
||||
let { data }: { data: any } = $props();
|
||||
</script>
|
||||
|
||||
<Section
|
||||
title="Current print"
|
||||
description="Historical printer information, last prints and current status."
|
||||
>
|
||||
<div class="images">
|
||||
<div>
|
||||
<h2>Camera</h2>
|
||||
<LiveImage imageUrl={`http://homeassistant.schleppe:8123${data['camera']?.picture}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Model</h2>
|
||||
<img src={`http://homeassistant.schleppe:8123${data['cover_image']?.picture}`} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Pick image</h2>
|
||||
<img src={`http://homeassistant.schleppe:8123${data['pick_image']?.picture}`} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<style lang="scss">
|
||||
.images {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
img {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
77
src/routes/printer/section_printer_attributes.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import { capitalizeFirstLetter } from '$lib/utils/string';
|
||||
|
||||
let { data }: { data: any } = $props();
|
||||
</script>
|
||||
|
||||
<Section
|
||||
title="Printer attributes"
|
||||
description="Historical printer information, last prints and current status."
|
||||
>
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Total print time</label>
|
||||
<span>
|
||||
{Math.floor(Number(data['total_usage']?.value) * 10) / 10}
|
||||
<!-- {formatDuration(totalUsage.value * 3600)} -->
|
||||
{data['total_usage']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Total print length</label>
|
||||
<span>
|
||||
{Math.floor(Number(data['print_length']?.value) * 10) / 10}
|
||||
{data['print_length']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Nozzle Type</label>
|
||||
<span
|
||||
>{capitalizeFirstLetter(data?.['nozzle_type']?.value?.replaceAll('_', ' '))}
|
||||
{data['nozzle_type']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Nozzle Size</label>
|
||||
<span>{data['nozzle_size']?.value} {data['nozzle_size']?.unit}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Bed type</label>
|
||||
<span
|
||||
>{capitalizeFirstLetter(data['print_bed_type']?.value?.replaceAll('_', ' ') || 'not found')}
|
||||
{data['print_bed_type']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>SD Card status</label>
|
||||
<span
|
||||
>{data['sd_card_status']?.value}
|
||||
{data['sd_card_status']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>WiFi signal</label>
|
||||
<span>{data['wi_fi_signal']?.value} {data['wi_fi_signal']?.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="printer-image">
|
||||
<img src="/printer.png" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<style lang="scss">
|
||||
.printer-image img {
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
top: 1.2rem;
|
||||
right: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,64 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import ThumbnailButton from '$lib/components/ThumbnailButton.svelte';
|
||||
|
||||
interface Site {
|
||||
title: string;
|
||||
image: string;
|
||||
link: string;
|
||||
background?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const sites: Array<Site> = [
|
||||
{
|
||||
title: 'Grafana',
|
||||
image:
|
||||
'https://www.stackhero.io/assets/src/images/servicesLogos/openGraphVersions/grafana.png',
|
||||
link: 'https://grafana.schleppe.cloud',
|
||||
background: '#082448',
|
||||
color: 'white'
|
||||
},
|
||||
{
|
||||
title: 'Prometheus',
|
||||
image: 'https://prometheus.io/_next/static/media/prometheus-logo.7aa022e5.svg',
|
||||
link: 'http://prome.schleppe:9090'
|
||||
},
|
||||
{
|
||||
title: 'Traefik',
|
||||
image: 'https://storage.googleapis.com/schleppe-files/Traefik.logo_shape_bordered.png',
|
||||
link: 'https://grafana.schleppe.cloud',
|
||||
background: '#30A4C2',
|
||||
color: '#343A40'
|
||||
},
|
||||
{
|
||||
title: 'Kibana',
|
||||
image: 'https://marketplace-assets.digitalocean.com/logos/sharklabs-kibana.svg',
|
||||
link: 'https://kibana.schleppe.cloud'
|
||||
},
|
||||
{
|
||||
title: 'HASS',
|
||||
image:
|
||||
'https://upload.wikimedia.org/wikipedia/en/thumb/4/49/Home_Assistant_logo_%282023%29.svg/2048px-Home_Assistant_logo_%282023%29.svg.png',
|
||||
link: 'http://homeassistant.schleppe:8123'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<PageHeader>Sites</PageHeader>
|
||||
|
||||
<div class="section-wrapper">
|
||||
{#each sites as site}
|
||||
<ThumbnailButton
|
||||
title={site.title}
|
||||
image={site.image}
|
||||
background={site.background}
|
||||
color={site.color}
|
||||
link={site.link}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<Section
|
||||
title="Expose HTTP traffic"
|
||||
description="You can reach your Application on a specific Port you configure, redirecting all your domains to it. You can make it Private by disabling HTTP traffic."
|
||||
@@ -26,3 +79,10 @@
|
||||
description="Connected services can communicate with your application over the private network."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.section-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
</style>
|
||||
|
||||
BIN
static/fonts/NormanVariable-Regular.woff2
Normal file
BIN
static/fonts/StopSN-Display.woff2
Normal file
@@ -1,3 +1,9 @@
|
||||
@font-face {
|
||||
font-family: 'Reckless Neue';
|
||||
font-style: normal;
|
||||
src: url('/fonts/RecklessNeue-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
@@ -6,6 +12,22 @@
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Stop';
|
||||
font-style: normal;
|
||||
src: url('/fonts/StopSN-Display.woff2') format('woff2');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Norman';
|
||||
font-style: normal;
|
||||
src: url('/fonts/NormanVariable-Regular.woff2') format('woff2');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #f9f5f3;
|
||||
--color: #1c1819;
|
||||
@@ -37,7 +59,10 @@ a:visited {
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Reckless Neue';
|
||||
font-family: 'Stop', 'Reckless Neue';
|
||||
font-family: 'Norman', 'Reckless Neue';
|
||||
letter-spacing: 2.2px;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@@ -69,27 +94,43 @@ button {
|
||||
border: none;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
height: unset;
|
||||
height: 100%;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
height: 100%;
|
||||
min-height: 1.5rem;
|
||||
padding: 0 0.5rem;
|
||||
margin-left: -0.5rem;
|
||||
border: 1px solid #eaddd5;
|
||||
border-radius: inherit;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
transition: all ease-in-out 0.2s;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
button.affirmative span {
|
||||
background-color: #1c1819;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.affirmative:hover span {
|
||||
background-color: #363132;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button::after {
|
||||
@@ -107,7 +148,49 @@ button::after {
|
||||
|
||||
button:hover span {
|
||||
border-color: #cab2aa;
|
||||
background: #f9f5f3 !important;
|
||||
background: #f9f5f3;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: sans-serif;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
table th {
|
||||
font-weight: 500;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
font-stretch: 2px;
|
||||
border-bottom: 1px solid #eaddd5;
|
||||
}
|
||||
|
||||
table td {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
table tr:not(table tr:last-of-type) {
|
||||
border-bottom: 1px solid #eaddd5;
|
||||
}
|
||||
|
||||
table tr:hover > td {
|
||||
background-color: var(--highlight);
|
||||
background-color: #f5ede9;
|
||||
}
|
||||
|
||||
table tr.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
@@ -126,7 +209,9 @@ button:hover span {
|
||||
|
||||
.section-row {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 2rem;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
.section-element {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
// import adapter from "svelte-adapter-bun";
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
|
||||
29
varnish/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
FROM debian:bullseye
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl gnupg apt-transport-https lsb-release \
|
||||
build-essential git autoconf libtool python3-docutils \
|
||||
libmhash-dev pkg-config
|
||||
|
||||
# Add Varnish Software repo
|
||||
RUN curl -fsSL https://packagecloud.io/varnishcache/varnish73/gpgkey | gpg --dearmor -o /usr/share/keyrings/varnish.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/varnish.gpg] https://packagecloud.io/varnishcache/varnish73/debian/ $(lsb_release -cs) main" \
|
||||
> /etc/apt/sources.list.d/varnish.list
|
||||
|
||||
# Install Varnish + dev headers
|
||||
RUN apt-get update && apt-get install -y varnish varnish-dev
|
||||
|
||||
# Build libvmod-digest
|
||||
RUN git clone https://github.com/varnish/libvmod-digest.git /opt/libvmod-digest && \
|
||||
cd /opt/libvmod-digest && \
|
||||
./autogen.sh && \
|
||||
./configure VARNISHSRC=/usr/include/varnish && \
|
||||
make && make install
|
||||
|
||||
COPY default.vcl /etc/varnish/default.vcl
|
||||
|
||||
EXPOSE 6081
|
||||
|
||||
CMD ["varnishd", "-F", "-f", "/etc/varnish/default.vcl", "-a", ":6081", "-s", "malloc,512m"]
|
||||
|
||||
35
yarn.lock
@@ -685,6 +685,11 @@
|
||||
"@typescript-eslint/types" "8.29.1"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@zerodevx/svelte-json-view@^1.0.11":
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@zerodevx/svelte-json-view/-/svelte-json-view-1.0.11.tgz#39d33dd066f5442ad58b5dffcf8f958a6b80e4ea"
|
||||
integrity sha512-mIjj0H1al/P4FPlbeDoiey93lNEUqBEAe5LIdD5GttZfEYt3awexD2lHwKNfUeY4jHizOJkoWTPN/2iO0GBqpw==
|
||||
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
@@ -1224,6 +1229,16 @@ globals@^14.0.0:
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
|
||||
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
|
||||
|
||||
globalyzer@0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465"
|
||||
integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==
|
||||
|
||||
globrex@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
|
||||
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
|
||||
|
||||
gopd@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||
@@ -1963,6 +1978,11 @@ sprintf-js@^1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"
|
||||
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
|
||||
|
||||
sqlite@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/sqlite/-/sqlite-5.1.1.tgz#26a6a200fdac490643880af2b6cb3940ada59274"
|
||||
integrity sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==
|
||||
|
||||
stream-buffers@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.3.tgz#9fc6ae267d9c4df1190a781e011634cac58af3cd"
|
||||
@@ -1992,6 +2012,13 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
svelte-adapter-bun@^0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/svelte-adapter-bun/-/svelte-adapter-bun-0.5.2.tgz#92c9984bb1555454e35e7732196cbf1cd5cedf40"
|
||||
integrity sha512-xEtFgaal6UgrCwwkSIcapO9kopoFNUYCYqyKCikdqxX9bz2TDYnrWQZ7qBnkunMxi1HOIERUCvTcebYGiarZLA==
|
||||
dependencies:
|
||||
tiny-glob "^0.2.9"
|
||||
|
||||
svelte-check@^4.0.0:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-4.1.5.tgz#afdb3f8050c123064124d5aa7821365c7befa7a4"
|
||||
@@ -2064,6 +2091,14 @@ tar@^7.0.0:
|
||||
mkdirp "^3.0.1"
|
||||
yallist "^5.0.0"
|
||||
|
||||
tiny-glob@^0.2.9:
|
||||
version "0.2.9"
|
||||
resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2"
|
||||
integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==
|
||||
dependencies:
|
||||
globalyzer "0.1.0"
|
||||
globrex "^0.1.2"
|
||||
|
||||
tmp-promise@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7"
|
||||
|
||||