source, static files & Dockerfile

This commit is contained in:
2025-04-08 21:47:35 +02:00
parent 24ab595ab3
commit 68ebc7568e
92 changed files with 4348 additions and 0 deletions

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
PROXMOX_URL=
PROXMOX_TOKEN=
HOMEASSISTANT_URL=
HOMEASSISTANT_TOKEN=
TRAEFIK_URL=

23
Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
FROM node:22-alpine3.20 AS builder
WORKDIR /app
COPY src/ src
COPY static/ static
COPY package.json yarn.lock svelte.config.js tsconfig.json vite.config.ts .
RUN yarn
RUN yarn build
FROM node:22-alpine3.20
WORKDIR /opt/infra-map
COPY --from=builder /app/build build
COPY package.json .
RUN yarn
EXPOSE 3000
ENV NODE_ENV=production
CMD [ "node", "build" ]

38
README.md Normal file
View File

@@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

13
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

13
src/app.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="stylesheet" href="%sveltekit.assets%/style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

10
src/hooks.server.ts Normal file
View File

@@ -0,0 +1,10 @@
import type { Handle } from '@sveltejs/kit';
import { dev } from '$app/environment';
export const handle: Handle = async ({ event, resolve }) => {
if (dev) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
return await resolve(event);
};

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import Pod from './Pod.svelte';
import type { V1DaemonSet } from '@kubernetes/client-node';
export let daemon: V1DaemonSet;
let { metadata, status, pods } = daemon;
const healthy =
status?.desiredNumberScheduled && status?.desiredNumberScheduled === status?.numberReady;
</script>
<div class="card-container">
<div class="namespace">
<h2>{pods?.length} of {metadata?.name} in {metadata?.namespace}</h2>
</div>
<p>heatlthy: {healthy}</p>
<div class="card-wrapper">
{#each daemon?.pods as pod, i (pod)}
<Pod parent={daemon} {pod} {i} />
{/each}
</div>
</div>
<style lang="scss">
.card-container {
background-color: #cab2aa40;
border-radius: 0.5rem;
width: 100%;
padding: 0.75rem;
.namespace {
width: 100%;
display: block;
}
.card-wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
}
.positive {
color: #077c35;
}
</style>

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import Pod from './Pod.svelte';
import type { V1Deployment } from '@kubernetes/client-node';
export let deploy: V1Deployment;
let { metadata, pods } = deploy;
</script>
<div class="card-container">
<div class="namespace">
<h2>{metadata?.name} in {metadata?.namespace}</h2>
</div>
<div class="card-wrapper">
{#each pods as pod, i (pod)}
<Pod parent={deploy} {pod} {i} />
{/each}
</div>
</div>
<style lang="scss">
.card-container {
background-color: #cab2aa40;
border-radius: 0.5rem;
width: 100%;
padding: 0.75rem;
.namespace {
width: 100%;
display: block;
}
.card-wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
}
</style>

View File

@@ -0,0 +1,115 @@
<script lang="ts">
import { page } from '$app/stores';
import { derived } from 'svelte/store';
// Create a derived store to extract breadcrumb data
const breadcrumbs = derived(page, ($page) => {
const segments = $page.url.pathname.split('/').filter(Boolean); // Remove empty segments
return segments.map((segment, index) => {
let label = decodeURI(segment);
// if not uuid pattern, this is weird order of ops
/*
if (!segment.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {
label = label.replace(/-/g, ' ')
}
*/
return {
label,
path: '/' + segments.slice(0, index + 1).join('/')
};
});
});
</script>
<div class="header">
<div class="left">
<!-- <img src="/logo.png" /> -->
<h1>schleppe.cloud</h1>
</div>
<div class="middle crumbs">
<a href="/">Home</a>
{#each $breadcrumbs as crumb (crumb.label)}
<span class="seperator">/</span>
<a href={crumb.path}>{crumb.label}</a>
{/each}
</div>
<div class="right">
<span>User profile</span>
</div>
</div>
<style lang="scss">
.header {
position: sticky;
top: 1rem;
left: 0;
display: grid;
grid-template-columns: 240px 1fr auto;
grid-template-areas: 'logoSection siteAndEnvironment profileAndHelp';
align-items: center;
background: #1c1819;
padding: 0 1rem;
border-radius: 6px;
color: white;
margin: 1rem 0.5rem 0 0.5rem;
font-weight: 400;
font-size: 1rem;
z-index: 100;
&::after {
content: '';
position: absolute;
width: 100%;
height: 1rem;
top: -1rem;
background-color: var(--bg);
/* opacity: 0.6; */
}
h1 {
font-size: 1.5rem;
padding: 0;
font-weight: 300;
}
img {
padding: 0rem 0;
max-height: 2.5rem;
}
a,
span {
color: inherit;
}
.left,
.middle,
.right {
min-height: 3.5rem;
display: flex;
align-items: center;
}
.left {
}
.crumbs {
margin-left: 0.6rem;
li {
display: block;
}
.seperator {
color: #7d6665;
padding: 0 0.75rem;
}
}
}
</style>

View File

@@ -0,0 +1,63 @@
<script lang="ts">
export let conditions: any;
const dict = {
Initialized: 'Initialized',
PodScheduled: 'Scheduled',
Ready: 'Ready',
ContainersReady: 'AllReady'
};
</script>
<div class="lifecycle-container">
{#each conditions as condition (condition)}
<div class="step" title={`Date: ${condition.lastTransitionTime}`}>
<span class="type">{dict[condition.type]}</span>
<span class="status">{condition.status === 'True' ? 'yes' : 'no'}</span>
</div>
<span class="divider"></span>
{/each}
</div>
<style lang="scss">
.lifecycle-container {
display: flex;
justify-content: space-between;
width: calc(100% - 1.5rem);
padding: 0.75rem;
}
.divider:not(:last-of-type) {
height: 10px;
width: 100%;
top: 2rem;
margin-top: auto;
margin-bottom: 0.5rem;
background-color: var(--positive);
opacity: 0.6;
/*background-color: #0A7C35;*/
}
.step {
display: flex;
flex-direction: column;
position: relative;
.type {
margin-bottom: 0.3rem;
font-weight: 500;
}
.status {
background-color: var(--positive);
opacity: 0.6;
/*background-color: #0a7c35;*/
border-radius: 0.4rem;
padding: 0.2rem;
padding-bottom: 0.3rem;
text-align: center;
font-weight: 300;
position: relative;
}
}
</style>

View File

@@ -0,0 +1,150 @@
<script lang="ts">
const logs1 = `+ yarn build
yarn run v1.22.22
$ vite build
▲ [WARNING] Cannot find base config file "./.svelte-kit/tsconfig.json" [tsconfig.json]
tsconfig.json:2:12:
2 │ "extends": "./.svelte-kit/tsconfig.json",
╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
vite v4.4.7 building SSR bundle for production...
transforming...
"swipe" is imported from external module "svelte-gestures" but never used in "src/lib/components/Modal.svelte".
✓ 139 modules transformed.
rendering chunks...
vite v4.4.7 building for production...
transforming...
✓ 136 modules transformed.
rendering chunks...
computing gzip size...
.svelte-kit/output/client/_app/version.json 0.03 kB │ gzip: 0.05 kB
.svelte-kit/output/client/vite-manifest.json 6.06 kB │ gzip: 0.82 kB
.svelte-kit/output/client/_app/immutable/assets/2.797db896.css 0.56 kB │ gzip: 0.21 kB
.svelte-kit/output/client/_app/immutable/assets/LowBattery.3e0e6805.css 1.34 kB │ gzip: 0.50 kB
.svelte-kit/output/client/_app/immutable/assets/0.4ae3ad4f.css 1.40 kB │ gzip: 0.54 kB
.svelte-kit/output/client/_app/immutable/assets/4.d8e69ad3.css 3.84 kB │ gzip: 0.99 kB
.svelte-kit/output/client/_app/immutable/assets/Graph.833c02c6.css 6.60 kB │ gzip: 1.42 kB
.svelte-kit/output/client/_app/immutable/chunks/each.e59479a4.js 0.09 kB │ gzip: 0.10 kB
.svelte-kit/output/client/_app/immutable/chunks/store.9bf92701.js 0.19 kB │ gzip: 0.16 kB
.svelte-kit/output/client/_app/immutable/chunks/stores.458b7cfe.js 0.24 kB │ gzip: 0.17 kB
.svelte-kit/output/client/_app/immutable/nodes/5.f2fe2a9e.js 0.44 kB │ gzip: 0.32 kB
.svelte-kit/output/client/_app/immutable/nodes/1.03b18b61.js 0.84 kB │ gzip: 0.52 kB
.svelte-kit/output/client/_app/immutable/chunks/index.4b915d9e.js 0.93 kB │ gzip: 0.58 kB
.svelte-kit/output/client/_app/immutable/nodes/3.60efb886.js 1.89 kB │ gzip: 0.89 kB
.svelte-kit/output/client/_app/immutable/chunks/index.e50902ae.js 2.29 kB │ gzip: 1.20 kB
.svelte-kit/output/client/_app/immutable/chunks/singletons.69e9afd3.js 2.46 kB │ gzip: 1.26 kB
.svelte-kit/output/client/_app/immutable/chunks/hive.5645a370.js 4.68 kB │ gzip: 2.37 kB
.svelte-kit/output/client/_app/immutable/chunks/scheduler.d05fc3de.js 6.27 kB │ gzip: 2.52 kB
.svelte-kit/output/client/_app/immutable/entry/app.566aae85.js 7.82 kB │ gzip: 2.59 kB
.svelte-kit/output/client/_app/immutable/chunks/WeightChanged.86ed6639.js 9.66 kB │ gzip: 3.58 kB
.svelte-kit/output/client/_app/immutable/nodes/4.9e87f003.js 12.31 kB │ gzip: 4.21 kB
.svelte-kit/output/client/_app/immutable/nodes/0.0875ba29.js 16.68 kB │ gzip: 6.97 kB
.svelte-kit/output/client/_app/immutable/chunks/LowBattery.ced8ef00.js 18.26 kB │ gzip: 5.97 kB
.svelte-kit/output/client/_app/immutable/entry/start.cf058992.js 24.06 kB │ gzip: 9.55 kB
.svelte-kit/output/client/_app/immutable/nodes/2.02031f29.js 24.52 kB │ gzip: 7.00 kB
.svelte-kit/output/client/_app/immutable/chunks/Graph.ed26d13c.js 238.67 kB │ gzip: 81.35 kB
✓ built in 4.13s
.svelte-kit/output/server/vite-manifest.json 3.81 kB
.svelte-kit/output/server/_app/immutable/assets/_page.797db896.css 0.56 kB
.svelte-kit/output/server/_app/immutable/assets/LowBattery.3e0e6805.css 1.34 kB
.svelte-kit/output/server/_app/immutable/assets/_layout.4ae3ad4f.css 1.40 kB
.svelte-kit/output/server/_app/immutable/assets/_page.d8e69ad3.css 3.84 kB
.svelte-kit/output/server/_app/immutable/assets/Graph.d70e5aa3.css 6.58 kB
.svelte-kit/output/server/internal.js 0.19 kB
.svelte-kit/output/server/chunks/store.js 0.19 kB
.svelte-kit/output/server/entries/pages/settings/_page.svelte.js 0.26 kB
.svelte-kit/output/server/entries/fallbacks/error.svelte.js 0.47 kB
.svelte-kit/output/server/chunks/stores.js 0.52 kB
.svelte-kit/output/server/entries/pages/alarms/_page.svelte.js 1.14 kB
.svelte-kit/output/server/chunks/index.js 2.58 kB
.svelte-kit/output/server/chunks/hive.js 4.23 kB
.svelte-kit/output/server/chunks/ssr.js 5.07 kB
.svelte-kit/output/server/chunks/internal.js 5.52 kB
.svelte-kit/output/server/chunks/WeightChanged.js 8.14 kB
.svelte-kit/output/server/entries/pages/dashboard/_page.svelte.js 10.25 kB
.svelte-kit/output/server/entries/pages/_layout.svelte.js 12.09 kB
.svelte-kit/output/server/chunks/LowBattery.js 15.20 kB
.svelte-kit/output/server/entries/pages/_page.svelte.js 15.97 kB
.svelte-kit/output/server/chunks/Graph.js 28.69 kB
.svelte-kit/output/server/index.js 87.83 kB
Run npm run preview to preview your production build locally.
> Using @sveltejs/adapter-static
Wrote site to "build"
✔ done
✓ built in 11.35s
Done in 13.18s.`;
export let logs: string[] = logs1.split('\n');
export let lineNumbers = true;
export let stream = false;
let codeElement: HTMLElement;
$: {
console.log(logs.length);
if (codeElement) {
codeElement.scroll({
top: 0,
left: 0,
behavior: 'smooth'
});
}
}
</script>
<div class="parent" style="margin-bottom: 2rem">
<div class="header"><button>follow</button></div>
<div class="code" bind:this={codeElement}>
<code>
{#each logs as log, i (log + i)}
<pre>{#if lineNumbers}<span class="line-number">{i + 1}</span>{/if}{log}</pre>
{/each}
</code>
</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;
code {
display: flex;
flex-direction: column-reverse;
scroll-margin: 1rem;
}
pre {
display: inline-block;
margin: 0;
line-height: 1.2rem;
}
}
.line-number {
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
display: inline-block;
min-width: 1.5rem;
padding-right: 1.5rem;
text-align: right;
z-index: -1;
}
</style>

View File

@@ -0,0 +1,201 @@
<script lang="ts">
import Connection from '$lib/icons/connection.svelte';
import Network from '$lib/icons/network.svelte';
import Layers from '$lib/icons/layers.svelte';
import Clock from '$lib/icons/clock.svelte';
import { convertKiToHumanReadable } from '$lib/utils/conversion';
export let node;
let { metadata, pods, status } = node;
</script>
<div class="card">
<div class="header">
<div class="icon"><Connection /></div>
<span class="name">{metadata.name}</span>
<!--
<span class={`status ${node?.online === 1 ? 'ok' : 'error'}`}></span>
-->
</div>
<div class="resource">
<div class="title">
<Network />
<span>Status</span>
</div>
<span>{status.phase}</span>
<div class="title">
<Network />
<span>IP address</span>
</div>
<span>{status.addresses[0].address}</span>
<div class="title">
<Layers />
<span>Pods</span>
</div>
<span>{pods.length}</span>
<div class="title">
<Connection />
<span>CPUs allocated</span>
</div>
<span>{status.capacity.cpu}</span>
<div class="title">
<Clock />
<span>Memory allocaed</span>
</div>
<span>{convertKiToHumanReadable(status.capacity.memory)}</span>
<!--
<span>{uptime}</span>
<Lifecycle {conditions} />
-->
</div>
<!--
<div class="footer">
{#each buttons as btn (btn)}
<button on:click={() => console.log(node)}>
<span>{btn}</span>
</button>
{/each}
</div>
-->
</div>
<style lang="scss">
.card {
flex-grow: 1;
max-width: 550px;
background: #fbf6f4;
box-shadow: var(
--str-shadow-s,
0px 0px 2px #22242714,
0px 1px 4px #2224271f,
0px 4px 8px #22242729
);
pointer-events: all;
cursor: auto;
}
.header {
display: flex;
padding: 0.75rem;
background-color: white;
align-items: center;
font-size: 16px;
.icon {
height: 24px;
width: 24px;
margin-right: 0.75rem;
}
.status {
height: 1rem;
width: 1rem;
border-radius: 50%;
margin-left: auto;
position: relative;
&.ok {
background-color: var(--positive);
}
&.warning {
background-color: var(--warning);
}
&.error {
background-color: var(--negative);
}
}
}
.footer {
padding: 0.5rem;
background-color: white;
}
.resource {
display: grid;
grid-template-columns: auto auto;
padding: 0.5rem;
background-color: var(--bg);
row-gap: 6px;
column-gap: 20px;
> div,
span {
display: flex;
padding: 0 0.5rem;
}
}
:global(.resource .title svg) {
height: 1rem;
width: 1rem;
}
.footer {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: auto;
background: white;
padding: 0.5rem;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
button {
border: none;
position: relative;
background: transparent;
height: unset;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
padding: 0 0.5rem;
flex: 1;
span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
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;
}
&::after {
content: '';
position: absolute;
right: 0;
top: 0;
border-radius: 0.5rem;
width: 100%;
height: 100%;
transition: transform 0.1s ease;
will-change: box-shadow 0.25s;
pointer-events: none;
}
}
}
.positive {
color: #077c35;
}
</style>

View File

@@ -0,0 +1,16 @@
<h1><slot></slot></h1>
<style lang="scss">
@font-face {
font-family: 'Reckless Neue';
font-style: normal;
src: url('/fonts/RecklessNeue-Regular.woff2') format('woff2');
}
h1 {
font-family: 'Reckless Neue';
font-weight: 400;
font-size: 2.3rem;
margin-top: 0;
}
</style>

View File

@@ -0,0 +1,203 @@
<script lang="ts">
import Connection from '$lib/icons/connection.svelte';
import Network from '$lib/icons/network.svelte';
import Layers from '$lib/icons/layers.svelte';
import Clock from '$lib/icons/clock.svelte';
import { formatDuration } from '$lib/utils/conversion';
import { onMount } from 'svelte';
import type { V1DaemonSet, V1Deployment, V1Pod } from '@kubernetes/client-node';
import { writable } from 'svelte/store';
import { goto } from '$app/navigation';
export let pod: V1Pod;
export let parent: V1Deployment | V1DaemonSet;
export let i: number;
let { metadata, spec, status } = pod;
// set name
const name =
metadata?.name || metadata?.labels?.app || metadata?.labels?.['app.kubernetes.io/app'];
// set replicas
let replicas = -1;
if (parent.spec?.['replicas'] !== undefined) {
parent = parent as V1Deployment;
replicas = parent.spec?.replicas || replicas;
} else if (parent.status?.['currentNumberScheduled'] !== undefined) {
parent = parent as V1DaemonSet;
replicas = parent.status?.currentNumberScheduled || replicas;
}
// set uptime
let uptime = writable(new Date().getTime() - new Date(status?.startTime || 0).getTime());
onMount(() => {
setInterval(() => uptime.update((n) => n + 1000), 1000);
});
</script>
<div class="card">
<div class="header">
<div class="icon"><Layers /></div>
<span class="name">{name}</span>
<!--
<span class={`status ${node?.online === 1 ? 'ok' : 'error'}`}></span>
-->
</div>
<div class="resource">
<div class="title">
<Network />
<span>Status</span>
</div>
<span>{status?.phase}</span>
<div class="title">
<Network />
<span>Pod IP address</span>
</div>
<span>{status?.podIP}</span>
<div class="title">
<Layers />
<span>Instances</span>
</div>
<span>{i + 1} of {replicas}</span>
<div class="title">
<Connection />
<span>Running on Node</span>
</div>
<span>{spec?.nodeName}</span>
<div class="title">
<Clock />
<span>Uptime</span>
</div>
<span>{formatDuration($uptime / 1000)}</span>
<!--
<span>{uptime}</span>
<Lifecycle {conditions} />
-->
</div>
<div class="footer">
<button on:click={() => goto(`/cluster/pod/${pod.metadata?.uid}`)}>
<span>Pod details</span>
</button>
</div>
</div>
<style lang="scss">
.card-container {
background-color: #cab2aa40;
border-radius: 0.5rem;
width: 100%;
padding: 0.75rem;
.namespace {
width: 100%;
display: block;
}
.card-wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
}
.card {
flex-grow: 1;
max-width: 550px;
background: #fbf6f4;
box-shadow: var(
--str-shadow-s,
0px 0px 2px #22242714,
0px 1px 4px #2224271f,
0px 4px 8px #22242729
);
pointer-events: all;
cursor: auto;
}
.header {
display: flex;
padding: 0.75rem;
background-color: white;
align-items: center;
font-size: 16px;
.icon {
height: 24px;
width: 24px;
margin-right: 0.75rem;
}
.status {
height: 1rem;
width: 1rem;
border-radius: 50%;
margin-left: auto;
position: relative;
&.ok {
background-color: var(--positive);
}
&.warning {
background-color: var(--warning);
}
&.error {
background-color: var(--negative);
}
}
}
.footer {
padding: 0.5rem;
background-color: white;
}
.resource {
display: grid;
grid-template-columns: auto auto;
padding: 0.5rem;
background-color: var(--bg);
row-gap: 6px;
column-gap: 20px;
> div,
span {
display: flex;
padding: 0 0.5rem;
}
}
:global(.resource .title svg) {
height: 1rem;
width: 1rem;
}
.footer {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: auto;
background: white;
padding: 0.5rem;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
.positive {
color: #077c35;
}
</style>

View File

@@ -0,0 +1,75 @@
<script lang="ts">
export let value: number | string;
export let max: number = 100;
</script>
<label for="file">Print progress</label>
<progress id="file" class="completed" {max} {value}>{value}%</progress>
<style lang="scss">
label {
font-weight: 500;
}
progress {
height: 30px;
width: 100%;
border: 1px solid #ced7e6;
opacity: 0.6;
z-index: 1;
background-color: rgba(0, 0, 0, 0);
margin-top: 0.5rem;
&.completed::-webkit-progress-value {
background: unset !important;
animation: unset !important;
background-size: unset !important;
background-image: linear-gradient(
to right,
rgb(255 80 24 / 48%),
rgb(255 80 24 / 92%)
) !important;
}
&,
&::-webkit-progress-inner-element,
&::-webkit-progress-value,
&::-webkit-progress-bar {
border-radius: 0.5rem;
}
&::-webkit-progress-value {
border: 1px solid #a92200;
margin-left: -1px;
height: calc(100% + 2px);
transform: translateY(-1px);
background-color: rgba(0, 0, 0, 0);
--width: 12px;
background: repeating-linear-gradient(
45deg,
rgb(255, 80, 24),
rgb(255, 80, 24) var(--width),
rgb(255 80 24 / 48%) var(--width),
rgb(255 80 24 / 48%) calc(var(--width) * 2)
);
background-size: 200% 100%;
animation: progress-animation 14s linear infinite;
}
@keyframes progress-animation {
0% {
background-position: -100% 0;
}
100% {
background-position: 100% 0;
}
}
&::-webkit-progress-bar {
background: #f9f5f3;
}
}
</style>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
export let title: string;
export let description: string;
</script>
<article class="main-container">
<div class="header">
<h2>{title}</h2>
<label>{description}</label>
</div>
<slot></slot>
</article>
<style lang="scss">
article {
position: relative;
display: flex;
flex-direction: column;
flex-wrap: unset;
display: flex;
gap: 1rem;
}
.header {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,259 @@
<script lang="ts">
import ServerIcon from '$lib/icons/server.svelte';
import CPU from '$lib/icons/cpu.svelte';
import Shield from '$lib/icons/shield.svelte';
import Floppy from '$lib/icons/floppy-disk.svelte';
import Box from '$lib/icons/box.svelte';
import LXC from '$lib/icons/cube-side.svelte';
import Network from '$lib/icons/network.svelte';
import Clock from '$lib/icons/clock.svelte';
import { formatBytes, formatDuration } from '$lib/utils/conversion';
import type { Node } from '$lib/interfaces/proxmox';
import { onMount } from 'svelte';
export let node: Node;
const buttons = ['View logs', 'Web terminal', 'graphs'];
let { cpuinfo, memory, uptime, loadavg } = node.info;
const vmsRunning = node.vms.filter((v) => v?.template !== 1 && v.status === 'running');
const vmsTotal = node.vms.filter((v) => v?.template !== 1);
const lxcsRunning = node.lxcs.filter((l) => l?.template !== 1 && l.status === 'running');
const lxcsTotal = node.lxcs.filter((l) => l?.template !== 1);
onMount(() => {
setInterval(() => (uptime += 1), 1000);
});
</script>
<div class="card">
<div class="header">
<div class="icon"><ServerIcon /></div>
<span class="name">{node?.name}</span>
<span class={`status ${node?.online === 1 ? 'ok' : 'error'}`}></span>
</div>
<div class="resource">
<div class="title">
<Network />
<span>Load</span>
</div>
<span>{loadavg}</span>
<div class="title">
<CPU />
<span>CPU cores</span>
</div>
<span
>{cpuinfo.cpus} Cores on {cpuinfo.sockets} {cpuinfo.sockets > 1 ? 'Sockets' : 'Socket'}</span
>
<div class="title">
<Shield />
<span>DDoS protection</span>
</div>
<span class="positive">Enabled</span>
<div class="title">
<Network />
<span>IPs</span>
</div>
<span>{node?.ip}</span>
<div class="title">
<Floppy />
<span>Memory</span>
</div>
<span>{formatBytes(memory?.total)}</span>
<div class="title">
<Box />
<span>VMs</span>
</div>
<span>{vmsRunning.length} / {vmsTotal.length}</span>
<div class="title">
<LXC />
<span>LXCs</span>
</div>
<span>{lxcsRunning.length} / {lxcsTotal.length}</span>
<div class="title">
<Clock />
<span>Uptime</span>
</div>
<span>{formatDuration(uptime)}</span>
</div>
<div class="footer">
{#each buttons as btn (btn)}
<button on:click={() => console.log(node)}>
<span>{btn}</span>
</button>
{/each}
</div>
</div>
<style lang="scss">
@keyframes pulse-live {
0% {
box-shadow: 0 0 0 0 rgba(0, 212, 57, 0.7);
box-shadow: 0 0 0 0 rgba(0, 212, 57, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(0, 212, 57, 0);
box-shadow: 0 0 0 10px rgba(0, 212, 57, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(0, 212, 57, 0);
box-shadow: 0 0 0 0 rgba(0, 212, 57, 0);
}
}
@mixin pulse-dot {
&::after {
content: '';
top: 50%;
margin-left: 0.4rem;
position: absolute;
display: block;
border-radius: 50%;
background-color: var(--color);
border-radius: 50%;
transform: translate(-50%, -50%);
animation: pulse-live 2s infinite;
height: 16px;
width: 16px;
}
}
.card {
background: #fbf6f4;
box-shadow: var(
--str-shadow-s,
0px 0px 2px #22242714,
0px 1px 4px #2224271f,
0px 4px 8px #22242729
);
pointer-events: all;
cursor: auto;
}
.header {
display: flex;
padding: 0.75rem;
background-color: white;
align-items: center;
font-size: 16px;
.icon {
height: 24px;
width: 24px;
margin-right: 0.75rem;
}
.status {
height: 1rem;
width: 1rem;
border-radius: 50%;
margin-left: auto;
position: relative;
&.ok {
--color: var(--positive);
@include pulse-dot;
}
&.warning {
background-color: var(--warning);
}
&.error {
background-color: var(--negative);
}
}
}
.footer {
padding: 0.5rem;
background-color: white;
}
.resource {
display: grid;
grid-template-columns: auto auto;
padding: 0.5rem;
background-color: var(--bg);
row-gap: 6px;
column-gap: 20px;
> div,
span {
display: flex;
padding: 0 0.5rem;
}
}
:global(.resource .title svg) {
height: 1rem;
width: 1rem;
}
.footer {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: auto;
background: white;
padding: 0.5rem;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
button {
border: none;
position: relative;
background: transparent;
height: unset;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
padding: 0 0.5rem;
flex: 1;
span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
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;
}
&::after {
content: '';
position: absolute;
right: 0;
top: 0;
border-radius: 0.5rem;
width: 100%;
height: 100%;
transition: transform 0.1s ease;
will-change: box-shadow 0.25s;
pointer-events: none;
}
}
}
.positive {
color: #077c35;
}
</style>

View File

@@ -0,0 +1,112 @@
<script lang="ts">
import { page } from '$app/stores';
import { derived } from 'svelte/store';
const pages = [
{
name: 'Home',
path: '/'
},
{
name: 'Sites',
path: '/sites'
},
{
name: 'Servers',
path: '/servers'
},
{
name: 'Printer',
path: '/printer'
},
{
name: 'Network',
path: '/network'
},
{
name: 'Cluster',
path: '/cluster'
},
{
name: 'Health',
path: '/health'
}
];
const activePage = derived(page, ($page) => $page.url.pathname);
</script>
<div class="nav-wrapper">
<nav>
{#each pages as page, i (page.name)}
{#if i === 0}
<a class={$activePage === page.path ? 'highlight' : ''} href={page.path}>{page.name}</a>
{:else}
<a
class={`${$activePage !== page.path && $activePage.startsWith(page.path) ? 'child' : ''} ${$activePage.startsWith(page.path) ? 'highlight' : ''}`}
href={page.path}>{page.name}</a
>
{/if}
{/each}
</nav>
</div>
<style lang="scss">
.nav-wrapper {
--nav-width: 240px;
top: 72px;
left: 0;
min-width: var(--nav-width);
margin-right: 1rem;
@media screen and (max-width: 700px) {
--nav-width: 100px;
margin-left: 0.5rem;
margin-right: 0;
}
}
nav {
display: flex;
flex-direction: column;
position: fixed;
width: var(--nav-width);
gap: 4px;
margin-top: 1rem;
a {
position: relative;
display: flex;
padding: 0.75rem 1rem;
font-weight: 600;
border: 2px solid transparent;
border-radius: 0.5rem;
transition: 0.3s ease all;
overflow: hidden;
&:hover,
&.highlight {
background-color: var(--highlight);
}
&::after {
position: absolute;
transition: inherit;
left: -2rem;
opacity: 0;
content: '->';
}
&.child {
padding-left: 2.5rem;
&::after {
opacity: 1;
position: absolute;
left: 1rem;
content: '->';
}
}
}
}
</style>

View File

@@ -0,0 +1,136 @@
<script lang="ts">
import { goto } from '$app/navigation';
export let title = '';
export let description = '';
export let columns: Array<string> | object;
export let data: Array<unknown> = [];
export let links: Array<string> = [];
export let footer = '';
const hasLinks = links?.length > 0;
let displayColumns: string[] = [];
if (typeof columns === 'object' && !Array.isArray(columns)) {
displayColumns = Object.values(columns);
columns = Object.keys(columns);
}
</script>
<div class="main-container">
<div class="header">
<h2>{title}</h2>
<div class="description">{description}</div>
</div>
<div class="actions">
<slot></slot>
</div>
<table>
<thead>
<tr>
{#if displayColumns.length > 0}
{#each displayColumns as column (column)}
<th>{column}</th>
{/each}
{:else}
{#each columns as column (column)}
<th>{column}</th>
{/each}
{/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>
</table>
{#if footer?.length}
<footer>{footer}</footer>
{/if}
</div>
<style lang="scss">
.header {
margin-bottom: 12px;
}
.description {
font-size: 0.875rem;
color: #666;
margin-bottom: 12px;
}
.actions {
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;
}
</style>

View File

@@ -0,0 +1,49 @@
<script>
import { getContext } from 'svelte';
import { TABS } from './Tabs.svelte';
const tab = {};
const { registerTab, selectTab, selectedTab } = getContext(TABS);
registerTab(tab);
</script>
<div class="tab">
<button class:selected={$selectedTab === tab} on:click={() => selectTab(tab)}>
<slot></slot>
</button>
</div>
<style lang="scss">
button {
background: none;
border: none;
border-bottom: 2px solid transparent;
border-radius: 0;
margin: 0;
letter-spacing: 0.2px;
&.selected {
opacity: 1;
letter-spacing: unset;
border-bottom-color: var(--color) !important;
font-weight: 600 !important;
}
}
.tab {
&:not(&:first-of-type) {
margin-left: 0.75rem;
}
button {
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
opacity: 0.7;
padding-bottom: 0.3rem;
transition: 0.3s ease-in-out all;
}
}
</style>

View File

@@ -0,0 +1,11 @@
<div class="tab-list">
<slot></slot>
</div>
<style lang="scss">
.tab-list {
display: flex;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
</style>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { getContext } from 'svelte';
import { fly, scale, slide, crossfade } from 'svelte/transition';
import { TABS } from './Tabs.svelte';
const panel = {
id: Math.random() * 100
};
const { registerPanel, selectedPanel } = getContext(TABS);
registerPanel(panel);
const [send, receive] = crossfade({ duration: 800 });
</script>
{#if $selectedPanel === panel}
<div in:fly={{ x: 200, duration: 500 }} out:fly={{ x: -50, duration: 300 }}>
<!-- <div in:send="{{key: panel}}" out:receive="{{key: panel}}"> -->
<slot></slot>
</div>
{/if}
<style lang="scss">
div {
position: absolute;
top: calc(3.25rem);
width: 100%;
}
</style>

View File

@@ -0,0 +1,63 @@
<script context="module">
export const TABS = {};
</script>
<script lang="ts">
import { setContext, onDestroy } from 'svelte';
import { writable } from 'svelte/store';
const tabs = [];
const panels = [];
const selectedTab = writable(null);
const selectedPanel = writable(null);
setContext(TABS, {
registerTab: (tab) => {
tabs.push(tab);
selectedTab.update((current) => current || tab);
onDestroy(() => {
const i = tabs.indexOf(tab);
tabs.splice(i, 1);
selectedTab.update((current) =>
current === tab ? tabs[i] || tabs[tabs.length - 1] : current
);
});
},
registerPanel: (panel) => {
panels.push(panel);
selectedPanel.update((current) => current || panel);
onDestroy(() => {
const i = panels.indexOf(panel);
panels.splice(i, 1);
selectedPanel.update((current) =>
current === panel ? panels[i] || panels[panels.length - 1] : current
);
});
},
selectTab: (tab) => {
const i = tabs.indexOf(tab);
selectedTab.set(tab);
selectedPanel.set(panels[i]);
},
selectedTab,
selectedPanel
});
</script>
<div class="tabs">
<slot></slot>
</div>
<style lang="scss">
.tabs {
position: relative;
display: flex;
flex-direction: column;
margin-bottom: 1.5rem;
}
</style>

98
src/lib/icons/Logo.svelte Normal file
View File

@@ -0,0 +1,98 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 677.000000 429.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,429.000000) scale(0.100000,-0.100000)"
fill="white"
stroke="none"
>
<path
d="M2980 4190 c-455 -39 -874 -260 -1157 -610 -110 -136 -225 -332 -285
-487 -15 -40 -19 -43 -55 -43 -21 0 -53 -4 -71 -9 -18 -6 -59 -13 -90 -16
-160 -16 -265 -46 -417 -120 -251 -121 -432 -297 -617 -597 -56 -91 -127 -301
-153 -453 -23 -136 -30 -335 -15 -466 11 -102 19 -146 51 -271 28 -113 119
-298 209 -426 70 -100 227 -256 326 -325 160 -111 388 -209 550 -236 38 -6 85
-15 104 -18 58 -12 3985 -15 4185 -4 218 12 308 32 449 101 234 114 400 282
511 515 70 146 96 259 103 441 7 212 -22 352 -113 534 -83 166 -195 293 -360
406 -71 49 -81 61 -131 148 -147 261 -387 498 -629 621 -176 90 -328 132 -505
142 l-106 6 -18 46 c-94 234 -234 449 -405 621 -232 234 -461 368 -771 449
-146 38 -207 47 -385 55 -60 2 -153 1 -205 -4z m389 -245 c353 -53 712 -265
927 -547 26 -35 51 -65 54 -68 3 -3 14 -21 23 -40 10 -19 28 -53 40 -75 31
-55 127 -269 127 -282 0 -6 4 -13 8 -15 4 -1 14 -20 22 -41 8 -21 26 -44 40
-52 15 -8 91 -18 185 -24 195 -13 293 -38 449 -114 159 -78 310 -200 430 -347
43 -53 129 -194 167 -274 25 -53 46 -86 56 -86 20 0 88 -37 138 -75 21 -16 42
-30 46 -30 24 0 192 -206 216 -265 23 -57 47 -105 56 -108 4 -2 5 -7 3 -11 -4
-7 -1 -24 21 -121 4 -19 8 -100 8 -180 -1 -165 -16 -231 -87 -377 -64 -132
-249 -328 -363 -385 -56 -28 -189 -74 -250 -85 -22 -5 -996 -8 -2165 -8 -1711
0 -2138 3 -2190 13 -385 78 -704 312 -867 637 -147 292 -169 686 -57 1021 111
333 353 601 654 727 133 56 195 68 365 73 242 7 238 6 268 62 14 26 33 70 42
97 27 78 111 240 179 345 220 336 552 555 956 630 122 23 364 25 499 5z"
/>
<path
d="M2324 3061 c-95 -23 -177 -91 -218 -180 -19 -42 -21 -66 -24 -273 -3
-173 0 -233 10 -252 17 -32 207 -226 222 -226 33 0 36 23 36 305 1 205 4 284
13 295 35 44 41 45 293 50 l244 5 2 -150 3 -150 80 -5 80 -5 3 -237 2 -238 26
0 c30 0 110 39 154 76 51 44 60 77 60 226 0 184 -3 178 80 178 67 0 68 0 79
31 6 18 11 85 11 150 l0 119 231 0 c132 0 238 -4 249 -10 10 -5 28 -25 39 -44
21 -33 21 -40 21 -770 0 -782 0 -782 -46 -800 -9 -3 -325 -6 -703 -6 -377 0
-692 0 -698 0 -7 0 -13 -6 -13 -14 0 -8 -33 -91 -72 -185 -49 -113 -79 -171
-90 -175 -10 -2 -18 -11 -18 -20 0 -14 66 -16 703 -16 l703 0 29 -62 30 -63
189 -3 c183 -2 190 -2 213 20 l23 21 0 1088 0 1088 -24 55 c-29 66 -85 124
-151 156 l-50 25 -845 2 c-465 1 -859 -2 -876 -6z"
/>
<path
d="M1335 2303 c-218 -20 -408 -131 -589 -348 -125 -148 -189 -343 -187
-565 1 -176 55 -356 142 -473 73 -98 135 -121 189 -67 32 32 26 81 -16 136
-58 75 -101 171 -122 272 l-20 92 54 0 54 0 0 60 0 60 -32 2 c-18 1 -43 1 -56
0 -21 -1 -22 2 -16 36 24 144 108 315 192 396 l31 28 31 -35 c31 -36 51 -41
73 -17 9 10 9 11 0 6 -7 -4 -13 -3 -13 3 0 5 13 15 29 22 l29 13 -30 39 c-29
36 -30 39 -12 52 47 36 160 87 230 105 104 27 114 26 115 -7 0 -16 6 -44 12
-63 10 -29 17 -35 42 -36 42 0 52 10 66 72 12 53 13 54 48 54 100 0 270 -72
388 -164 124 -97 226 -262 257 -418 22 -107 25 -101 -44 -93 l-60 7 0 -61 0
-60 59 -3 58 -3 -13 -55 c-24 -92 -54 -160 -121 -267 -35 -55 -63 -109 -63
-120 0 -40 14 -62 47 -78 30 -15 42 -15 75 -6 48 15 113 79 129 129 6 20 25
63 41 95 73 147 102 347 73 507 -9 52 -17 102 -17 110 0 8 -24 65 -53 125 -55
113 -149 240 -236 320 -63 58 -187 133 -266 161 -101 36 -324 53 -498 37z"
/>
<path
d="M5148 2168 c-83 -10 -139 -45 -159 -96 -5 -13 -27 -31 -51 -41 -82
-36 -84 -36 -102 9 -9 22 -19 40 -22 40 -4 0 -32 -21 -63 -46 -64 -52 -89 -99
-90 -166 -1 -34 -8 -54 -26 -76 -14 -17 -25 -35 -25 -41 0 -17 -17 -13 -61 15
-23 14 -42 24 -43 22 -8 -10 -36 -129 -36 -152 0 -32 20 -74 48 -99 17 -15 19
-24 11 -50 -5 -18 -9 -47 -9 -66 0 -34 0 -34 -40 -28 -22 3 -42 2 -44 -1 -6
-11 40 -160 58 -187 9 -14 31 -33 49 -41 23 -11 38 -30 52 -62 11 -25 21 -49
23 -52 1 -4 -13 -15 -33 -26 -19 -10 -35 -24 -35 -30 0 -15 63 -72 112 -102
23 -14 61 -26 95 -29 71 -7 93 -24 93 -72 0 -61 10 -79 47 -92 51 -17 141 -3
177 28 23 19 37 23 59 19 16 -3 40 -6 53 -6 22 0 24 -4 24 -45 0 -51 -2 -50
87 -31 75 16 108 36 134 81 25 42 128 91 122 58 -2 -7 1 -13 7 -13 5 0 12 -16
16 -35 3 -19 9 -35 12 -35 11 0 92 72 113 102 33 45 49 98 38 126 -9 24 5 69
34 107 14 18 17 18 34 4 10 -8 31 -23 46 -33 l28 -17 15 37 c9 21 20 64 25 96
10 64 -2 104 -41 131 -18 12 -19 21 -14 74 l7 60 47 -5 48 -4 -5 43 c-13 116
-58 200 -115 214 -13 3 -34 24 -47 47 l-24 42 38 34 37 34 -34 35 c-49 51
-120 93 -157 93 -45 0 -81 26 -81 60 0 70 -10 88 -58 104 -63 22 -117 20 -167
-5 -34 -17 -53 -20 -89 -15 -44 7 -46 9 -46 39 0 49 -8 54 -72 45z m168 -560
c64 -44 104 -111 111 -188 5 -51 2 -65 -20 -103 -29 -49 -96 -113 -133 -127
-53 -21 -133 -12 -194 21 -52 27 -62 38 -90 96 -29 58 -32 71 -26 121 7 60 28
102 73 143 56 53 94 69 164 69 59 0 73 -4 115 -32z"
/>
<path
d="M1777 1831 c-53 -6 -61 -10 -103 -56 -25 -28 -64 -75 -86 -105 -53
-72 -60 -77 -121 -85 -68 -9 -97 -25 -141 -76 -71 -83 -70 -191 2 -271 87 -96
204 -103 298 -16 55 50 68 91 60 180 -6 67 -5 72 34 145 22 42 40 81 40 88 0
6 9 21 20 33 11 12 20 28 20 37 0 10 5 13 12 9 7 -4 8 -3 4 5 -9 15 20 62 36
58 17 -3 22 31 8 48 -7 8 -15 14 -19 14 -3 -1 -32 -5 -64 -8z"
/>
<path
d="M2943 1818 c-42 -14 -68 -63 -59 -110 4 -24 18 -45 46 -67 22 -18 40
-38 40 -46 0 -13 -24 -69 -98 -225 -22 -47 -43 -95 -46 -107 l-7 -23 375 0
c353 0 375 1 369 18 -4 9 -22 49 -41 87 -67 137 -122 267 -116 276 3 5 14 9
24 9 10 0 32 12 47 26 15 14 22 22 14 18 -11 -6 -12 -3 -6 12 19 41 10 86 -23
116 l-32 28 -228 -1 c-125 -1 -241 -6 -259 -11z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,18 @@
<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="M352 480v-384c0-52.9 43.1-96 96-96s96 43.1 96 96v384c16.9 12.7 31.2 28.6 41.9 46.9 14.5 24.4 22.1 52.5 22.1 81.1 0 88.2-71.8 160-160 160s-160-71.8-160-160c0-50.6 24.1-98.1 64-128zM448 704c52.9 0 96-43.1 96-96 0-34.2-18.4-66-48-83.2-9.9-5.7-16-16.3-16-27.7v-401.1c0-17.6-14.4-32-32-32s-32 14.4-32 32v401.1c0 11.4-6.1 22-16 27.7-29.6 17.1-48 49-48 83.2 0 52.9 43.1 96 96 96z"
></path>
<path
d="M447.998 544v32c17.6 0 32 14.4 32 32s-14.4 32-32 32v32c35.3 0 64-28.7 64-64s-28.7-64-64-64z"
></path>
<path
d="M288 416h-64v32h64v-32zM288 352h-64v32h64v-32zM288 288h-128v32h128v-32zM288 224h-64v32h64v-32zM288 160h-64v32h64v-32zM288 96h-128v32h128v-32z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 823 B

11
src/lib/icons/box.svelte Normal file
View File

@@ -0,0 +1,11 @@
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 768 768"
>
<path
d="M701.2 186.6c0 0 0 0 0 0l-288-147.6c-18.3-9.3-40.1-9.3-58.4 0l-288 147.6c-21.4 11-34.8 32.9-34.8 57v280.9c0 24.1 13.3 45.9 34.8 57l288 147.6c9.1 4.7 19.1 7 29.1 7s20.1-2.3 29.2-7l288-147.6c21.4-11 34.8-32.9 34.8-57v-280.9c0.1-24.1-13.2-45.9-34.7-57zM384 352l-107.3-55 249.8-128 107.3 55-249.8 128zM384 96l107.4 55-249.8 128-107.4-55 249.8-128zM96 276.4l256 131.2v248.2l-256-131.3v-248.1zM416 655.7v-248.1l256-131.2v248.2l-256 131.1z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 573 B

View File

@@ -0,0 +1,13 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M704 517.5v-101.5c0-35.3-28.7-64-64-64h-224v-101.5c37.2-13.2 64-48.8 64-90.5 0-52.9-43.1-96-96-96s-96 43.1-96 96c0 41.7 26.8 77.3 64 90.5v101.5h-224c-35.3 0-64 28.7-64 64v101.5c-37.2 13.2-64 48.8-64 90.5 0 52.9 43.1 96 96 96s96-43.1 96-96c0-41.7-26.8-77.3-64-90.5v-101.5h224v101.5c-37.2 13.2-64 48.8-64 90.5 0 52.9 43.1 96 96 96s96-43.1 96-96c0-41.7-26.8-77.3-64-90.5v-101.5h224v101.5c-37.2 13.2-64 48.8-64 90.5 0 52.9 43.1 96 96 96s96-43.1 96-96c0-41.7-26.8-77.3-64-90.5zM320 160c0-35.3 28.7-64 64-64s64 28.7 64 64-28.7 64-64 64c-35.3 0-64-28.7-64-64zM160 608c0 35.3-28.7 64-64 64s-64-28.7-64-64 28.7-64 64-64 64 28.7 64 64zM448 608c0 35.3-28.7 64-64 64s-64-28.7-64-64 28.7-64 64-64c35.3 0 64 28.7 64 64zM672 672c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 980 B

View File

@@ -0,0 +1,18 @@
<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="M737.8 234.5c-19.3-45.7-47-86.8-82.3-122-35.3-35.3-76.3-62.9-122-82.3-47.4-20-97.7-30.2-149.5-30.2s-102.1 10.2-149.5 30.2c-45.7 19.3-86.8 47-122 82.3-35.3 35.3-62.9 76.3-82.3 122-20 47.4-30.2 97.7-30.2 149.5s10.2 102.1 30.2 149.5c19.3 45.7 47 86.8 82.3 122 35.3 35.3 76.3 62.9 122 82.3 47.4 20 97.7 30.2 149.5 30.2s102.1-10.2 149.5-30.2c45.7-19.3 86.8-47 122-82.3 35.3-35.3 62.9-76.3 82.3-122 20-47.4 30.2-97.7 30.2-149.5s-10.2-102.1-30.2-149.5zM384 704c-176.4 0-320-143.6-320-320s143.6-320 320-320c176.4 0 320 143.6 320 320s-143.6 320-320 320z"
></path>
<path
d="M528 249.4l-135.7 135.7c-2.6-0.7-5.4-1.1-8.3-1.1-4.7 0-9.2 1-13.2 2.9l-102.5-73.2-18.6 26 102.5 73.2c-0.1 1-0.1 2-0.1 3.1 0 17.6 14.4 32 32 32s32-14.4 32-32c0-2.9-0.4-5.6-1.1-8.3l135.6-135.7-22.6-22.6z"
></path>
<path
d="M384 96c-76.9 0-149.3 30-203.6 84.4s-84.4 126.7-84.4 203.6 30 149.3 84.4 203.6c54.3 54.4 126.7 84.4 203.6 84.4s149.3-30 203.6-84.4c54.4-54.3 84.4-126.7 84.4-203.6s-30-149.3-84.4-203.6c-54.3-54.4-126.7-84.4-203.6-84.4zM384 640c-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>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,12 @@
<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="M704 384h-128v-240c0-8.8-7.2-16-16-16h-144v-64c0-35.3-28.7-64-64-64h-288c-35.3 0-64 28.7-64 64v160c0 35.3 28.7 64 64 64h42.2l-9.8 58.7c-1.5 9.3 1.1 18.8 7.1 25.9s15 11.3 24.4 11.3h160c9.4 0 18.3-4.1 24.4-11.3s8.7-16.7 7.1-25.9l-9.8-58.7h42.4c35.3 0 64-28.7 64-64v-64h128v224h-128c-35.3 0-64 28.7-64 64v160c0 35.3 28.7 64 64 64h42.2l-9.8 58.7c-1.5 9.3 1.1 18.8 7.1 25.9 6.1 7.2 15 11.3 24.4 11.3h160c9.4 0 18.3-4.1 24.4-11.3s8.7-16.7 7.1-25.9l-9.8-58.7h42.4c35.3 0 64-28.7 64-64v-160c0-35.3-28.7-64-64-64zM255.6 352h-95.1l10.7-64h73.8l10.6 64zM352 224h-288v-160h288v160c0 0 0 0 0 0zM607.6 736h-95.1l10.7-64h73.8l10.6 64zM704 608h-288v-160h288v160c0 0 0 0 0 0z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 829 B

View File

@@ -0,0 +1,25 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M768 256v-32h-96v-32c0-52.9-43.1-96-96-96h-32v-96h-32v96h-64v-96h-32v96h-64v-96h-32v96h-64v-96h-32v96h-32c-52.9 0-96 43.1-96 96v32h-96v32h96v64h-96v32h96v64h-96v32h96v64h-96v32h96v32c0 52.9 43.1 96 96 96h32v96h32v-96h64v96h32v-96h64v96h32v-96h64v96h32v-96h32c52.9 0 96-43.1 96-96v-32h96v-32h-96v-64h96v-32h-96v-64h96v-32h-96v-64h96zM608 576c0 17.6-14.4 32-32 32h-384c-17.6 0-32-14.4-32-32v-384c0-17.6 14.4-32 32-32h384c17.6 0 32 14.4 32 32v384z"
></path>
<path
d="M512 192c-35.3 0-64 28.7-64 64s28.7 64 64 64 64-28.7 64-64-28.7-64-64-64zM512 288c-17.6 0-32-14.4-32-32s14.4-32 32-32 32 14.4 32 32-14.4 32-32 32z"
></path>
<path
d="M272 441.4l-32 32-32-32-22.6 22.6 32 32-32 32 22.6 22.6 32-32 32 32 22.6-22.6-32-32 32-32z"
></path>
<path
d="M475.1 449.9c-17.5 21-27.1 48.7-27.1 78.1 0 26.5 21.5 48 48 48s48-21.5 48-48-21.5-48-48-48c-1.1 0-2.2 0-3.2 0.1 11.7-19.5 30.3-32.1 51.2-32.1v-32c-26.2 0-50.7 12-68.9 33.9zM496 512c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"
></path>
<path
d="M368 416c-26.5 0-48 21.5-48 48 0 12.3 4.6 23.5 12.3 32-7.6 8.5-12.3 19.7-12.3 32 0 26.5 21.5 48 48 48s48-21.5 48-48c0-12.3-4.6-23.5-12.3-32 7.6-8.5 12.3-19.7 12.3-32 0-26.5-21.5-48-48-48zM368 448c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16zM368 544c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

16
src/lib/icons/cpu.svelte Normal file
View File

@@ -0,0 +1,16 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M768 256v-32h-96v-32c0-52.9-43.1-96-96-96h-32v-96h-32v96h-64v-96h-32v96h-64v-96h-32v96h-64v-96h-32v96h-32c-52.9 0-96 43.1-96 96v32h-96v32h96v64h-96v32h96v64h-96v32h96v64h-96v32h96v32c0 52.9 43.1 96 96 96h32v96h32v-96h64v96h32v-96h64v96h32v-96h64v96h32v-96h32c52.9 0 96-43.1 96-96v-32h96v-32h-96v-64h96v-32h-96v-64h96v-32h-96v-64h96zM608 576c0 17.6-14.4 32-32 32h-384c-17.6 0-32-14.4-32-32v-384c0-17.6 14.4-32 32-32h384c17.6 0 32 14.4 32 32v384z"
></path>
<path
d="M512 192c-35.3 0-64 28.7-64 64s28.7 64 64 64 64-28.7 64-64-28.7-64-64-64zM512 288c-17.6 0-32-14.4-32-32s14.4-32 32-32 32 14.4 32 32-14.4 32-32 32z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 816 B

View File

@@ -0,0 +1,16 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M701.2 186.6c0 0 0 0 0 0l-288-147.6c-18.3-9.3-40.1-9.3-58.4 0l-288 147.6c-21.4 11-34.8 32.9-34.8 57v280.9c0 24.1 13.3 45.9 34.8 57l288 147.6c9.1 4.7 19.1 7 29.1 7s20.1-2.3 29.2-7l288-147.6c21.4-11 34.8-32.9 34.8-57v-280.9c0.1-24.1-13.2-45.9-34.7-57zM384 96l249.8 128-249.8 128-249.8-128 249.8-128zM96 276.4l256 131.2v248.2l-256-131.3v-248.1zM416 655.7v-248.1l256-131.2v248.2l-256 131.1z"
></path>
<path
d="M632.3 341.2c-4.8-2.9-10.7-3.1-15.6-0.6l-160 82c-5.3 2.7-8.7 8.2-8.7 14.2v140.2c0 5.6 2.9 10.7 7.7 13.7 2.6 1.6 5.4 2.3 8.3 2.3 2.5 0 5-0.6 7.3-1.8l160-82c5.3-2.7 8.7-8.2 8.7-14.2v-140.1c0-5.6-2.9-10.8-7.7-13.7zM608 485.3l-128 65.6v-104.2l128-65.6v104.2z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 866 B

View File

@@ -0,0 +1,13 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M666.9 54.1c-17.3-11.3-41.1-21.3-70.7-29.5-57.1-15.9-132.5-24.6-212.2-24.6s-155.1 8.7-212.2 24.6c-29.6 8.2-53.4 18.2-70.7 29.5-30.7 20.1-37.1 42.6-37.1 57.9v544c0 15.3 6.4 37.8 37.1 57.9 17.3 11.3 41.1 21.3 70.7 29.5 57.1 15.9 132.5 24.6 212.2 24.6s155.1-8.7 212.2-24.6c29.7-8.2 53.4-18.2 70.7-29.5 30.6-20.1 37.1-42.6 37.1-57.9v-544c0-15.3-6.4-37.8-37.1-57.9zM640 402.1c-11.1 7.2-29 15.3-56.6 23-53.1 14.8-123.9 22.9-199.4 22.9s-146.3-8.1-199.4-22.8c-27.7-7.7-45.6-15.8-56.6-23v-91.5c13.4 6.4 29.4 12.2 48.1 17.4 55.7 15.5 129.6 24 207.9 24s152.2-8.5 207.9-24c18.6-5.2 34.7-11 48.1-17.4v91.4zM640 274.1c-11.1 7.2-29 15.3-56.6 23-53.1 14.8-123.9 22.9-199.4 22.9s-146.3-8.1-199.4-22.8c-27.7-7.7-45.6-15.8-56.6-23v-89.9c12.7 5.6 27.4 10.6 43.8 15.2 57.1 15.8 132.5 24.5 212.2 24.5s155.1-8.7 212.2-24.6c16.4-4.6 31.1-9.7 43.8-15.2v89.9zM128 438.6c13.4 6.4 29.4 12.2 48.1 17.4 55.7 15.5 129.6 24 207.9 24s152.2-8.5 207.9-24c18.6-5.2 34.7-11 48.1-17.4v91.5c-11.1 7.2-29 15.3-56.6 23-53.1 14.8-123.9 22.9-199.4 22.9s-146.3-8.1-199.4-22.8c-27.7-7.7-45.6-15.8-56.6-23v-91.6zM203.1 82.6c49.9-12 114.1-18.6 180.9-18.6s131 6.6 180.9 18.6c45.2 10.9 65.7 22.9 72.9 29.4-7.2 6.5-27.7 18.5-72.9 29.4-49.9 12-114.1 18.6-180.9 18.6s-131-6.6-180.9-18.6c-45.2-10.9-65.7-22.9-72.9-29.4 7.2-6.5 27.7-18.5 72.9-29.4zM564.9 685.4c-49.9 12-114.1 18.6-180.9 18.6s-131-6.6-180.9-18.6c-51.3-12.3-70.8-26.2-75.1-31.7v-87.1c13.4 6.4 29.4 12.2 48.1 17.4 55.7 15.5 129.6 24 207.9 24s152.2-8.5 207.9-24c18.6-5.2 34.7-11 48.1-17.4v87.1c-4.3 5.5-23.8 19.4-75.1 31.7z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,13 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M737.8 234.5c-19.3-45.7-47-86.8-82.3-122-35.3-35.3-76.3-62.9-122-82.3-47.4-20-97.7-30.2-149.5-30.2s-102.1 10.2-149.5 30.2c-45.7 19.3-86.8 47-122 82.3-35.3 35.3-62.9 76.3-82.3 122-20 47.4-30.2 97.7-30.2 149.5s10.2 102.1 30.2 149.5c19.3 45.7 47 86.8 82.3 122 35.3 35.3 76.3 62.9 122 82.3 47.4 20 97.7 30.2 149.5 30.2s102.1-10.2 149.5-30.2c45.7-19.3 86.8-47 122-82.3 35.3-35.3 62.9-76.3 82.3-122 20-47.4 30.2-97.7 30.2-149.5s-10.2-102.1-30.2-149.5zM658.5 219.6c-4.1 2-8.2 4.6-12.2 7.7l-2.1 1.6-1.5 2.2c-15 22.2-38.5 29-54.1 24.2-8.4-2.6-12.7-7.7-12.7-15.3 0-21.6-11.5-38.4-20.7-52-8-11.8-12.8-19.3-11.2-24.8 1.4-4.8 8.6-14.7 39.1-29.6 30 23.9 55.6 53.1 75.4 86zM384 64c6.5 0 12.9 0.2 19.2 0.6-3.2 2.1-7 3.8-11.3 5.8-12.5 5.7-29.7 13.6-38.7 35.6l-0.3 0.6-0.2 0.6c-20.6 65.2-48.2 90.8-66.5 107.7-13.1 12.1-24.4 22.6-24.6 40-0.2 15.1 7.9 31.1 29.8 59 17.5 22.3 34.3 33.6 51.4 34.8 21.6 1.4 36.8-13.4 49.1-25.3 6.2-6 12.6-12.3 17.4-13.9 1.3-0.4 4.2-1.4 11.5 5.8 26.6 26.6 48.3 33.6 64.2 38.8 14.7 4.8 21.4 7 28.9 21.3 12.5 23.7 31.4 32.9 45.2 39.6 15.1 7.3 17 9.2 17 17 0 5.1 0.2 10.6 0.3 16.3 0.6 20.4 1.4 51.3-7.8 60.8-1.3 1.4-3.5 2.9-8.5 2.9-31.9 0-44.7 28.4-53.2 47.2-2.4 5.4-6.4 14.1-9 16.9-20.9-2.9-46.5 21-86.2 59.8-8.8 8.6-20.4 20-29.6 28 1-10.6 3.4-26.5 8.6-49.1 7-30.7 17-63.8 24.2-80.5 4.7-10.9 6-27.9-14.6-46.6-11.1-10-26.4-19-41.3-27.7-10.7-6.2-20.7-12.1-28.4-17.9-8.6-6.5-10.2-9.8-10.4-10.4 0-5.9 1.1-12.6 2.1-19.7 4.1-26.7 10.2-67.1-43.7-90.7-5.6-2.5-11.5-4.8-17.1-7-45.1-17.8-91.6-36.2-99.9-160 57.4-55.8 135.9-90.3 222.4-90.3zM91.6 513.9c19.1 3.3 33-10.7 43.3-21 3.4-3.4 10.5-10.5 13.4-11.2 1.4 0.6 11.5 6 28.9 52 10.2 27 10.7 54.8 11.2 86.9 0.1 5.4 0.2 10.9 0.3 16.7-41.8-32.3-75.4-74.7-97.1-123.4zM221.5 659.6c-0.8-13.7-1-26.8-1.3-39.6-0.6-33.7-1.1-65.4-13.3-97.7-17.8-47-32.9-67.8-52.2-72-18.7-4-32.5 9.9-42.6 20-4.1 4.1-12.5 12.5-15.2 12-1.1-0.2-10.7-3.3-27.9-41.8-3.3-18.4-5-37.2-5-56.5 0-74.5 25.6-143.2 68.5-197.6 5.8 43 17.3 76.2 34.7 100.7 24.1 34 55.1 46.2 82.4 57 5.6 2.2 10.9 4.3 16.1 6.6 31.4 13.7 29 29.8 24.9 56.5-1.2 8.1-2.5 16.5-2.5 24.8 0 23.7 26.6 39.2 54.7 55.7 13.4 7.9 27.3 16 35.9 23.8 7.4 6.7 7 9.5 6.7 10.2-8.4 19.5-19.6 56.8-27.1 90.8-4.1 18.4-6.8 34.8-8.1 47.5-1.8 18.7-0.4 29.8 4.6 37.1 1.8 2.6 4.2 4.7 6.9 6.2-50.9-3.6-98.6-19.1-140.2-43.7zM384 704c-1.6 0-3.1 0-4.7 0 12.6-4.5 28-19.2 54.6-45.2 12.8-12.5 26-25.4 37.7-35.4 14.7-12.5 20.9-15 22.6-15.4 6.9 1 18.9 0.3 29.1-12.6 5.2-6.5 8.8-14.5 12.6-23 9-20 14.4-28.3 24.1-28.3 12.5 0 23.4-4.4 31.4-12.6 18.7-19.2 17.7-53.6 16.9-84-0.2-5.5-0.3-10.7-0.3-15.4 0-28.9-20.3-38.7-35.1-45.8-11.9-5.8-23.2-11.2-30.8-25.6-13.6-25.9-30.8-31.5-47.4-36.9-14.2-4.6-30.3-9.9-51.5-31-14.1-14.1-29-18.7-44.3-13.5-11.7 3.9-20.7 12.7-29.5 21.3-8.9 8.6-17.2 16.8-24.6 16.3-4.2-0.3-13.4-3.6-28.4-22.6-15.3-19.5-23.1-32.5-23-38.7 0.1-3.6 5.5-8.8 14.4-17 19.4-18 51.9-48.1 75.1-120.8 4-9.4 10.5-12.7 22.2-18.1 12-5.5 27-12.4 35.6-30.4 40.9 7.4 79.1 22.5 113 43.8-23.7 13.5-36.2 26.6-40.5 41.5-5.9 20.3 5.5 37 15.5 51.7 7.8 11.4 15.2 22.2 15.2 33.9 0 21.6 13.5 39.2 35.3 45.9 5.9 1.8 12.5 2.8 19.4 2.8 23 0 49.9-10.7 69-37.3 2.1-1.5 4.1-2.6 5.9-3.5 19.6 41.1 30.5 87.2 30.5 135.9 0 176.4-143.6 320-320 320z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,28 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
height="100%"
width="100%"
viewBox="0 0 115.000000 108.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,108.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M504 1040 c-117 -17 -195 -56 -279 -139 -152 -151 -188 -386 -87
-575 36 -68 135 -167 200 -200 142 -72 306 -74 448 -6 59 29 153 116 194 180
97 151 97 368 1 519 -101 157 -297 247 -477 221z m249 -113 c88 -43 152 -108
193 -195 37 -75 45 -212 19 -293 -37 -116 -120 -209 -233 -262 -61 -29 -75
-32 -162 -31 -74 0 -107 5 -150 22 -276 111 -356 452 -157 669 89 96 184 135
323 130 83 -3 99 -7 167 -40z"
/>
<path
d="M629 604 l-126 -126 -49 51 c-27 28 -56 51 -64 51 -19 0 -40 -19 -40
-37 0 -15 140 -153 155 -153 6 0 77 67 159 149 135 135 147 150 137 170 -7 11
-19 21 -29 21 -9 0 -74 -57 -143 -126z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 896 B

View File

@@ -0,0 +1,16 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M672 32h-576c-35.3 0-64 28.7-64 64v498.7c0 17.1 6.7 33.1 18.8 45.2l77.3 77.3c12.1 12.1 28.2 18.8 45.2 18.8h498.7c35.3 0 64-28.7 64-64v-576c0-35.3-28.7-64-64-64zM192 64h384v256c0 17.6-14.4 32-32 32h-320c-17.6 0-32-14.4-32-32v-256zM544 704h-288v-192h288v192zM672 672h-96v-176c0-8.8-7.2-16-16-16h-320c-8.8 0-16 7.2-16 16v176h-50.7l-77.3-77.3v-498.7h64v224c0 35.3 28.7 64 64 64h320c35.3 0 64-28.7 64-64v-224h64v576z"
></path>
<path
d="M304 672h64c8.8 0 16-7.2 16-16v-96c0-8.8-7.2-16-16-16h-64c-8.8 0-16 7.2-16 16v96c0 8.8 7.2 16 16 16zM320 576h32v64h-32v-64z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 760 B

View File

@@ -0,0 +1,23 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M640 0h-512c-35.3 0-64 28.7-64 64v640c0 35.3 28.7 64 64 64h512c35.3 0 64-28.7 64-64v-640c0-35.3-28.7-64-64-64zM640 704h-512v-640h512v640c0 0 0 0 0 0z"
></path>
<path
d="M224 544c-35.3 0-64 28.7-64 64s28.7 64 64 64 64-28.7 64-64-28.7-64-64-64zM224 640c-17.6 0-32-14.4-32-32s14.4-32 32-32 32 14.4 32 32-14.4 32-32 32z"
></path>
<path d="M576 640h-192v32h208c8.8 0 16-7.2 16-16v-48h-32v32z"></path>
<path
d="M368 352c26.5 0 48-21.5 48-48s-21.5-48-48-48c-26.5 0-48 21.5-48 48s21.5 48 48 48zM368 288c8.8 0 16 7.2 16 16s-7.2 16-16 16-16-7.2-16-16 7.2-16 16-16z"
></path>
<path
d="M563.8 451.1l-37.5-12.2c31.8-37.4 49.7-85.3 49.7-134.9 0-114.7-93.3-208-208-208s-208 93.3-208 208 93.3 208 208 208c18.5 0 36.8-2.4 54.5-7.2l84.7 59.5c5.2 3.6 10.9 6.5 17 8.5 6.6 2.1 13.2 3.2 19.8 3.2 27 0 52.1-17.2 60.9-44.2 5.3-16.3 3.9-33.6-3.8-48.8-7.8-15.3-21-26.6-37.3-31.9zM368 480c-97 0-176-79-176-176s79-176 176-176 176 79 176 176c0 46.5-18.6 91.2-51.2 124.1l-133.3-43.3h-0.5c-14.1-3.2-29.1 3.7-35.7 16.9-7 14.1-2.6 31.5 10.2 40.5l52.4 36.9c-5.8 0.6-11.9 0.9-17.9 0.9zM574.4 521.9c-5.5 16.8-23.6 26-40.3 20.5-2.8-0.9-5.4-2.2-7.8-3.7l-174.3-122.7 2.4 0.8 199.5 64.8c8.1 2.6 14.7 8.3 18.6 15.9s4.6 16.3 1.9 24.4z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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 400c0-6.3-3.8-12.1-9.6-14.7l-116.8-51.3 107.6-48.9c11.4-5.2 18.8-16.6 18.8-29.1s-7.3-23.9-18.8-29.1l-352-160c-8.4-3.8-18.1-3.8-26.5 0l-352 160c-11.4 5.2-18.7 16.6-18.7 29.1s7.3 23.9 18.8 29.1l107.6 48.9-116.8 51.3c-5.8 2.6-9.6 8.4-9.6 14.7s3.8 12.1 9.6 14.7l112.4 49.3-112.4 49.3c-5.8 2.6-9.6 8.4-9.6 14.7s3.8 12.1 9.6 14.7l368 161.6c2 0.9 4.2 1.3 6.4 1.3s4.4-0.5 6.4-1.3l368-161.6c5.8-2.6 9.6-8.3 9.6-14.7 0-6.3-3.8-12.1-9.6-14.7l-112.4-49.3 112.4-49.3c5.8-2.6 9.6-8.4 9.6-14.7zM384 131.2l274.7 124.8-274.7 124.8-274.7-124.8 274.7-124.8zM712.2 528l-328.2 144.1-328.2-144.1 106-46.5 215.8 94.8c2 0.9 4.2 1.3 6.4 1.3s4.4-0.5 6.4-1.3l215.8-94.8 106 46.5zM384 544.1l-328.2-144.1 109.7-48.2 205.3 93.3c4.2 1.9 8.7 2.9 13.2 2.9s9-1 13.2-2.9l205.3-93.3 109.7 48.2-328.2 144.1z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 979 B

16
src/lib/icons/link.svelte Normal file
View File

@@ -0,0 +1,16 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M433 281.4l-34 54.3c6.7 4.2 12.9 9.1 18.5 14.7 40.5 40.5 40.5 106.5 0 147l-112.1 112.2c-20.3 20.3-46.9 30.4-73.5 30.4s-53.2-10.1-73.5-30.4c-40.5-40.5-40.5-106.5 0-147l64.2-63.9-45.2-45.3-64.2 64c-65.5 65.5-65.5 172 0 237.5 32.7 32.7 75.7 49.1 118.8 49.1 43 0 86-16.4 118.8-49.1l112.1-112.1c65.5-65.5 65.5-172 0-237.5-9.1-9.2-19.2-17.1-29.9-23.9z"
></path>
<path
d="M654.8 113.2c-65.5-65.5-172-65.5-237.5 0l-112.1 112.1c-65.5 65.5-65.5 172 0 237.5 9 9 19.1 17 29.9 23.8l33.9-54.3c-6.7-4.2-12.9-9.1-18.5-14.7-40.5-40.5-40.5-106.5 0-147l112.1-112.1c40.5-40.5 106.5-40.5 147 0s40.5 106.5 0 147l-64.2 63.9 45.2 45.3 64.2-64c65.5-65.5 65.5-172 0-237.5z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@@ -0,0 +1,16 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M565 75c-48.3-48.4-112.6-75-181-75s-132.7 26.6-181 75c-48.4 48.4-75 112.6-75 181 0 85.4 39.1 195.8 116.4 328.1 56.4 96.7 112 168.5 114.4 171.5 6.1 7.8 15.4 12.4 25.3 12.4s19.2-4.6 25.3-12.4c2.3-3 58-74.8 114.4-171.5 77.1-132.3 116.2-242.7 116.2-328.1 0-68.4-26.6-132.7-75-181zM384 682c-57.3-80.4-192-284.5-192-426 0-105.9 86.1-192 192-192s192 86.1 192 192c0 141.5-134.7 345.6-192 426z"
></path>
<path
d="M384 128c-70.6 0-128 57.4-128 128s57.4 128 128 128c70.6 0 128-57.4 128-128s-57.4-128-128-128zM384 352c-52.9 0-96-43.1-96-96s43.1-96 96-96 96 43.1 96 96-43.1 96-96 96z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 776 B

View File

@@ -0,0 +1,18 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M607.9 0h-351.9c-35.3 0-64 28.7-64 64v146.8l-77.3 77.3c-12.1 12.1-18.8 28.2-18.8 45.2v50.7c0 8.5 3.4 16.6 9.4 22.6s14.1 9.4 22.6 9.4h32.1v18.7l-54.7 54.6c-6 6-9.4 14.1-9.4 22.6v192c0 35.3 28.7 64 64 64h448c35.3 0 64-28.7 64-64v-639.9c0-35.3-28.7-64-64-64zM607.9 704h-448v-178.7l54.7-54.6c6-6 9.4-14.1 9.4-22.6v-64c0-8.5-3.4-16.6-9.4-22.6s-14.1-9.4-22.6-9.4h-32.1v-18.7l77.3-77.3c11.9-11.9 18.8-28.5 18.8-45.3v-146.8h351.9v640z"
></path>
<path d="M288 128h32v96h-32v-96z"></path>
<path d="M352 96h32v128h-32v-128z"></path>
<path d="M416 128h32v96h-32v-96z"></path>
<path d="M480 96h32v128h-32v-128z"></path>
<path d="M544 128h32v96h-32v-96z"></path>
</svg>

After

Width:  |  Height:  |  Size: 845 B

View File

@@ -0,0 +1,16 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M190.6 430.6l-45.3-45.3-136 136c-12.5 12.5-12.5 32.8 0 45.3l136 136 45.3-45.3-81.3-81.3h466.7v-64h-466.7l81.3-81.4z"
></path>
<path
d="M758.6 201.4l-136-136-45.3 45.3 81.4 81.4h-466.7v64h466.7l-81.4 81.4 45.3 45.3 136-136c12.5-12.7 12.5-32.9 0-45.4z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 455 B

View File

@@ -0,0 +1,30 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 128.000000 137.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,137.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M483 1145 c-323 -87 -481 -440 -333 -742 61 -123 180 -224 313 -265
57 -17 86 -20 175 -16 122 6 164 19 264 85 214 140 289 422 175 663 -38 82
-149 195 -227 233 -116 56 -256 72 -367 42z m219 -66 c245 -51 407 -309 343
-548 -35 -134 -128 -245 -251 -303 -92 -43 -219 -50 -314 -18 -196 66 -311
224 -312 427 -1 289 254 501 534 442z"
/>
<path
d="M472 838 c-17 -17 -17 -369 0 -386 15 -15 61 -15 76 0 17 17 17 369
0 386 -15 15 -61 15 -76 0z"
/>
<path
d="M672 838 c-17 -17 -17 -369 0 -386 15 -15 61 -15 76 0 17 17 17 369
0 386 -15 15 -61 15 -76 0z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 854 B

View File

@@ -0,0 +1,13 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M669.3 307.2c-5.1-11.6-16.6-19.2-29.3-19.2h-179.6l82-245.9c4.8-14.3-1.1-30-14.1-37.7s-29.6-5.2-39.8 5.8l-384 416c-8.6 9.3-10.9 22.9-5.8 34.5s16.6 19.2 29.3 19.2h179.6l-82 245.9c-4.8 14.3 1.1 30 14.1 37.7 5.1 3 10.7 4.5 16.3 4.4 8.7 0 17.3-3.5 23.5-10.3l384-416c8.6-9.2 10.9-22.8 5.8-34.4zM333.4 605l48.9-146.8c3.3-9.8 1.6-20.5-4.4-28.8s-15.7-13.3-26-13.3h-150.8l233.5-253-48.9 146.8c-3.3 9.8-1.6 20.5 4.4 28.8s15.7 13.3 26 13.3h150.9l-233.6 253z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 647 B

View File

@@ -0,0 +1,31 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 170.000000 151.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,151.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M149 1451 l-29 -29 0 -521 c0 -500 -1 -521 -18 -521 -43 0 -52 -29
-52 -163 0 -101 3 -129 16 -141 14 -14 94 -16 733 -16 544 0 720 3 729 12 8 8
12 56 12 150 0 127 -1 138 -20 148 -11 6 -28 10 -38 8 -16 -3 -17 31 -22 525
l-5 529 -28 24 -28 24 -610 0 -611 0 -29 -29z m1249 -143 l3 -98 -206 0 -205
0 0 30 0 30 -200 0 -200 0 0 -30 0 -30 -200 0 -200 0 0 100 0 100 603 -2 602
-3 3 -97z m-478 -173 l0 -55 -130 0 -130 0 0 55 0 55 130 0 130 0 0 -55z
m-330 -65 l0 -70 85 0 85 0 0 -53 c0 -48 12 -77 31 -77 22 0 39 35 39 81 l0
49 80 0 80 0 0 70 0 70 205 0 205 0 0 -380 0 -380 -605 0 -605 0 0 380 0 380
200 0 200 0 0 -70z m890 -850 l0 -90 -680 0 -680 0 0 90 0 90 680 0 680 0 0
-90z"
/>
<path
d="M367 543 c-13 -13 -7 -51 10 -57 9 -4 197 -6 419 -6 324 1 406 4 421
15 14 11 16 18 7 32 -10 17 -38 18 -431 21 -231 1 -423 -1 -426 -5z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,36 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="139.000000pt"
height="157.000000pt"
viewBox="0 0 139.000000 157.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,157.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M173 1506 l-28 -24 -5 -529 -5 -528 -22 3 c-41 6 -53 -30 -53 -165 0
-97 3 -125 16 -137 14 -14 82 -16 602 -16 352 0 593 4 602 10 12 7 16 37 18
137 4 141 -4 166 -52 168 l-27 2 1 501 c0 540 -1 550 -51 586 -21 14 -75 16
-496 16 l-472 0 -28 -24z m975 -148 l3 -98 -156 0 -155 0 0 30 0 30 -160 0
-160 0 0 -30 0 -30 -160 0 -160 0 0 93 c0 52 3 97 7 100 3 4 216 6 472 5 l466
-3 3 -97z m-378 -173 l0 -55 -90 0 -90 0 0 55 0 55 90 0 90 0 0 -55z m-250
-65 l0 -70 160 0 160 0 0 70 0 70 155 0 155 0 -2 -380 -3 -380 -470 0 -470 0
-3 380 -2 380 160 0 160 0 0 -70z m710 -850 l0 -90 -550 0 -550 0 0 90 0 90
550 0 550 0 0 -90z"
/>
<path
d="M452 884 c-21 -15 -22 -20 -22 -199 0 -179 1 -184 22 -199 27 -20
158 -22 176 -4 9 9 12 68 12 205 0 180 -1 193 -19 203 -31 16 -143 12 -169 -6z
m118 -199 l0 -145 -35 0 -35 0 0 145 0 145 35 0 35 0 0 -145z"
/>
<path
d="M732 884 c-21 -15 -22 -20 -22 -199 0 -216 0 -215 105 -215 106 0
105 -2 105 221 l0 189 -26 10 c-40 16 -136 12 -162 -6z m118 -199 l0 -145 -35
0 -35 0 0 145 0 145 35 0 35 0 0 -145z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,30 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 163.000000 151.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,151.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M149 1461 l-29 -29 0 -519 0 -519 -32 -9 -33 -10 -3 -139 c-2 -93 1
-143 9 -152 17 -21 1441 -21 1458 0 8 9 11 59 9 152 l-3 139 -32 10 -33 9 0
519 0 519 -29 29 -29 29 -612 0 -612 0 -29 -29z m1241 -141 l0 -100 -205 0
-205 0 0 30 0 30 -195 0 -195 0 0 -30 0 -30 -205 0 -205 0 0 84 c0 46 3 91 6
100 6 14 68 16 605 16 l599 0 0 -100z m-480 -175 l0 -55 -125 0 -125 0 0 55 0
55 125 0 125 0 0 -55z m-320 -65 l0 -70 80 0 80 0 0 -49 c0 -47 -1 -49 -50
-79 -81 -50 -99 -113 -50 -175 20 -25 53 -38 173 -68 26 -6 47 -14 47 -18 0
-3 -23 -19 -51 -35 -28 -16 -59 -41 -68 -57 l-16 -29 -121 0 c-128 0 -159 -9
-152 -46 3 -18 15 -19 328 -19 l325 0 3 27 c4 36 -13 40 -171 36 -70 -2 -127
-1 -127 2 1 3 24 19 53 35 111 64 81 162 -57 180 -58 8 -110 27 -121 45 -9 15
30 57 76 80 39 21 49 45 49 126 l0 44 80 0 80 0 0 70 0 70 208 0 207 0 0 -380
0 -380 -607 0 -608 0 0 380 0 380 205 0 205 0 0 -70z m873 -852 l3 -88 -676 0
-675 0 2 87 c1 49 2 89 3 91 0 1 302 1 670 0 l671 -3 2 -87z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,28 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="176.000000pt"
height="156.000000pt"
viewBox="0 0 176.000000 156.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,156.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M223 1486 l-28 -24 -5 -528 c-5 -501 -6 -527 -23 -530 -47 -7 -47 -7
-47 -152 0 -94 4 -142 12 -150 17 -17 1439 -17 1456 0 8 8 12 56 12 150 l0
138 -26 10 c-14 6 -30 8 -35 4 -5 -3 -9 201 -9 511 0 361 -3 523 -11 538 -6
12 -21 30 -33 39 -19 16 -68 18 -628 18 l-607 0 -28 -24z m1235 -148 l3 -98
-206 0 -205 0 0 30 0 30 -195 0 -195 0 0 -30 0 -30 -205 0 -205 0 0 84 c0 46
3 92 6 100 6 15 65 16 603 14 l596 -3 3 -97z m-483 -173 l0 -55 -122 0 -123 0
0 55 0 55 123 0 122 0 0 -55z m-315 -65 l0 -70 195 0 195 0 0 70 0 70 205 0
205 0 0 -382 0 -383 -172 2 -173 2 -5 213 -5 213 -250 0 -250 0 -5 -213 -5
-213 -170 -1 -170 0 -3 381 -2 381 205 0 205 0 0 -70z m383 -511 l-2 -181
-188 -1 -188 -2 -3 183 -2 182 192 0 193 0 -2 -181z m487 -339 l0 -90 -675 0
-675 0 0 90 0 90 675 0 675 0 0 -90z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,27 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 132.000000 120.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,120.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M510 1100 c-301 -46 -495 -338 -425 -639 42 -175 184 -322 364 -375
32 -9 91 -16 141 -16 149 0 257 45 360 149 135 136 187 311 145 489 -61 259
-320 432 -585 392z m275 -114 c225 -111 313 -364 206 -591 -112 -238 -405
-326 -634 -192 -25 15 -67 51 -95 82 -220 241 -125 615 185 725 48 17 76 20
160 17 94 -3 108 -7 178 -41z"
/>
<path
d="M686 675 c-46 -35 -68 -45 -97 -45 -28 0 -42 -6 -53 -22 -34 -48 -7
-108 48 -108 32 0 66 24 66 47 0 6 32 35 71 63 59 45 70 57 67 79 -5 41 -36
36 -102 -14z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 793 B

View 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"
>
<g id="icomoon-ignore"> </g>
<path
d="M736 320v-128c0-15.1-5.3-29.1-14.1-40.1v0l-117.4-145.9c-3-3.8-7.6-6-12.5-6h-416c-4.8 0-9.4 2.2-12.5 6l-117.4 146c-8.8 10.9-14.1 24.9-14.1 40v128c0 19.1 8.4 36.3 21.7 48-13.3 11.7-21.7 28.9-21.7 48v64c0 19.1 8.4 36.3 21.7 48-13.3 11.7-21.7 28.9-21.7 48v128c0 35.3 28.7 64 64 64h576c35.3 0 64-28.7 64-64v-128c0-19.1-8.4-36.3-21.7-48 13.3-11.7 21.7-28.9 21.7-48v-64c0-19.1-8.4-36.3-21.7-48 13.3-11.7 21.7-28.9 21.7-48zM183.7 32h400.7l77.2 96h-555.2l77.3-96zM672 704h-576l-0.1-128c0 0 0 0 0.1 0v-32h576v160zM672.1 480c0 0-0.1 0 0 0l-0.1 32h-576v-32l-0.1-64c0 0 0 0 0.1 0v-32h576v32l0.1 64zM96 352v-160h576v160h-576z"
></path>
<path d="M544 256h96v32h-96v-32z"></path>
<path d="M480 256h32v32h-32v-32z"></path>
<path d="M544 416h96v32h-96v-32z"></path>
<path d="M480 416h32v32h-32v-32z"></path>
<path d="M128 576h32v32h-32v-32z"></path>
<path d="M192 576h32v32h-32v-32z"></path>
<path d="M256 576h32v32h-32v-32z"></path>
<path d="M320 576h32v32h-32v-32z"></path>
<path d="M544 640h96v32h-96v-32z"></path>
<path d="M480 640h32v32h-32v-32z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,16 @@
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="768"
height="768"
viewBox="0 0 768 768"
>
<g id="icomoon-ignore"> </g>
<path
d="M726.8 137.6c-6-6.1-14.2-9.6-22.8-9.6-42.5 0-101.4-15.4-165.8-43.4-54-23.4-106.3-53.6-133.3-76.8-12-10.3-29.8-10.3-41.8 0-27 23.3-79.3 53.5-133.3 76.9-64.4 27.9-123.3 43.3-165.8 43.3-8.6 0-16.8 3.5-22.8 9.6s-9.3 14.4-9.2 23c0.1 3.8 2.2 94.5 44.2 214 24.6 70 58 135.7 99.2 195.1 51.5 74.4 115.4 139.1 190 192.3 5.6 4 12.1 6 18.6 6s13-2 18.6-6c74.5-53.2 138.4-117.9 190-192.3 41.2-59.4 74.5-125.1 99.2-195.1 42-119.5 44.2-210.2 44.2-214 0.2-8.6-3.1-16.9-9.2-23zM630.6 355.6c-49.5 139.2-132.4 253.7-246.6 340.6-114.2-87-197.1-201.5-246.6-340.6-25.2-70.9-35.1-131.4-39-165.8 56-6.8 114.5-28.1 156.8-46.5 48.9-21.1 95.8-47 128.8-70.8 33 23.7 79.9 49.6 128.7 70.8 42.3 18.4 100.8 39.7 156.8 46.5-3.8 34.4-13.7 95-38.9 165.8z"
></path>
<path
d="M374.7 131c-54.6 39-115.2 78.4-201.4 93.3-8 1.4-13.7 8.6-13.2 16.7 3.8 63.5 28.3 139.6 69.1 214.1 40 73.1 91.4 137.5 144.8 181.3 3 2.4 6.6 3.6 10.2 3.6s7.2-1.2 10.2-3.6c53.4-43.8 104.8-108.2 144.8-181.3 40.8-74.5 65.3-150.6 69.1-214.1 0.5-8.1-5.2-15.3-13.2-16.7-86.6-15-147.2-54.3-201.8-93.3-5.6-4-13-4-18.6 0zM384 603c-96.1-84.4-178.4-235.5-190.8-350 80.4-16.7 138.7-52.6 190.8-89.4 52.2 36.8 110.5 72.7 190.8 89.4-12.4 114.5-94.7 265.6-190.8 350z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,26 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 141.000000 122.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,122.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M581 1094 c-132 -35 -266 -141 -329 -260 -135 -258 -33 -583 223
-712 112 -56 268 -69 387 -32 258 80 419 366 353 625 -26 100 -63 165 -139
241 -109 109 -222 155 -375 153 -36 0 -90 -7 -120 -15z m221 -65 c129 -27 255
-125 311 -241 142 -296 -75 -648 -402 -648 -174 0 -329 98 -402 254 -157 336
134 710 493 635z"
/>
<path
d="M532 768 c-17 -17 -17 -339 0 -356 17 -17 339 -17 356 0 8 8 12 63
12 180 0 155 -1 168 -19 178 -29 15 -334 13 -349 -2z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 746 B

View File

@@ -0,0 +1,38 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 152.000000 182.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,182.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M650 1704 c-45 -20 -77 -50 -101 -96 -17 -34 -19 -61 -19 -299 l0
-262 -41 -44 c-60 -66 -83 -123 -83 -213 -1 -91 30 -167 94 -228 69 -68 120
-87 230 -87 87 0 100 3 149 30 184 102 228 342 91 498 -33 37 -40 53 -40 86
l0 40 53 3 c48 3 52 5 52 28 0 23 -4 25 -52 28 l-53 3 0 65 0 64 59 0 c44 0
60 4 65 16 8 21 8 20 -10 38 -10 10 -33 16 -64 16 l-48 0 -4 104 c-3 99 -5
106 -34 143 -59 74 -162 103 -244 67z m140 -69 c15 -8 38 -27 49 -41 20 -26
21 -38 21 -303 l0 -277 36 -35 c171 -162 63 -439 -172 -439 -180 0 -308 198
-228 353 10 17 37 56 61 85 l43 54 0 261 c0 287 2 296 59 334 39 26 89 30 131
8z"
/>
<path
d="M702 1458 c-9 -9 -12 -82 -12 -288 l0 -277 -26 -13 c-72 -39 -69
-156 6 -195 49 -26 95 -19 137 18 32 30 36 40 36 81 0 37 -5 52 -28 76 -16 17
-35 30 -42 30 -10 0 -13 60 -15 288 -3 279 -4 287 -23 290 -11 1 -26 -3 -33
-10z"
/>
<path
d="M204 397 c-2 -7 -3 -56 -2 -108 l3 -94 197 -3 c195 -2 197 -3 200
-25 3 -21 7 -22 123 -22 117 0 120 1 123 22 3 22 5 23 210 25 l207 3 0 105 0
105 -528 3 c-432 2 -528 0 -533 -11z m996 -92 l0 -45 -470 0 -470 0 0 38 c0
21 3 42 7 45 3 4 215 7 470 7 l463 0 0 -45z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,31 @@
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 118.000000 181.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,181.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M475 1675 c-5 -2 -22 -6 -38 -9 -37 -9 -105 -71 -123 -114 -11 -28
-14 -99 -14 -360 l0 -325 -41 -44 c-62 -68 -84 -123 -84 -218 0 -67 4 -89 28
-136 31 -65 96 -129 161 -161 46 -22 55 -35 91 -133 20 -54 26 -60 51 -60 24
0 30 5 40 42 22 74 64 139 100 152 54 20 121 87 156 158 58 118 39 255 -48
352 l-44 49 0 105 0 106 46 3 c87 6 84 68 -2 68 l-44 0 0 60 0 59 56 3 c40 2
60 8 67 20 19 29 -9 48 -69 48 l-54 0 0 78 c0 107 -14 149 -65 198 -48 46
-126 74 -170 59z m107 -96 c56 -40 58 -57 58 -417 l0 -328 40 -39 c87 -85 108
-205 54 -306 -88 -163 -308 -188 -427 -48 -37 43 -67 118 -67 169 0 51 32 121
81 174 l48 53 3 347 3 348 37 34 c33 29 45 34 89 34 36 0 60 -6 81 -21z"
/>
<path
d="M482 1408 c-9 -9 -12 -101 -12 -352 l-1 -341 -32 -20 c-94 -59 -47
-215 64 -215 104 0 161 93 109 178 -11 18 -31 38 -45 44 l-25 11 0 342 c0 342
-2 365 -35 365 -6 0 -16 -5 -23 -12z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
src/lib/index.ts Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -0,0 +1,235 @@
export interface Entity {
entity_id: string;
state: string;
attributes: Attributes;
last_changed: Date;
last_reported: Date;
last_updated: Date;
context: Context;
}
export interface Attributes {
editable?: boolean;
id?: string;
device_trackers?: string[];
latitude?: number;
longitude?: number;
gps_accuracy?: number;
source?: string;
user_id?: string;
friendly_name?: string;
entity_id?: string[];
icon?: string;
min_color_temp_kelvin?: number;
max_color_temp_kelvin?: number;
min_mireds?: number;
max_mireds?: number;
supported_color_modes?: ColorMode[];
color_mode?: ColorMode | null;
brightness?: number | null;
color_temp_kelvin?: number | null;
color_temp?: number | null;
hs_color?: number[] | null;
rgb_color?: number[] | null;
xy_color?: number[] | null;
supported_features?: number;
effect_list?: string[];
effect?: null;
initial?: null;
min?: number;
max?: number;
step?: number;
mode?: Mode;
unit_of_measurement?: string;
radius?: number;
passive?: boolean;
persons?: string[];
next_dawn?: Date;
next_dusk?: Date;
next_midnight?: Date;
next_noon?: Date;
next_rising?: Date;
next_setting?: Date;
elevation?: number;
azimuth?: number;
rising?: boolean;
device_class?: string;
has_date?: boolean;
has_time?: boolean;
hour?: number;
minute?: number;
second?: number;
timestamp?: number;
source_type?: string;
'Fast User Switched'?: boolean;
Idle?: boolean;
Locked?: boolean;
'Screen Off'?: boolean;
Screensaver?: boolean;
Sleeping?: boolean;
Terminating?: boolean;
'Battery Provides Time Remaining'?: boolean;
BatteryHealth?: string;
BatteryHealthCondition?: string;
Current?: number;
'Current Capacity'?: number;
DesignCycleCount?: number;
'Hardware Serial Number'?: string;
'Is Charging'?: boolean;
'Is Present'?: boolean;
'LPM Active'?: boolean;
'Max Capacity'?: number;
Name?: string;
'Optimized Battery Charging Engaged'?: boolean;
'Power Source ID'?: number;
'Power Source State'?: string;
'Time to Empty'?: number;
'Time to Full Charge'?: number;
'Transport Type'?: string;
Type?: string;
'Low Power Mode'?: boolean;
Available?: string;
'Available (Important)'?: string;
'Available (Opportunistic)'?: string;
Total?: string;
'Hardware Address'?: string;
'Active Camera'?: any[];
'All Camera'?: string[];
'Active Audio Input'?: any[];
'All Audio Input'?: string[];
'Active Audio Output'?: any[];
'All Audio Output'?: string[];
'Display IDs'?: string[];
'Display Names'?: string[];
'Bundle Identifier'?: string;
'Is Hidden'?: boolean;
'Launch Date'?: Date;
'Owns Menu Bar'?: boolean;
'Allows VoIP'?: boolean;
'Carrier ID'?: string;
'Carrier Name'?: string;
'ISO Country Code'?: string;
'Mobile Country Code'?: string;
'Mobile Network Code'?: string;
battery_level?: number;
altitude?: number;
vertical_accuracy?: number;
'Current Radio Technology'?: string;
'Administrative Area'?: string;
'Areas Of Interest'?: string;
Country?: string;
'Inland Water'?: string;
Locality?: string;
Location?: number[];
Ocean?: string;
'Postal Code'?: string;
'Sub Administrative Area'?: string;
'Sub Locality'?: string;
'Sub Thoroughfare'?: string;
Thoroughfare?: string;
'Time Zone'?: string;
Zones?: string[];
temperature?: number | null;
dew_point?: number;
temperature_unit?: string;
humidity?: number;
cloud_coverage?: number;
uv_index?: number;
pressure?: number;
pressure_unit?: string;
wind_bearing?: number;
wind_gust_speed?: number;
wind_speed?: number;
wind_speed_unit?: string;
visibility_unit?: string;
precipitation_unit?: string;
attribution?: string;
access_token?: string;
width?: number;
height?: number;
fps?: number;
bitrate?: number;
channel_id?: number;
entity_picture?: string;
motion_detection?: boolean;
frontend_stream_type?: string;
options?: string[];
state_class?: StateClass;
auto_update?: boolean;
installed_version?: string;
in_progress?: boolean;
latest_version?: null | string;
release_summary?: null | string;
release_url?: null | string;
skipped_version?: null;
title?: null;
Count?: number;
preset_modes?: null;
percentage?: number;
percentage_step?: number;
preset_mode?: null;
active?: boolean;
color?: string;
k_value?: number;
name?: string;
nozzle_temp_min?: string;
nozzle_temp_max?: string;
type?: string;
modifier?: number;
'AMS Slot 1'?: number;
remain?: number;
tag_uid?: string;
tray_uuid?: string;
hvac_modes?: string[];
min_temp?: number;
max_temp?: number;
current_temperature?: number;
hvac_action?: string;
occupied_cooling_setpoint?: number;
occupied_heating_setpoint?: number;
system_mode?: string;
unoccupied_heating_setpoint?: number;
off_with_transition?: boolean;
off_brightness?: number | null;
battery_size?: string;
battery_quantity?: number;
battery_voltage?: number;
measurement_type?: MeasurementType;
device_type?: string;
status?: string;
zcl_unit_of_measurement?: number;
last_triggered?: Date;
current?: number;
restored?: boolean;
source_list?: string[];
}
export enum ColorMode {
Brightness = 'brightness',
ColorTemp = 'color_temp',
Onoff = 'onoff',
Xy = 'xy'
}
export enum MeasurementType {
ActiveMeasurement = 'ACTIVE_MEASUREMENT',
ActiveMeasurementPhaseAMeasurement = 'ACTIVE_MEASUREMENT, PHASE_A_MEASUREMENT'
}
export enum Mode {
Auto = 'auto',
Box = 'box',
Single = 'single',
Slider = 'slider'
}
export enum StateClass {
Measurement = 'measurement',
TotalIncreasing = 'total_increasing'
}
export interface Context {
id: string;
parent_id: null | string;
user_id: null | string;
}

View File

@@ -0,0 +1,8 @@
export interface Filament {
Hex: string;
Color: string;
Material: string;
Weight: string;
Count: number;
Link: string;
}

View File

@@ -0,0 +1,79 @@
export interface Cluster {
id: string;
quorate: number;
version: number;
type: string;
nodes: number;
name: string;
}
interface Memory {
used: number;
free: number;
total: number;
}
interface RootFs {
used: number;
free: number;
avail: number;
total: number;
}
interface Swap {
total: number;
free: number;
used: number;
}
interface CpuInfo {
cores: number;
mhz: string;
cpus: number;
model: string;
sockets: number;
user_hz: number;
flags: string;
hvm: string;
}
interface KernelInfo {
release: string;
sysname: string;
version: string;
machine: string;
}
interface BootInfo {
secureboot?: number;
mode: string;
}
interface NodeStatus {
memory: Memory;
kversion: string;
cpu: number;
ksm: { shared: number };
uptime: number;
currentKernel: KernelInfo;
rootfs: RootFs;
swap: Swap;
idle: number;
cpuinfo: CpuInfo;
pveversion: string;
loadavg: [string, string, string];
wait: number;
bootInfo: BootInfo;
}
export interface Node {
info: NodeStatus;
online: number;
nodeid: number;
local: number;
name: string;
id: string;
type: string;
ip: string;
level: string;
}

View File

@@ -0,0 +1,92 @@
import type { Filament } from '$lib/interfaces/printer';
const filament: Filament[] = [
{
Hex: '#DD4344',
Color: 'Scarlet Red',
Material: 'PLA Matte',
Weight: '1kg',
Count: 2,
Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742848731'
},
{
Hex: '#61C57F',
Color: 'Grass Green',
Material: 'PLA Matte',
Weight: '1kg',
Count: 2,
Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742783195'
},
{
Hex: '#F7DA5A',
Color: 'Lemon Yellow',
Material: 'PLA Matte',
Weight: '1kg',
Count: 2,
Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742717659'
},
{
Hex: '#E8DBB7',
Color: 'Desert Tan',
Material: 'PLA Matte',
Weight: '1kg',
Count: 1,
Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=48612736401756'
},
{
Hex: "url('https://www.transparenttextures.com/patterns/asfalt-dark.png'",
Color: 'White Marble',
Material: 'PLA Marble',
Weight: '1kg',
Count: 1,
Link: 'https://eu.store.bambulab.com/en-no/products/pla-marble?variant=43964050964699'
},
{
Hex: '#0078C0',
Color: 'Marine Blue',
Material: 'PLA Matte',
Weight: '1kg',
Count: 1,
Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996751073499'
},
{
Hex: '#000000',
Color: 'Charcoal',
Material: 'PLA Matte',
Weight: '1kg',
Count: 2,
Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742750427'
},
{
Hex: '#ffffff',
Color: 'Ivory White',
Material: 'PLA Matte',
Weight: '1kg',
Count: 2,
Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742586587'
},
{
Hex: '#E8AFCE',
Color: 'Sakura Pink',
Material: 'PLA Matte',
Weight: '1kg',
Count: 1,
Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742684891'
},
{
Hex: '#AE96D5',
Color: 'Lilac Purple',
Material: 'PLA Matte',
Weight: '1kg',
Count: 1,
Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742914267'
}
];
export function filamentByColor(name: string) {
return filament.find((f) => f.Color?.toLowerCase() === name?.toLowerCase());
}
export function currentFilament(): Filament[] {
return filament;
}

View File

@@ -0,0 +1,36 @@
import { env } from '$env/dynamic/private';
import type { Entity } from '$lib/interfaces/homeassistant';
function buildHomeassistantRequest() {
const url = env.HOMEASSISTANT_URL || '';
const token = env.HOMEASSISTANT_TOKEN || '';
const options = {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
return { url, options };
}
async function fetchHassStates() {
const { url, options } = buildHomeassistantRequest();
return fetch(url, options).then((resp) => resp.json());
}
export async function fetchP1P(): Promise<Entity[]> {
try {
let hassStates = await fetchHassStates();
hassStates = hassStates.filter(
(el: Entity) => el.attributes.friendly_name?.includes('P1P') === true
);
return hassStates;
} catch (error) {
console.log('ERROR! from fetchP1P:', error);
return Promise.reject(null);
}
}

View File

@@ -0,0 +1,116 @@
import * as k8s from '@kubernetes/client-node';
import stream from 'stream';
import { writable } from 'svelte/store';
const context = {
name: 'kazan-insecure',
user: 'admin',
cluster: 'kazan-insecure'
};
const kc = new k8s.KubeConfig();
kc.loadFromDefault({ contexts: [context] });
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
const appsV1Api = kc.makeApiClient(k8s.AppsV1Api);
const k8sLog = new k8s.Log(kc);
export async function getReplicas() {
try {
const allReplicas = await appsV1Api.listReplicaSetForAllNamespaces();
return allReplicas.items;
} catch (error) {
console.log('error when getting replicas:', error);
return [];
}
}
export async function getPods() {
try {
const allPods = await k8sApi.listPodForAllNamespaces();
return allPods.items;
} catch (error) {
console.log('error when getting k8s resources:', error);
return [];
}
}
export async function getPod(name: string, namespace: string) {
try {
return await k8sApi.readNamespacedPodTemplate({ name, namespace });
} catch (error) {
console.log(`error when getting pod:`, error);
return undefined;
}
}
export async function getDeployments() {
try {
const allDeploys = await appsV1Api.listDeploymentForAllNamespaces();
return allDeploys.items;
} catch (error) {
console.log('error when getting deployments:', error);
return [];
}
}
export async function getDaemons() {
try {
const allDaemons = await appsV1Api.listDaemonSetForAllNamespaces();
return allDaemons.items;
} catch (error) {
console.log('error when getting daemons:', error);
return [];
}
}
export async function getNodes() {
try {
const nodes = await k8sApi.listNode();
return nodes.items;
} catch (error) {
console.log('error when getting k8s nodes:', error);
return [];
}
}
export function createLogStream(podName: string, namespace: string, containerName: string) {
// const logEmitter = new EventTarget(); // use EventTarget or EventEmitter
const logEmitter = writable();
const maxLines = 400;
let liveStream: stream.PassThrough | null = null;
let logAbortController;
async function start() {
// Live logs
liveStream = new stream.PassThrough();
liveStream.on('data', (chunk) => {
let chunks = chunk.toString().split('\n').filter(Boolean);
console.log('chynjks length:', chunks?.length);
if (chunks?.length > maxLines) {
chunks = chunks.slice(maxLines);
}
chunks.forEach((line) => logEmitter.set(line));
});
console.log('setting logAbortController, prev:', logAbortController);
logAbortController = await k8sLog.log(namespace, podName, containerName, liveStream, {
follow: true,
timestamps: false,
pretty: false,
tailLines: maxLines
});
}
function stop() {
console.log('ending livestream!!');
logAbortController?.abort();
liveStream?.end();
}
return { start, stop, logEmitter };
}

86
src/lib/server/proxmox.ts Normal file
View File

@@ -0,0 +1,86 @@
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 options = {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': 'application/json'
}
};
return { url, options };
}
async function fetchNodeVMs(node: Node) {
const r = buildProxmoxRequest();
r.url += 'nodes/' + node?.name + '/qemu';
return fetch(r.url, r?.options)
.then((resp) => resp.json())
.then((response) => response.data);
}
async function fetchNodeLXCs(node: Node) {
const r = buildProxmoxRequest();
r.url += 'nodes/' + node?.name + '/lxc';
return fetch(r.url, r?.options)
.then((resp) => resp.json())
.then((response) => response.data);
}
async function fetchNodeInfo(node: Node) {
const r = buildProxmoxRequest();
r.url += 'nodes/' + node?.name + '/status';
return fetch(r.url, r?.options)
.then((resp) => resp.json())
.then((response) => response.data);
}
async function getClusterInfo() {
const r = buildProxmoxRequest();
r.url += 'cluster/status';
return fetch(r.url, r?.options)
.then((resp) => resp.json())
.then((response) => {
const { data } = response;
const cluster = data.filter((d: Node | Cluster) => d?.type === 'cluster')[0];
const nodes = data.filter((d: Node | Cluster) => d?.type === 'node');
return { cluster, nodes };
});
}
export async function fetchNodes(): Promise<{ nodes: Node[]; cluster: Cluster | null }> {
try {
const { nodes, cluster } = await getClusterInfo();
const infoP = Promise.all(nodes.map((node: Node) => fetchNodeInfo(node)));
const vmsP = Promise.all(nodes.map((node: Node) => fetchNodeVMs(node)));
const lxcsP = Promise.all(nodes.map((node: Node) => fetchNodeLXCs(node)));
const [info, vms, lxcs] = await Promise.all([infoP, vmsP, lxcsP]);
return {
cluster,
nodes: nodes.map((node: Node, i: number) => {
return {
...node,
info: info[i],
vms: vms[i],
lxcs: lxcs[i]
};
})
};
} catch (error) {
console.log('ERROR from fetchnodes:', error);
return { nodes: [], cluster: null };
}
}

22
src/lib/server/traefik.ts Normal file
View File

@@ -0,0 +1,22 @@
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 url = `${baseURL}${TRAEFIK_HTTP_URL}/${path}`;
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
};
return { url, options };
}
export async function getRouters() {
const { url, options } = buildTraefikRequest('routers');
return fetch(url, options).then((resp) => resp.json());
}

View File

@@ -0,0 +1,46 @@
export function formatBytes(bytes: number) {
if (bytes < 1024) return '0 KB'; // Ensure we don't show bytes, only KB and above
const units = ['KB', 'MB', 'GB', 'TB'];
let unitIndex = -1;
let formattedSize = bytes;
do {
formattedSize /= 1024;
unitIndex++;
} while (formattedSize >= 1024 && unitIndex < units.length - 1);
return `${formattedSize.toFixed(2)} ${units[unitIndex]}`;
}
export function formatDuration(seconds: number) {
if (seconds === 0) return 'Uptime: 0 days 00:00:00';
const days = Math.floor(seconds / 86400);
seconds %= 86400;
const hours = Math.floor(seconds / 3600);
seconds %= 3600;
const minutes = Math.floor(seconds / 60);
seconds %= 60;
return `${days} days ${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(Math.floor(seconds)).padStart(2, '0')}`;
}
export function convertKiToHumanReadable(input: string) {
const match = input.match(/^(\d+)(Ki)$/);
if (!match) return 'Invalid input';
const kibibytes = parseInt(match[1], 10);
const bytes = kibibytes * 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
let humanReadable = bytes;
while (humanReadable >= 1024 && i < sizes.length - 1) {
humanReadable /= 1024;
i++;
}
return `${humanReadable.toFixed(2)} ${sizes[i]}`;
}

3
src/lib/utils/string.ts Normal file
View File

@@ -0,0 +1,3 @@
export function capitalizeFirstLetter(text: string) {
return text.replace(/\b\w/g, (char) => char.toUpperCase());
}

37
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,37 @@
<script lang="ts">
import Header from '$lib/components/Header.svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
</script>
<div class="page">
<Header />
<div class="content">
<Sidebar />
<main>
<slot></slot>
</main>
</div>
</div>
<style lang="scss">
.page {
display: flex;
flex-direction: column;
max-width: 1440px;
margin: 0 auto;
}
.content {
display: flex;
padding-top: 1.3rem;
padding-bottom: 3rem;
main {
/* mobile: 1rem */
margin: 2rem;
width: 100%;
}
}
</style>

82
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,82 @@
<script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte';
</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>
<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.
</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>

View File

@@ -0,0 +1,90 @@
import type { PageServerLoad } from './$types';
import { getPods, getNodes, getDeployments, getDaemons } from '$lib/server/kubernetes';
import type { V1DaemonSet, V1Deployment, V1Node, V1Pod } from '@kubernetes/client-node';
function filterAndStackNodes(nodes: V1Node[], pods: V1Pod[]) {
const getNode = (name: string) => nodes.find((node) => node.metadata?.name === name);
pods.forEach((pod) => {
if (!pod.spec?.nodeName) return;
const node = getNode(pod.spec.nodeName);
node.pods.push(pod);
});
return nodes;
}
function filterAndStackDaemons(daemons: V1DaemonSet[], pods: V1Pod[]) {
const getDaemon = (name: string) =>
daemons.find((daemon: V1DaemonSet) => daemon.metadata?.name === name);
pods.forEach((pod: V1Pod) => {
if (!pod.metadata?.ownerReferences?.[0].name) return;
const daemon = getDaemon(pod.metadata.ownerReferences[0].name);
if (!daemon) return;
daemon?.pods.push(pod);
});
return daemons;
}
function filterAndStackDeploys(deploys: V1Deployment[], pods: V1Pod[]) {
const getDeploy = (name: string) =>
deploys.find((deploy) => {
return (
(deploy.spec?.selector.matchLabels?.app &&
deploy.spec.selector.matchLabels?.app === name) ||
(deploy.metadata?.labels?.['app.kubernetes.io/name'] &&
deploy.metadata.labels['app.kubernetes.io/name'] === name) ||
(deploy.metadata?.labels?.['k8s-app'] && deploy.metadata.labels['k8s-app'] === name)
);
});
pods.forEach((pod) => {
const name =
pod.metadata?.labels?.['k8s-app'] ||
pod.metadata?.labels?.['app.kubernetes.io/name'] ||
pod.metadata?.labels?.app ||
'not found';
const deploy = getDeploy(name);
if (!deploy) return;
deploy.pods.push(pod);
});
return deploys;
}
export const load: PageServerLoad = async () => {
const [podsResp, nodesResp, deployResp, daemonsResp] = await Promise.all([
getPods(),
getNodes(),
getDeployments(),
getDaemons()
]);
const pods: V1Pod[] = JSON.parse(JSON.stringify(podsResp));
let nodes: V1Node[] = JSON.parse(JSON.stringify(nodesResp));
let deployments: V1Deployment[] = JSON.parse(JSON.stringify(deployResp));
let daemons: V1DaemonSet[] = JSON.parse(JSON.stringify(daemonsResp));
nodes.forEach((node) => (node['pods'] = []));
deployments.forEach((deploy) => (deploy['pods'] = []));
daemons.forEach((daemon) => (daemon['pods'] = []));
nodes = filterAndStackNodes(nodes, pods);
deployments = filterAndStackDeploys(deployments, pods);
daemons = filterAndStackDaemons(daemons, pods);
// TODO move to frontend
deployments = deployments.sort((a, b) =>
a.metadata?.name && b.metadata?.name && a.metadata?.name > b.metadata?.name ? 1 : -1
);
return {
nodes,
deployments,
daemons,
pods
};
};

View File

@@ -0,0 +1,94 @@
<script lang="ts">
import Node from '$lib/components/Node.svelte';
import Deploy from '$lib/components/Deploy.svelte';
import Daemon from '$lib/components/Daemon.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import type { PageData } from './$types';
import type { V1DaemonSet, V1Deployment, V1Node } from '@kubernetes/client-node';
let { data }: { data: PageData } = $props();
const deployments: V1Deployment[] = data?.deployments;
const daemons: V1DaemonSet[] = data?.daemons;
const nodes: V1Node[] = data?.nodes;
</script>
<PageHeader>Cluster overview</PageHeader>
<details open>
<summary>
<h2>Cluster <span>{nodes.length} nodes</span></h2>
</summary>
<div class="server-list">
{#each nodes as node (node)}
<Node {node} />
{/each}
</div>
</details>
<details open>
<summary>
<h2>
Daemons <span
>{daemons.length} daemons ({daemons.reduce(
(total, item) => total + (item.pods ? item.pods.length : 0),
0
)} pods)</span
>
</h2>
</summary>
<div class="server-list deploys">
{#each daemons as daemon (daemon)}
<Daemon {daemon} />
{/each}
</div>
</details>
<details open>
<summary>
<h2>
Pods <span
>{deployments.length} deployments ({deployments.reduce(
(total, item) => total + (item.pods ? item.pods.length : 0),
0
)} pods)</span
>
</h2>
</summary>
<div class="server-list deploys">
{#each deployments as deploy (deploy)}
<Deploy {deploy} />
{/each}
</div>
</details>
<style lang="scss">
.server-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
margin-bottom: 2rem;
}
.server-list.deploys {
display: flex;
flex-wrap: wrap;
}
details summary::-webkit-details-marker,
details summary::marker {
display: none;
}
details > summary {
list-style: none;
cursor: pointer;
}
h2 {
font-family: 'Reckless Neue';
}
</style>

View File

@@ -0,0 +1,17 @@
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
};
};

View File

@@ -0,0 +1,165 @@
<script lang="ts">
import { browser } from '$app/environment';
import type { PageData } from './$types';
import type { V1Pod } from '@kubernetes/client-node';
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 { source } from 'sveltekit-sse';
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;
function setupWS() {
const url = new URL(`${window.location.origin}/cluster/pod/${pod?.metadata?.uid}/logs`);
if (pod?.metadata) {
console.error('missing pod info. not enough metadata to setup WS connection.');
return;
}
url.searchParams.append('pod', pod?.metadata?.name || '');
url.searchParams.append('namespace', pod?.metadata?.namespace || '');
url.searchParams.append('container', pod?.spec?.containers[0].name || '');
eventSource = new EventSource(url);
eventSource.onmessage = function (event) {
logs.update((arr) => arr.concat(event.data));
};
return (eventSource.onerror = (err) => {
console.error('EventSource failed:', err);
});
}
onMount(() => {
setInterval(() => uptime.update((n) => n + 1000), 1000);
return setupWS();
});
onDestroy(() => {
if (eventSource) {
eventSource.close();
console.log('EventSource closed');
}
});
</script>
<PageHeader>Pod: {pod?.metadata?.name}</PageHeader>
<Tabs>
<TabList>
<Tab>Details</Tab>
<Tab>Logs</Tab>
<Tab>Metadata</Tab>
<Tab>Deployment logs</Tab>
</TabList>
<TabView>
<div class="section-wrapper">
<Section title="Status" description="">
<div class="section-row">
<div class="section-element">
<label>Phase</label>
<span>{status?.phase}</span>
</div>
<div class="section-element">
<label>Pod IP</label>
<span>{status?.podIP}</span>
</div>
<div class="section-element">
<label>QoS Class</label>
<span>{status?.qosClass}</span>
</div>
<div class="section-element">
<label>Running for</label>
<span>{formatDuration($uptime / 1000)}</span>
</div>
</div>
</Section>
<Section title="Metadata" description="">
<div class="section-row">
<div class="section-element">
<label>Namespace</label>
<span>{metadata?.namespace}</span>
</div>
<div class="section-element">
<label>Parent resource</label>
<span>{metadata?.ownerReferences?.[0].kind}</span>
</div>
</div>
</Section>
<Section title="Spec" description="">
<div class="section-row">
<div class="section-element">
<label>Node name</label>
<span>{spec?.nodeName}</span>
</div>
<div class="section-element">
<label>Restart policy</label>
<span>{spec?.restartPolicy}</span>
</div>
<div class="section-element">
<label>Service account</label>
<span>{spec?.serviceAccount}</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>
<Logs logs={$logs} stream={true} />
</TabView>
<TabView>
<Logs logs={JSON.stringify(metadata, null, 2).split('\n')} lineNumbers={false} />
</TabView>
<TabView>
<Logs lineNumbers={false} />
</TabView>
</Tabs>

View File

@@ -0,0 +1,27 @@
import { createLogStream } from '$lib/server/kubernetes';
import { produce } from 'sveltekit-sse';
export function GET({ request }) {
return produce(async function start({ emit }) {
console.log('----- REQUEST -----');
const url = new URL(request.url);
const pod = url.searchParams.get('pod');
const namespace = url.searchParams.get('namespace');
const container = url.searchParams.get('container');
console.log('pod, namespace:', pod, namespace);
const k8sLogs = createLogStream(pod, namespace, container);
k8sLogs.start();
const unsubscribe = k8sLogs.logEmitter.subscribe((msg: string) => {
emit('message', msg);
});
const { error } = emit('message', `the time is ${Date.now()}`);
if (error) {
k8sLogs.stop();
unsubscribe();
return;
}
});
}

View File

@@ -0,0 +1,20 @@
<script>
import PageHeader from '$lib/components/PageHeader.svelte';
import Table from '$lib/components/Table.svelte';
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' }
];
</script>
<PageHeader>Health</PageHeader>
<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}
/>

View File

@@ -0,0 +1,26 @@
import type { PageServerLoad } from './$types';
import { getRouters } from '$lib/server/traefik';
let cache = {
timestamp: 0,
data: null
};
export const load: PageServerLoad = async () => {
const now = Date.now();
if (cache.data && now - cache.timestamp < 10000) {
console.log('Serving from cache');
return {
routers: cache.data
};
}
const routers = await getRouters();
cache = { timestamp: now, data: routers };
return {
routers
};
};

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte';
import Section from '$lib/components/Section.svelte';
import Table from '$lib/components/Table.svelte';
import type { PageData } from './$types';
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>
<div class="section-wrapper">
<Section title="Traefik" description="Treafik is a network proxy and webserver.">
<div class="section-row">
<div class="section-element">
<label>Number of routers</label>
<span>{routers.length}</span>
</div>
<div class="section-element">
<label>Providers</label>
<span>{providers?.join(', ')}</span>
</div>
</div>
</Section>
<Table title="Routers" description="Traefik routers available" {columns} data={routers} {links} />
</div>
<style lang="scss">
.server-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: left;
gap: 2rem;
}
</style>

View File

@@ -0,0 +1,29 @@
import type { PageServerLoad } from './$types';
import { getRouters } from '$lib/server/traefik';
const cache = {
timestamp: 0,
data: {}
};
export const load: PageServerLoad = async ({ params }) => {
const now = Date.now();
const { id } = params;
if (cache.data[id] && now - cache.timestamp < 10000) {
console.log('Serving from cache');
return {
router: cache.data[id]
};
}
const routers = await getRouters();
const router = routers.find((router) => router.service === id);
cache.time = now;
cache.data[id] = router;
return {
router
};
};

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
const { router } = data;
</script>
<PageHeader>Network: {router.service}</PageHeader>
<div>
<p>router:</p>
<pre><code>{JSON.stringify(router, null, 2)}</code></pre>
</div>

View File

@@ -0,0 +1,11 @@
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 type { Filament } from '$lib/interfaces/printer';
export const load: PageServerLoad = async (): Promise<{ p1p: Entity[]; filament: Filament[] }> => {
const p1p = await fetchP1P();
const filament = currentFilament();
return { p1p, filament };
};

View File

@@ -0,0 +1,237 @@
<script lang="ts">
import { capitalizeFirstLetter } from '$lib/utils/string';
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 Finished from '$lib/icons/finished.svelte';
import Paused from '$lib/icons/paused.svelte';
import Stopped from '$lib/icons/stopped.svelte';
import Printing from '$lib/icons/printing.svelte';
import PrinterIdle from '$lib/icons/printer-idle.svelte';
import PrinterPaused from '$lib/icons/printer-paused.svelte';
import PrinterPrinting from '$lib/icons/printer-printing.svelte';
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 type { PageData } from './$types';
import type { Entity } from '$lib/interfaces/homeassistant';
import type { Filament } from '$lib/interfaces/printer';
let { data }: { data: PageData } = $props();
const p1p: Entity[] = data?.p1p;
const filament: Filament[] = data?.filament;
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];
console.log(p1p);
let columns = ['Hex', 'Color', 'Material', 'Weight', 'Count', 'Link'];
const links = filament.map((f) => `/printer/${f.Color.replaceAll(' ', '-').toLowerCase()}`);
const iconDictStage = {
idle: PrinterIdle,
printing: PrinterPrinting,
paused: PrinterPaused,
stopped: PrinterStopped,
heatbed_preheating: PrinterPrinting,
cleaning_nozzle_tip: PrinterPrinting,
homing_toolhead: PrinterPrinting
};
const iconDictState = { running: Printing, pause: Paused, failed: Stopped, finish: Finished };
interface FilamentUpdated {
date: Date;
title?: string;
}
const lastUpdateFilament: FilamentUpdated = {
date: new Date('2025-04-01T05:47:01+00:00')
};
lastUpdateFilament.title = lastUpdateFilament.date
.toLocaleDateString('en-US', {
weekday: 'long',
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
.toLowerCase();
</script>
<PageHeader>Printer</PageHeader>
<div class="section-wrapper">
<Section
title="Printer status"
description="Historical printer information, last prints and current status."
>
<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
>
</div>
<div class="section-element">
<label>Bed temperature</label>
<span
><span class="icon"><BedTemperature /></span>{bedTemp.state}
{bedTemp.attributes.unit_of_measurement}</span
>
</div>
<div class="section-element">
<label>Nozzle temperature</label>
<span
><span class="icon"><NozzleTemperature /></span>{nozzleTemp.state}
{nozzleTemp.attributes.unit_of_measurement}</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
>
{printStatus.state}
</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>
{:else}
<span>Finished printing {currentLayer.state} of {totalLayer.state} 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>
<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>
<Table
title="Filaments"
description={`${filament.length} colors are currently in stock. Overview of currently stocked filament.`}
{columns}
data={filament}
{links}
footer={`Last updated on ${lastUpdateFilament.title}`}
/>
</div>
<style lang="scss">
img {
width: 120px;
position: absolute;
top: 1.2rem;
right: 1.2rem;
}
.section-element {
.icon {
display: inline-block;
--size: 2rem;
height: var(--size);
width: var(--size);
padding-right: 0.5rem;
&.spin {
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
animation: rotate 6s linear infinite;
transform-origin: calc((var(--size) / 2) - 2px) calc(var(--size) / 2);
}
}
}
.progress {
display: flex;
flex-direction: column;
width: 100%;
span {
margin-top: 0.5rem;
}
}
</style>

View File

@@ -0,0 +1,12 @@
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 };
};

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
const filament = data?.filament;
</script>
{#if filament !== null}
<PageHeader>Filament: {filament?.Color}</PageHeader>
<div class="page">
<div class="color-block" style={`background: ${filament?.Hex}`}></div>
</div>
{:else}
<PageHeader>Filament not found!</PageHeader>
<p>Unable to find filament {data.id}, no swatch to display.</p>
{/if}
<style lang="scss">
.color-block {
--size: 2rem;
padding: var(--size);
width: calc(100% - (2 * var(--size)));
height: calc(100% - (2 * var(--size)));
height: calc(90vh - 18rem);
border-radius: 1.5rem;
}
</style>

View File

@@ -0,0 +1,43 @@
import type { PageServerLoad } from './$types';
import type { Node, Cluster } from '$lib/interfaces/proxmox';
import { fetchNodes } from '$lib/server/proxmox';
const TTL = 10000; // 10 seconds
interface ClusterCache {
timestamp: number;
data: {
nodes: Node[];
cluster: Cluster | null;
};
}
let cache: ClusterCache = {
timestamp: 0,
data: {
nodes: [],
cluster: null
}
};
export const load: PageServerLoad = async () => {
const now = Date.now();
const hit = cache.data.cluster && cache.data.nodes?.length && now - cache.timestamp < TTL;
if (hit) {
const { nodes, cluster } = cache.data;
return { nodes, cluster };
}
const { nodes, cluster } = await fetchNodes();
nodes.sort((a: Node, b: Node) => {
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
});
cache = { timestamp: now, data: { nodes, cluster } };
return {
nodes,
cluster
};
};

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte';
import ServerComp from '$lib/components/Server.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
const { cluster, nodes } = data;
</script>
<PageHeader>Servers</PageHeader>
<div class="server-list">
{#each nodes as node (node.name)}
<div>
<ServerComp {node} />
</div>
{/each}
</div>
<style lang="scss">
.server-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: left;
gap: 2rem;
}
</style>

View File

@@ -0,0 +1,28 @@
<script>
import PageHeader from '$lib/components/PageHeader.svelte';
import Section from '$lib/components/Section.svelte';
</script>
<PageHeader>Sites</PageHeader>
<div class="section-wrapper">
<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."
/>
<Section
title="IP restrictions"
description="Restrict or block access to your application based on specific IP addresses or CIDR blocks."
/>
<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."
/>
<Section
title="Connected services"
description="Connected services can communicate with your application over the private network."
/>
</div>

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/fonts/Inter.woff2 Normal file

Binary file not shown.

Binary file not shown.

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
static/logo_grey.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
static/logo_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
static/printer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

146
static/style.css Normal file
View File

@@ -0,0 +1,146 @@
@font-face {
font-family: 'Inter';
font-style: normal;
src: url('/fonts/Inter.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
}
:root {
--bg: #f9f5f3;
--color: #1c1819;
--highlight: #eaddd5;
--positive: #00d439;
--negative: #ff5449;
--warning: #ffa312;
--border: 1px solid #eaddd5;
--border-radius: 0.75rem;
}
body {
font-family: 'Inter', sans-serif;
font-optical-sizing: auto;
margin: 0;
padding: 0;
background-color: var(--bg);
color: var(--color);
font-size: 14px;
}
a,
a:visited {
color: var(--color);
text-decoration-line: none;
}
h1 {
font-family: 'Reckless Neue';
}
h2 {
font-size: 20px;
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--str-space-100, 8px);
flex: 1 1 auto;
/* align-self: center; */
align-self: left;
/* text-transform: capitalize; */
flex-wrap: wrap;
font: var(
--str-font-heading-regular-l,
400 1.429rem/1.4 Inter,
Arial,
-apple-system,
BlinkMacSystemFont,
sans-serif
);
margin-top: 0;
color: var(--str-color-text-neutral-default);
font-weight: 500;
line-height: 100%;
}
button {
border: none;
position: relative;
background: transparent;
height: unset;
border-radius: 0.5rem;
display: inline-block;
text-decoration: none;
padding: 0 0.5rem;
flex: 1;
}
button span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
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;
}
button::after {
content: '';
position: absolute;
right: 0;
top: 0;
border-radius: 0.5rem;
width: 100%;
height: 100%;
transition: transform 0.1s ease;
will-change: box-shadow 0.25s;
pointer-events: none;
}
button:hover span {
border-color: #cab2aa;
background: #f9f5f3 !important;
}
.main-container {
background: white;
padding: 1.5rem;
border: var(--border);
border-radius: var(--border-radius, 1rem);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.section-wrapper {
display: flex;
flex-direction: column;
gap: 24px;
}
.section-row {
display: flex;
gap: 2rem;
}
.section-element {
display: flex;
flex-direction: column;
}
.section-element label {
font-weight: 500;
font-size: 15px;
}
.section-element > span {
display: flex;
align-items: center;
margin-top: 0.5rem;
}