1 Commits

Author SHA1 Message Date
b3985340aa better checks for when to build+deploy varnish image 2025-08-26 20:18:05 +02:00
99 changed files with 878 additions and 2878 deletions

View File

@@ -1,7 +1,7 @@
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: Build name: Build app
platform: platform:
os: linux os: linux
@@ -26,7 +26,7 @@ steps:
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: Publish name: Publish app to gchr
platform: platform:
os: linux os: linux
@@ -64,7 +64,58 @@ depends_on:
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: Deploy name: Publish varnish image
platform:
os: linux
arch: amd64
steps:
- name: check-config
image: alpine/git
commands:
- git fetch --no-tags --depth=2
- |
if git diff-tree --no-commit-id --name-only -r HEAD | grep -qE '(\.drone.yml|(varnish/.+(vcl|tmpl)(\n|$)))'; then
echo "Changes detected in varnish config"
else
echo "No changes in varnish config file, skipping..."
exit 78 # exit code 78 = skip in Drone
fi
- name: Publish varnish to ghcr
image: plugins/docker
settings:
registry: ghcr.io
repo: ghcr.io/kevinmidboe/varnish-infra-map
contexT: varnish
dockerfile: Dockerfile
compress: true
username:
from_secret: GITHUB_USERNAME
password:
from_secret: GHCR_UPLOAD_TOKEN
build_args_from_env:
- IMAGE_HOST
- IMAGE_PROXY
tags:
- latest
- ${DRONE_COMMIT_SHA}
trigger:
event:
include:
- push
exclude:
- pull_request
branch:
- main
- update
---
kind: pipeline
type: docker
name: Deploy to kubernetes
platform: platform:
os: linux os: linux
@@ -81,7 +132,7 @@ steps:
commands: commands:
- mkdir -p /root/.kube - mkdir -p /root/.kube
- echo "IMAGE=ghcr.io/kevinmidboe/${DRONE_REPO_NAME}:${DRONE_COMMIT_SHA}" > /root/.kube/.env - echo "IMAGE=ghcr.io/kevinmidboe/${DRONE_REPO_NAME}:${DRONE_COMMIT_SHA}" > /root/.kube/.env
- echo "VARNISH_IMAGE=ghcr.io/kevinmidboe/varnish-${DRONE_REPO_NAME}:latest" >> /root/.kube/.env - echo "VARNISH_IMAGE=ghcr.io/kevinmidboe/varnish-${DRONE_REPO_NAME}" >> /root/.kube/.env
- echo "NAMESPACE=${DRONE_REPO_NAME}" >> /root/.kube/.env - echo "NAMESPACE=${DRONE_REPO_NAME}" >> /root/.kube/.env
- 'curl -s - 'curl -s
-H "X-Vault-Token: $VAULT_TOKEN" -H "X-Vault-Token: $VAULT_TOKEN"
@@ -129,57 +180,6 @@ depends_on:
volumes: volumes:
- name: kube-config - name: kube-config
temp: {} temp: {}
---
kind: pipeline
type: docker
name: Publish varnish
platform:
os: linux
arch: amd64
steps:
- name: Check for varnish changes
image: alpine/git
commands:
- git fetch --no-tags --depth=2
- |
if git diff-tree --no-commit-id --name-only -r HEAD | grep -qE '(\.drone.yml|(varnish/.+(vcl|tmpl)(\n|$)))'; then
echo "Changes detected in varnish config"
else
echo "No changes in varnish config file, skipping..."
exit 78 # exit code 78 = skip in Drone
fi
- name: Publish varnish image to ghcr
image: plugins/docker
settings:
registry: ghcr.io
repo: ghcr.io/kevinmidboe/varnish-infra-map
context: varnish
dockerfile: varnish/Dockerfile
compress: true
username:
from_secret: GITHUB_USERNAME
password:
from_secret: GHCR_UPLOAD_TOKEN
tags:
- latest
- ${DRONE_COMMIT_SHA}
trigger:
event:
include:
- push
exclude:
- pull_request
branch:
- main
- update
--- ---
kind: signature kind: signature
hmac: b4b6a98b76fdf3cf297b46cf986a3d46f3d4050e623f2c769267181c7075a6ca hmac: 3ba933b1c9b7f6bda1691bfb0335290f461d08cebfc5e9fe60c2f272604189d0
...

View File

@@ -1,9 +0,0 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: varnish-config
namespace: ${NAMESPACE}
data:
PROXY_HOST: ${PROXY_HOST}
IMAGE_HOST: ${IMAGE_HOST}

View File

@@ -0,0 +1,8 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: varnish-vcl
namespace: ${NAMESPACE}
binaryData:
default.vcl: dmNsIDQuMDsKCmltcG9ydCBzdGQ7CmltcG9ydCBkaWdlc3Q7CgojIERlZmluZSBiYWNrZW5kIHBvaW50aW5nIHRvIEhvbWUgQXNzaXN0YW50IElQCmJhY2tlbmQgaGFzc19iYWNrZW5kIHsKICAgIC5ob3N0ID0gIjEwLjAuMC44MiI7CiAgICAucG9ydCA9ICI4MTIzIjsKfQoKc3ViIHZjbF9yZWN2IHsKICAgICMgSGFuZGxlIENPUlMgcHJlZmxpZ2h0CiAgICBpZiAocmVxLm1ldGhvZCA9PSAiT1BUSU9OUyIpIHsKICAgICAgICByZXR1cm4gKHN5bnRoKDIwNCwgIlByZWZsaWdodCIpKTsKICAgIH0KCiAgICAjIFJld3JpdGUgaW1hZ2UgVVJMCiAgICBpZiAocmVxLnVybCB+ICJeL2ltYWdlLyIpIHsKICAgICAgICAjIEV4dHJhY3QgZXZlcnl0aGluZyBhZnRlciAvaW1hZ2UvIGFuZCBzdG9yZSBpdAogICAgICAgIHNldCByZXEuaHR0cC5YLUltYWdlLVVSTCA9IHJlZ3N1YihyZXEudXJsLCAiXi9pbWFnZS8oLiopIiwgIlwxIik7CiAgICAgICAgIyBSZXdyaXRlIHJlcS51cmwgdG8gbWF0Y2ggYmFja2VuZCBleHBlY3RhdGlvbnMKICAgICAgICBzZXQgcmVxLnVybCA9IHJlZ3N1YihyZXEuaHR0cC5YLUltYWdlLVVSTCwgIl5odHRwOi8vW14vXSsiLCAiIik7CiAgICB9CgogICAgIyBSZW1vdmUgY29va2llcyBzbyBjb250ZW50IGlzIGNhY2hlYWJsZQogICAgdW5zZXQgcmVxLmh0dHAuQ29va2llOwp9CgpzdWIgdmNsX3N5bnRoIHsKICAgIGlmIChyZXNwLnN0YXR1cyA9PSAyMDQpIHsKICAgICAgICBzZXQgcmVzcC5odHRwLkFjY2Vzcy1Db250cm9sLUFsbG93LU9yaWdpbiA9ICIqIjsKICAgICAgICBzZXQgcmVzcC5odHRwLkFjY2Vzcy1Db250cm9sLUFsbG93LU1ldGhvZHMgPSAiR0VULCBPUFRJT05TIjsKICAgICAgICBzZXQgcmVzcC5odHRwLkFjY2Vzcy1Db250cm9sLUFsbG93LUhlYWRlcnMgPSAiQ29udGVudC1UeXBlLCBYLUNhY2hlLUlEIjsKICAgICAgICBzZXQgcmVzcC5odHRwLkNvbnRlbnQtTGVuZ3RoID0gIjAiOwogICAgICAgIHJldHVybiAoZGVsaXZlcik7CiAgICB9CgogICAgaWYgKHJlc3Auc3RhdHVzID09IDMwNCkgewogICAgICAgIHNldCByZXNwLmh0dHAuRVRhZyA9IHJlcS5odHRwLklmLU5vbmUtTWF0Y2g7CiAgICAgICAgc2V0IHJlc3AuaHR0cC5Db250ZW50LUxlbmd0aCA9ICIwIjsKICAgICAgICByZXR1cm4gKGRlbGl2ZXIpOwogICAgfQp9CgpzdWIgdmNsX2JhY2tlbmRfZmV0Y2ggewogICAgIyBBbHdheXMgdXNlIHRoZSBIQVNTIGJhY2tlbmQKICAgIHNldCBiZXJlcS5iYWNrZW5kID0gaGFzc19iYWNrZW5kOwoKICAgICMgU2V0IHByb3BlciBIb3N0IGhlYWRlciBmcm9tIG9yaWdpbmFsIFVSTAogICAgIyBpZiAoYmVyZXEuaHR0cC5YLUltYWdlLVVSTCkgewogICAgIyAgICAgc2V0IGJlcmVxLmh0dHAuSG9zdCA9IHJlZ3N1YihiZXJlcS5odHRwLlgtSW1hZ2UtVVJMLCAiXmh0dHA6Ly8oW14vXSspLioiLCAiXDEiKTsKICAgICMgICAgIHNldCBiZXJlcS5odHRwLkhvc3QgPSByZWdzdWIoYmVyZXEuaHR0cC5Ib3N0LCAiOlswLTldKyQiLCAiIik7CiAgICAjIH0KfQoKc3ViIHZjbF9iYWNrZW5kX3Jlc3BvbnNlIHsKICAgIHNldCBiZXJlc3AudHRsID0gMXM7CiAgICBzZXQgYmVyZXNwLmdyYWNlID0gNjBzOwogICAgc2V0IGJlcmVzcC5rZWVwID0gNjBzOwoKICAgICMgRW5zdXJlIEVUYWcgaXMgcGFzc2VkIHRvIGNsaWVudAogICAgaWYgKGJlcmVzcC5odHRwLkVUYWcpIHsKICAgICAgICBzZXQgYmVyZXNwLmh0dHAuWC1DYWNoZS1FVGFnID0gYmVyZXNwLmh0dHAuRVRhZzsKICAgIH0gZWxzZSB7CiAgICAgICAgIyBPcHRpb25hbDogZ2VuZXJhdGUgb25lIGlmIG5vdCBwcm92aWRlZAogICAgICAgICMgc2V0IGJlcmVzcC5odHRwLkVUYWcgPSBkaWdlc3QuaGFzaF9tZDUoYmVyZXNwLmJvZHkpOwogICAgICAgIHNldCBiZXJlc3AuaHR0cC5FVGFnID0gYmVyZXNwLmh0dHAuQ29udGVudC1MZW5ndGg7CiAgICAgICAgc2V0IGJlcmVzcC5odHRwLlgtQ2FjaGUtRVRhZyA9IGJlcmVzcC5odHRwLkVUYWc7CiAgICB9Cn0KCnN1YiB2Y2xfaGl0IHsKICAgIGlmIChvYmoudHRsIDwgMHMgJiYgc3RkLmhlYWx0aHkocmVxLmJhY2tlbmRfaGludCkpIHsKICAgICAgICByZXR1cm4gKGRlbGl2ZXIpOwogICAgfQp9CgpzdWIgdmNsX2RlbGl2ZXIgewogICAgdW5zZXQgcmVzcC5odHRwLlgtSW1hZ2UtVVJMOwogICAgc2V0IHJlc3AuaHR0cC5BY2Nlc3MtQ29udHJvbC1BbGxvdy1PcmlnaW4gPSAiKiI7CgogICAgIyBIYW5kbGUgY29uZGl0aW9uYWwgcmVxdWVzdCB3aXRoIEVUYWcKICAgIGlmICgKICAgICAgICByZXEuaHR0cC5JZi1Ob25lLU1hdGNoICYmCiAgICAgICAgcmVxLmh0dHAuSWYtTm9uZS1NYXRjaCA9PSByZXNwLmh0dHAuRVRhZwogICAgKSB7CiAgICAgICAgcmV0dXJuIChzeW50aCgzMDQpKTsKICAgIH0KfQo=

View File

@@ -1,46 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
labels:
app: infra-map
name: infra-map
namespace: ${NAMESPACE}
spec:
replicas: 2
selector:
matchLabels:
app: infra-map
template:
metadata:
labels:
app: infra-map
spec:
containers:
- name: infra-map
image: ${IMAGE}
imagePullPolicy: IfNotPresent
resources:
limits:
cpu: 300m
memory: 828Mi
requests:
cpu: 250m
memory: 64Mi
env:
- name: ORIGIN
value: http://infra-map.infra-map.svc.cluster.local:3000
- name: PROTOCOL_HEADER
value: x-forwarded-proto
- name: HOST_HEADER
value: x-forwarded-host
- name: PORT_HEADER
value: x-forwarded-port
- name: ENV
value: production
envFrom:
- secretRef:
name: secret-env-values
imagePullSecrets:
- name: ghcr-login-secret

View File

@@ -1,40 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
labels:
app: varnish
name: varnish
namespace: ${NAMESPACE}
spec:
replicas: 2
selector:
matchLabels:
app: varnish
template:
metadata:
labels:
app: varnish
spec:
containers:
- command:
- /usr/local/bin/docker-entrypoint.sh
envFrom:
- configMapRef:
name: varnish-config
image: ghcr.io/kevinmidboe/varnish-infra-map:latest
imagePullPolicy: Always
name: varnish
resources:
limits:
cpu: 900m
memory: 828Mi
requests:
cpu: 250m
memory: 64Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
imagePullSecrets:
- name: ghcr-login-secret
dnsPolicy: ClusterFirst

View File

@@ -0,0 +1,56 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
labels:
app: infra-map
name: infra-map
namespace: ${NAMESPACE}
spec:
replicas: 2
selector:
matchLabels:
app: infra-map
template:
metadata:
labels:
app: infra-map
spec:
containers:
- image: ${IMAGE}
imagePullPolicy: IfNotPresent
name: infra-map
envFrom:
- secretRef:
name: secret-env-values
resources:
limits:
cpu: 900m
memory: 828Mi
requests:
cpu: 250m
memory: 64Mi
- image: ${VARNISH_IMAGE}:latest
imagePullPolicy: IfNotPresent
name: varnish
command: ['varnishd']
args: ['-F', '-f', '/etc/varnish/default.vcl', '-a', ':6081', '-s', 'malloc,512m']
volumeMounts:
- name: varnish-vcl
mountPath: /etc/varnish/default.vcl
subPath: default.vcl
resources:
limits:
cpu: 900m
memory: 828Mi
requests:
cpu: 250m
memory: 64Mi
restartPolicy: Always
imagePullSecrets:
- name: ghcr-login-secret
volumes:
- name: varnish-vcl
configMap:
name: varnish-vcl

View File

@@ -12,7 +12,7 @@ spec:
paths: paths:
- backend: - backend:
service: service:
name: varnish name: infra-map-service
port: port:
number: 80 number: 80
path: / path: /

View File

@@ -3,35 +3,18 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
labels: labels:
app: varnish app: infra-map
name: varnish name: infra-map-service
namespace: ${NAMESPACE} namespace: ${NAMESPACE}
spec: spec:
ports: ports:
- port: 80 - port: 80
name: http-varnish name: http
protocol: TCP protocol: TCP
targetPort: 6081 targetPort: 6081
selector:
app: varnish
sessionAffinity: None
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
labels:
app: infra-map
name: infra-map
namespace: ${NAMESPACE}
spec:
ports:
- port: 80
name: http-app
protocol: TCP
targetPort: 3000
selector: selector:
app: infra-map app: infra-map
sessionAffinity: None sessionAffinity: None
type: ClusterIP type: ClusterIP
status:
loadBalancer: {}

View File

@@ -5,8 +5,8 @@ services:
build: build:
context: varnish context: varnish
dockerfile: Dockerfile dockerfile: Dockerfile
environment: args:
# sets environment variables. Overridden by env, but has sane defaults # sets build variables. Overridden by env, but has sane defaults
IMAGE_HOST: ${IMAGE_HOST:-homeassistant.local} IMAGE_HOST: ${IMAGE_HOST:-homeassistant.local}
PROXY_HOST: ${PROXY_HOST:-app} PROXY_HOST: ${PROXY_HOST:-app}
ports: ports:
@@ -20,7 +20,6 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
env_file: .env # sets container's environment env_file: .env # sets container's environment
environment: environment:
- ORIGIN=http://localhost:3000
- NODE_ENV=production - NODE_ENV=production
- PROTOCOL_HEADER=x-forwarded-proto - PROTOCOL_HEADER=x-forwarded-proto
- HOST_HEADER=x-forwarded-host - HOST_HEADER=x-forwarded-host

View File

@@ -1,53 +0,0 @@
#!/usr/bin/env node
/**
* Usage: node convert-svg-to-svelte.js [inputDir] [outputDir]
* Defaults: ./svgs ./svelte
*/
import fs from 'fs';
import path from 'path';
const INPUT_DIR = process.argv[2] || '../svgs';
const OUTPUT_DIR = process.argv[3] || '../src/lib/icons';
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true });
function processSvg(svgContent) {
// Strip XML/DOCTYPE
let out = svgContent.replace(/<\?xml[\s\S]*?\?>\s*/i, '').replace(/<!DOCTYPE[\s\S]*?>\s*/i, '');
// Remove ALL comments
out = out.replace(/<!--[\s\S]*?-->\s*/g, '');
// Remove <g id="icomoon-ignore"></g> with any whitespace between tags
out = out.replace(/<g\s+id=(["'])icomoon-ignore\1\s*>\s*<\/g>\s*/gi, '');
// Ensure only width="100%" height="100%" on the <svg> tag
out = out.replace(/<svg\b[^>]*>/i, (match) => {
let tag = match
.replace(/\s+(width|height)\s*=\s*"[^"]*"/gi, '')
.replace(/\s+(width|height)\s*=\s*'[^']*'/gi, '');
return tag.replace(/>$/, ' width="100%" height="100%">');
});
// Prepend the single license comment
out = '<!-- generated by icomoon.io - licensed Lindua icon -->\n' + out.replace(/^\s+/, '');
return out;
}
function convertSvgs(inputDir = INPUT_DIR, outputDir = OUTPUT_DIR) {
if (!fs.existsSync(inputDir)) {
console.warn(`Input directory not found: ${inputDir}`);
return;
}
const files = fs.readdirSync(inputDir).filter((f) => f.toLowerCase().endsWith('.svg'));
files.forEach((file) => {
const src = path.join(inputDir, file);
const dest = path.join(outputDir, file.replace(/\.svg$/i, '.svelte'));
const svgContent = fs.readFileSync(src, 'utf8');
const processed = processSvg(svgContent);
fs.writeFileSync(dest, processed, 'utf8');
console.log(`Converted: ${file} -> ${path.basename(dest)}`);
});
}
convertSvgs();

View File

@@ -1,52 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
function systemDarkModeEnabled() {
const computedStyle = window.getComputedStyle(document.body);
if (computedStyle?.colorScheme != null) {
return computedStyle.colorScheme.includes("dark");
}
return false;
}
function updateBodyClass() {
document.body.className = darkmode ? "dark" : "light";
}
let darkmode = $state(false);
const darkmodeToggleIcon = $derived(darkmode ? "🌝" : "🌚");
function toggleDarkmode() {
darkmode = !darkmode;
updateBodyClass()
}
onMount(() => {
darkmode = systemDarkModeEnabled()
updateBodyClass()
})
</script>
<div class="darkToggle">
<span on:click={toggleDarkmode}>{ darkmodeToggleIcon }</span>
</div>
<style lang="scss" scoped>
.darkToggle {
height: 25px;
width: 25px;
cursor: pointer;
position: fixed;
margin-bottom: 1.5rem;
margin-right: 2px;
bottom: 0;
right: 0;
z-index: 10;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>

View File

@@ -1,17 +1,12 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy, createEventDispatcher } from 'svelte';
import { clickOutside } from '$lib/utils/mouseEvents'; import { clickOutside } from '$lib/utils/mouseEvents';
interface Props { export let title: string;
title: string; export let description: string | null = null;
description: string | null;
close(): void const dispatch = createEventDispatcher();
} const close = () => dispatch('close');
const {
title,
description,
close
}: Props = $props()
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
@@ -45,7 +40,6 @@
class="dialog" class="dialog"
> >
<div tabindex="-1" id="dialog-title" class="title"> <div tabindex="-1" id="dialog-title" class="title">
{#if title.length || description?.length}
<header> <header>
<button on:click={close} aria-disabled="false" aria-label="Close" type="button" tabindex="0" <button on:click={close} aria-disabled="false" aria-label="Close" type="button" tabindex="0"
><svg viewBox="0 0 24 24" aria-hidden="true" tabindex="-1" height="100%" width="100%" ><svg viewBox="0 0 24 24" aria-hidden="true" tabindex="-1" height="100%" width="100%"
@@ -56,14 +50,13 @@
</button> </button>
<h5>{title}</h5> <h5>{title}</h5>
</header> </header>
{/if}
<main> <main>
{#if description}
<div id="dialog-description"> <div id="dialog-description">
{#if description}
{@html description} {@html description}
</div>
{/if} {/if}
</div>
<!-- <!--
<div class="alerts"> <div class="alerts">
@@ -90,8 +83,6 @@
align-items: flex-start; align-items: flex-start;
justify-content: center; justify-content: center;
position: fixed; position: fixed;
--offset-top: 4rem;
padding-top: var(--offset-top);
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
@@ -105,26 +96,18 @@
visibility 0.4s ease; visibility 0.4s ease;
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
align-items: center;
@media screen and (max-width: 480px) {
padding: 1rem;
width: calc(100vw - 2rem);
height: calc(100vh - 2rem);
}
> div { > div {
max-width: 880px; max-width: 880px;
// max-width: unset; max-width: unset;
} }
} }
.title { .title {
--padding: 1rem; --padding: 1rem;
--background-color: #ffffff;
--text-color: black;
position: relative; position: relative;
background-color: var(--background-color); background-color: #ffffff;
color: var(--text-color);
background-clip: padding-box; background-clip: padding-box;
border-radius: 12px; border-radius: 12px;
display: flex; display: flex;
@@ -140,6 +123,7 @@
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 9px 28px 8px rgba(0, 0, 0, 0.05); 0 9px 28px 8px rgba(0, 0, 0, 0.05);
pointer-events: auto; pointer-events: auto;
max-height: 90vh;
padding: var(--padding); padding: var(--padding);
width: calc(880px - calc(--padding * 2)); width: calc(880px - calc(--padding * 2));
z-index: 2008; z-index: 2008;

View File

@@ -4,10 +4,10 @@
import { clickOutside } from '$lib/utils/mouseEvents'; import { clickOutside } from '$lib/utils/mouseEvents';
export let options = ['Today', 'Yesterday', 'Last 7 Days', 'Last 30 Days', 'All time']; export let options = ['Today', 'Yesterday', 'Last 7 Days', 'Last 30 Days', 'All time'];
export let selected: string | undefined = undefined; export let selected;
export let placeholder = ''; export let placeholder = '';
export let label = ''; export let label = '';
export let icon: unknown = undefined; export let icon = undefined;
export let required = false; export let required = false;
let dropdown: Element; let dropdown: Element;
@@ -29,7 +29,9 @@
} }
function handleClick(event: MouseEvent) { function handleClick(event: MouseEvent) {
console.log('dropdown element:', dropdown);
const outside = clickOutside(event, dropdown); const outside = clickOutside(event, dropdown);
console.log('click outside:', outside);
if (outside === false) { if (outside === false) {
return; return;
} }

View File

@@ -1,345 +0,0 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { goto } from '$app/navigation';
import Dialog from './Dialog.svelte';
import Input from './Input.svelte';
import { allRoutes } from '$lib/remote/filesystem.remote.ts';
import type { PageRoute } from '$lib/remote/filesystem.remote.ts';
type ShortcutHandler = (event: KeyboardEvent) => void;
interface Shortcut {
keys: string[];
handler: ShortcutHandler;
description?: string;
}
interface MinimalElement {
name: string;
link: string;
}
interface OverlayData {
type: 'elements' | 'pages' | null;
content: MinimalElement | unknown;
}
class KeyboardShortcutManager {
private shortcuts: Shortcut[] = [];
constructor() {
window.addEventListener('keydown', this.handleKeydown);
}
register(shortcut: Shortcut) {
this.shortcuts.push(shortcut);
}
unregisterAll() {
this.shortcuts = [];
window.removeEventListener('keydown', this.handleKeydown);
}
private handleKeydown = (event: KeyboardEvent) => {
const pressedKeys = [
event.metaKey ? 'Meta' : '',
event.ctrlKey ? 'Control' : '',
event.shiftKey ? 'Shift' : '',
event.altKey ? 'Alt' : '',
event.key.toUpperCase()
].filter(Boolean);
for (const shortcut of this.shortcuts) {
if (this.isMatch(shortcut.keys, pressedKeys)) {
event.preventDefault();
shortcut.handler(event);
return;
}
}
// some other key, but not overlay is not open. Nothing to do
if (!overlayStore.type) return;
// listen for text, any letter should reset focusIndex
const singleLetter = (event.key.length == 1 && event.key.match(/\D/)) || 0 > 0;
if (singleLetter) {
updateFocus(0);
}
// listen for number as shortcut actions
const digit = event.key.match(/\d/)?.[0];
if (digit?.length && digit?.length > 0) {
setTimeout(() => {
filterString = String(filterString)?.replaceAll(digit, '');
}, 1);
updateFocus(Number(digit) - 1);
}
// listen for arrow keys
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
const direction = event.key === 'ArrowDown' ? 1 : -1;
updateFocus(focusIndex + 1 * direction);
}
// listen for enter key
if (event.key === 'Enter' && filteredchildren.length > 0) {
const { link, path } = filteredchildren[focusIndex];
hideOverlay();
openElement(link || path);
}
};
private isMatch(shortcutKeys: string[], pressedKeys: string[]) {
return (
shortcutKeys.length === pressedKeys.length &&
shortcutKeys.every((key) => pressedKeys.includes(key))
);
}
}
let overlayStore: OverlayData = $state({ type: null, content: null });
let pages: Array<PageRoute> = $state([]);
function showOverlay(type: 'elements' | 'pages', content: unknown) {
if (type === overlayStore.type) return hideOverlay();
overlayStore = { type, content };
focusSearchInput();
filterString = '';
updateFocus(0);
}
function hideOverlay() {
overlayStore = { type: null, content: null };
}
async function resolvePages() {
pages = await allRoutes();
}
// call as soon as possible, even if blocking
resolvePages();
// setup managers
let manager: KeyboardShortcutManager;
const className = 'search-container';
// search & filter
let filterString = $state('');
let focusIndex = $state(0);
let filteredchildren = $derived.by(() => {
return overlayStore.content?.filter((a) => a?.name.toLowerCase().includes(filterString));
});
const updateFocus = (index: number) => {
if (index < 0) index = 0;
else if (index >= filteredchildren.length) {
index = filteredchildren.length - 1;
}
focusIndex = index;
};
// setup & register
const focusSearchInput = () => {
setTimeout(() => {
const input = document.getElementsByClassName(className)[0]?.getElementsByTagName('input')[0];
input.focus();
}, 50);
};
function openElement(link: string) {
if (String(link)?.startsWith('/')) {
goto(link);
} else {
window.open(link, '_blank');
}
}
onMount(() => {
manager = new KeyboardShortcutManager();
manager.register({
keys: ['Meta', 'K'],
handler: () => {
if (!window || !('elements' in window)) return;
const elements = window?.elements;
showOverlay('elements', elements);
}
});
manager.register({
keys: ['Meta', 'J'],
handler: () => showOverlay('pages', pages)
});
});
onDestroy(() => manager?.unregisterAll());
</script>
{#if overlayStore.type}
<Dialog close={hideOverlay} title="" description="">
<div class={className}>
<Input label="" bind:value={filterString} placeholder="Search anything..." />
<ul>
{#each filteredchildren as element, index (element)}
<li
class={index === focusIndex ? 'focus' : ''}
on:click={() => 'link' in element && openElement(element.link ?? '/')}
>
<div class="header">
<h3>{element?.name}</h3>
<span class="sub">{element?.link}</span>
</div>
<div class="group">
{#each Object.entries(element) as [key, value] (key)}
<span><b>{key}:</b> {value}</span>
{/each}
</div>
</li>
{/each}
</ul>
<div class="hint">
Press <span class="kbd">Esc</span> to close · <span class="kbd"></span> +
<span class="kbd">K</span> to toggle
</div>
</div>
</Dialog>
{/if}
<style lang="scss">
:global(.dialog .title) {
--padding: 1.2rem;
--background-color: rgba(255, 255, 255, 0.92);
--text-color: black;
}
:global(body.dark .dialog .title) {
--background-color: rgba(25, 25, 34, 0.92);
--text-color: white;
}
:global(.dialog .title .input) {
--padding-w: 1rem;
}
.search-container {
width: 700px;
position: relative;
@media screen and (max-width: 480px) {
width: 100%;
ul {
max-height: calc(50vh);
}
}
ul {
list-style-type: none;
margin: 0;
margin-top: 0.3rem;
padding-left: 0;
overflow-y: scroll;
overflow-y: scroll;
max-height: calc(100vh - var(--offset-top) * 4);
max-height: 75vh;
}
li {
--blur: 18px;
border: var(--border);
border-radius: var(--border-radius);
position: relative;
display: flex;
flex-direction: column;
transition: background 0.15s;
padding: 0.875rem 1rem;
margin: 0.5rem 0;
background-color: var(--card-bg);
cursor: pointer;
user-select: none;
gap: 0.375rem;
&.focus,
&:hover {
&::after {
content: '';
position: absolute;
width: calc(100% - 4px);
height: calc(100% - 2px);
border: 2px solid var(--theme);
top: -1px;
left: -1px;
border-radius: var(--border-radius);
}
}
.header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.5rem;
h3 {
font-weight: 600;
font-size: 1.1rem;
letter-spacing: 0.2px;
margin: 0;
}
.sub {
color: #999;
font-size: 13px;
font-family: monospace;
}
}
.group {
display: flex;
flex-wrap: wrap;
gap: 0.625rem 1.125rem;
margin-top: 0.25rem;
font-size: 0.8rem;
font-size: 13px;
color: var(--muted);
span {
display: inline-flex;
gap: 4px;
b {
color: var(--key);
font-weight: 400;
}
}
}
}
/* Small footer hint */
.hint {
margin-top: 10px;
text-align: right;
color: var(--muted);
font-size: 13px;
.kbd {
background-color: rgba(0, 0, 0, 0.16);
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
}
}
:global(.search-container .label-input) {
position: sticky;
top: 0;
overflow: unset;
}
</style>

View File

@@ -46,7 +46,7 @@
background: var(--theme); background: var(--theme);
padding: 0 1rem; padding: 0 1rem;
border-radius: 6px; border-radius: 6px;
color: var(--bg); color: white;
margin: 1rem 0.5rem 0 0.5rem; margin: 1rem 0.5rem 0 0.5rem;
font-weight: 400; font-weight: 400;
font-size: 1rem; font-size: 1rem;
@@ -66,7 +66,7 @@
font-size: 1.5rem; font-size: 1.5rem;
padding: 0; padding: 0;
font-weight: 300; font-weight: 300;
color: var(--bg) !important; color: white !important;
} }
img { img {

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
export let label: string; export let label: string;
export let value: string;
export let placeholder: string; export let placeholder: string;
export let value: string = '';
export let required = false; export let required = false;
export let icon: unknown = null; export let icon: unknown;
let focus = false; let focus = false;
</script> </script>
@@ -33,10 +33,6 @@
</div> </div>
<style lang="scss"> <style lang="scss">
:global(body.dark .label-input .input) {
background: var(--highlight);
}
.label-input { .label-input {
width: 100%; width: 100%;
@@ -58,9 +54,8 @@
.input { .input {
position: relative; position: relative;
display: flex; display: flex;
--padding-h: 0.25rem; --padding: 0.75rem;
--padding-w: 0.75rem; width: calc(100% - (var(--padding) * 2));
width: calc(100% - (var(--padding-w) * 2));
height: 2.5rem; height: 2.5rem;
background: #ffffff; background: #ffffff;
align-items: center; align-items: center;
@@ -71,7 +66,7 @@
outline: none; outline: none;
display: flex; display: flex;
align-items: center; align-items: center;
padding: var(--padding-h) var(--padding-w); padding: 0px var(--padding);
&.focus { &.focus {
box-shadow: 0px 0px 0px 4px #7d66654d; box-shadow: 0px 0px 0px 4px #7d66654d;

View File

@@ -1,96 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import CubeSide from '$lib/icons/cube-side.svelte';
import HardDrive from '$lib/icons/hard-disk.svelte';
import Network from '$lib/icons/network.svelte';
import CPU from '$lib/icons/cpu.svelte';
import Fingerprint from '$lib/icons/fingerprint.svelte';
import ExtractUp from '$lib/icons/extract-up.svelte';
import InsertDown from '$lib/icons/insert-down.svelte';
import Clock from '$lib/icons/clock.svelte';
import Memory from '$lib/icons/floppy-disk.svelte';
import { formatBytes, formatDuration } from '$lib/utils/conversion';
import type { LXC } from '$lib/interfaces/proxmox';
const { lxc }: { lxc: LXC } = $props();
</script>
<div class="card">
<div class="header">
<div class="icon"><CubeSide /></div>
<span class="name">{lxc.name} <span class="subtle">{lxc.vmid}</span></span>
<span class={`status ${lxc.status === 'running' ? 'ok' : 'error'}`}></span>
</div>
<div class="resource">
<div class="title">
<Network />
<span>Status</span>
</div>
<span>{lxc.status}</span>
<div class="title">
<Network />
<span>CPUs</span>
</div>
<span>{lxc.cpus}</span>
<div class="title">
<CPU />
<span>CPU</span>
</div>
<span>{Math.floor(lxc.cpu * 100) / 100}</span>
<div class="title">
<HardDrive />
<span>Max Disk</span>
</div>
<span>{formatBytes(lxc.maxdisk)}</span>
<div class="title">
<Memory />
<span>Memory</span>
</div>
<span>{formatBytes(lxc.mem)}</span>
<div class="title">
<InsertDown />
<span>Net In</span>
</div>
<span>{formatBytes(lxc.netin)}</span>
<div class="title">
<ExtractUp />
<span>Net Out</span>
</div>
<span>{formatBytes(lxc.netout / 8)}</span>
<div class="title">
<Clock />
<span>Uptime</span>
</div>
<span>{formatDuration(lxc.uptime)}</span>
<div class="title">
<Fingerprint />
<span>lxc ID</span>
</div>
<span>{lxc.vmid}</span>
</div>
<div class="footer">
<button on:click={() => goto(`/servers/lxc/${lxc.vmid}`)}>
<span>Pod details</span>
</button>
</div>
</div>
<style lang="scss">
@import '../styles/card.scss';
.card {
flex-grow: 1;
max-width: 550px;
}
</style>

View File

@@ -3,7 +3,7 @@
import { grey400x225 } from '$lib/utils/staticImageSource'; import { grey400x225 } from '$lib/utils/staticImageSource';
import Dialog from './Dialog.svelte'; import Dialog from './Dialog.svelte';
const IMAGE_REFRESH_INTERVAL = 1000; const IMAGE_REFRESH_INTERVAL = 3000;
let { imageUrl }: { imageUrl: string } = $props(); let { imageUrl }: { imageUrl: string } = $props();
let lastUpdated = new Date(); let lastUpdated = new Date();
@@ -21,8 +21,8 @@
imageSource = reader?.result || ''; imageSource = reader?.result || '';
if (imageSource === '') { if (imageSource === '') {
console.log('no image data, returning'); console.log("no image data, returning")
return; return
} }
// set imageSource to image element // set imageSource to image element
@@ -78,38 +78,27 @@
}); });
</script> </script>
<div class="liveimage"> <div>
{#if !fullscreen} {#if !fullscreen}
<img on:click={() => (fullscreen = !fullscreen)} src={String(imageSource)} id="live-image" /> <img on:click={() => (fullscreen = !fullscreen)} src={String(imageSource)} id="live-image" />
{:else} {:else}
<div class="fullscreen-container"> <Dialog title="Live stream of printer" on:close={() => (fullscreen = false)}>
<Dialog title="Live stream of printer" close={() => (fullscreen = false)}> <img style="width: 100%;" src={String(imageSource)} id="live-image" />
<img src={String(imageSource)} id="live-image" />
<span>Last update {timestamp}s ago</span> <span>Last update {timestamp}s ago</span>
</Dialog> </Dialog>
<img src={String(grey400x225)} /> <img src={String(grey400x225)} />
</div>
{/if} {/if}
<span>Last update {timestamp}s ago</span> <span>Last update {timestamp}s ago</span>
</div> </div>
<style lang="scss"> <style lang="scss">
.liveimage img { img {
width: 100%; width: 400px;
max-width: 400px;
border-radius: 0.5rem; border-radius: 0.5rem;
} }
span { span {
display: block; display: block;
} }
:global(.fullscreen-container .dialog img) {
max-width: unset;
}
:global(.fullscreen-container #dialog-title) {
max-width: 98vw;
}
</style> </style>

View File

@@ -69,5 +69,133 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../styles/card.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> </style>

View File

@@ -105,8 +105,6 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../styles/card.scss";
.card-container { .card-container {
background-color: #cab2aa40; background-color: #cab2aa40;
border-radius: 0.5rem; border-radius: 0.5rem;
@@ -124,4 +122,99 @@
gap: 2rem; 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;
&.not-running {
border: 2px dashed var(--theme);
opacity: 0.6;
}
}
.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> </style>

View File

@@ -4,17 +4,13 @@
</script> </script>
<article class="main-container"> <article class="main-container">
{#if title || description}
<div class="header"> <div class="header">
<div class="title"> <div class="title">
<h2>{title}</h2> <h2>{title}</h2>
<slot name="top-left" /> <slot name="top-left" />
</div> </div>
{#if description && description?.length > 0}
<label>{description}</label> <label>{description}</label>
{/if}
</div> </div>
{/if}
<slot></slot> <slot></slot>
</article> </article>

View File

@@ -24,7 +24,7 @@
link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:=jsconsole::::::` link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:=jsconsole::::::`
}, },
{ name: 'Graphs', link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:5::::::` }, { name: 'Graphs', link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:5::::::` },
{ name: 'Details', link: `/servers/node/${node.name}` } { name: 'Web', link: `https://${node.ip}:8006/` }
]; ];
let { cpuinfo, memory, uptime, loadavg } = node.info; let { cpuinfo, memory, uptime, loadavg } = node.info;
@@ -116,7 +116,7 @@
<div class="footer"> <div class="footer">
{#each buttons as btn (btn)} {#each buttons as btn (btn)}
<a href={btn.link} target={btn.link[0] === '/' ? '' : '_blank'} rel="noopener noreferrer"> <a href={btn.link} target="_blank" rel="noopener noreferrer">
<button> <button>
<span>{btn.name}</span> <span>{btn.name}</span>
</button> </button>
@@ -126,5 +126,163 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import '../styles/card.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;
max-width: 330px;
> div,
span {
display: flex;
padding: 0 0.5rem;
}
}
:global(.resource .title svg) {
height: 1rem;
width: 1rem;
}
.footer {
display: flex;
align-items: center;
justify-content: space-evenly;
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> </style>

View File

@@ -173,19 +173,6 @@
} }
} }
:global(body.dark .nav-wrapper.open) {
@media (prefers-color-scheme: dark) {
background-color: var(--bg);
nav a {
&:hover,
&.highlight {
color: white !important;
}
}
}
}
nav { nav {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,4 +1,10 @@
<script lang="ts"> <script lang="ts">
import Dialog from './Dialog.svelte';
import { goto } from '$app/navigation';
import Certificate from '$lib/icons/certificate.svelte';
import { daysUntil } from '$lib/utils/conversion';
import JsonViewer from './JsonViewer.svelte';
export let title = ''; export let title = '';
export let description = ''; export let description = '';
export let columns: Array<string> | object; export let columns: Array<string> | object;
@@ -6,6 +12,7 @@
export let links: Array<string> = []; export let links: Array<string> = [];
export let footer = ''; export let footer = '';
const hasLinks = links?.length > 0;
let displayColumns: string[] = []; let displayColumns: string[] = [];
if (typeof columns === 'object' && !Array.isArray(columns)) { if (typeof columns === 'object' && !Array.isArray(columns)) {
displayColumns = Object.values(columns); displayColumns = Object.values(columns);
@@ -14,17 +21,13 @@
</script> </script>
<div class="main-container"> <div class="main-container">
{#if title?.length || description?.length}
<div class="header"> <div class="header">
<h2>{title}</h2> <h2>{title}</h2>
<div class="description">{description}</div> <div class="description">{description}</div>
</div> </div>
{/if}
<div class="actions"> <div class="actions">
<slot name="actions"></slot> <slot name="actions"></slot>
</div> </div>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -55,7 +58,7 @@
.description { .description {
font-size: 0.875rem; font-size: 0.875rem;
opacity: 0.6; color: #666;
margin-bottom: 12px; margin-bottom: 12px;
} }

View File

@@ -1,7 +1,13 @@
<script lang="ts"> <script lang="ts">
import External from '$lib/icons/external.svelte'; import External from '$lib/icons/external.svelte';
import type { Site } from '$lib/interfaces/site.ts';
interface Site {
title: string;
image: string;
link: string;
background?: string;
color?: string;
}
let { title, image, background, color, link }: Site = $props(); let { title, image, background, color, link }: Site = $props();
@@ -54,7 +60,7 @@
h2, h2,
.link, .link,
.title { .title {
transition: all 0.18s ease-in-out; transition: all 0.2s ease-in-out;
} }
.title { .title {
@@ -84,8 +90,7 @@
.image { .image {
height: 8rem; height: 8rem;
width: 100%; width: 100%;
width: 8rem; margin: 1.2rem 0;
margin: 1.2rem auto;
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
@@ -100,7 +105,6 @@
left: calc(100% - 2rem); left: calc(100% - 2rem);
top: 0; top: 0;
opacity: 0; opacity: 0;
fill: var(--color);
} }
@media screen and (max-width: 750px) { @media screen and (max-width: 750px) {
@@ -126,6 +130,7 @@
opacity: 1; opacity: 1;
left: calc(100% - 1rem); left: calc(100% - 1rem);
top: 2px; top: 2px;
fill: var(--color);
} }
} }
} }

View File

@@ -1,95 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation'
import Desktop from '$lib/icons/desktop.svelte';
import HardDrive from '$lib/icons/hard-disk.svelte';
import Network from '$lib/icons/network.svelte';
import CPU from '$lib/icons/cpu.svelte';
import Fingerprint from '$lib/icons/fingerprint.svelte';
import ExtractUp from '$lib/icons/extract-up.svelte';
import InsertDown from '$lib/icons/insert-down.svelte';
import Clock from '$lib/icons/clock.svelte';
import Memory from '$lib/icons/floppy-disk.svelte';
import { formatBytes, formatDuration } from '$lib/utils/conversion';
import type { VM } from '$lib/interfaces/proxmox';
const { vm }: { vm: VM } = $props();
</script>
<div class="card">
<div class="header">
<div class="icon"><Desktop /></div>
<span class="name">{vm.name} <span class="subtle">{vm.vmid}</span></span>
<span class={`status ${vm.status === 'running' ? 'ok' : 'error'}`}></span>
</div>
<div class="resource">
<div class="title">
<Network />
<span>Status</span>
</div>
<span>{vm.status}</span>
<div class="title">
<Network />
<span>CPUs</span>
</div>
<span>{vm.cpus}</span>
<div class="title">
<CPU />
<span>CPU</span>
</div>
<span>{Math.floor(vm.cpu * 100) / 100}</span>
<div class="title">
<HardDrive />
<span>Max Disk</span>
</div>
<span>{formatBytes(vm.maxdisk)}</span>
<div class="title">
<Memory />
<span>Memory</span>
</div>
<span>{formatBytes(vm.mem)}</span>
<div class="title">
<InsertDown />
<span>Net In</span>
</div>
<span>{formatBytes(vm.netin)}</span>
<div class="title">
<ExtractUp />
<span>Net Out</span>
</div>
<span>{formatBytes(vm.netout / 8)}</span>
<div class="title">
<Clock />
<span>Uptime</span>
</div>
<span>{formatDuration(vm.uptime)}</span>
<div class="title">
<Fingerprint />
<span>VM ID</span>
</div>
<span>{vm.vmid}</span>
</div>
<div class="footer">
<button on:click={() => goto(`/servers/vm/${vm.name}`)}>
<span>VM details</span>
</button>
</div>
</div>
<style lang="scss">
@import '../styles/card.scss';
.card {
flex-grow: 1;
max-width: 550px;
}
</style>

View File

@@ -1,67 +0,0 @@
<script lang="ts">
import Input from '$lib/components/Input.svelte';
import TextSize from '$lib/icons/text-size.svelte';
import Quill from '$lib/icons/quill.svelte';
import Tag from '$lib/icons/tag.svelte';
import Dropdown from '../Dropdown.svelte';
const { close }: { close: () => void } = $props();
const dnsTypes = ['A', 'AAAA', 'CNAME', 'NS', 'SOA', 'MX', 'TXT', 'PTR'];
</script>
<form method="POST">
<div class="wrapper">
<Input label="Name" icon={TextSize} placeholder="Website name" required />
<Dropdown
placeholder="Record type"
label="Type"
required={true}
icon={Tag}
options={dnsTypes}
/>
<Input label="Content" icon={Quill} placeholder="IPv4 address" required />
</div>
<footer>
<button on:click={close} aria-disabled="false" type="button" tabindex="0"
><span tabindex="-1">Cancel</span></button
>
<button class="affirmative" type="submit" tabindex="-1">
<span tabindex="-1">Add record</span>
</button>
</footer>
</form>
<style lang="scss">
form {
.wrapper {
display: flex;
flex-direction: column;
}
footer {
padding: 0.75rem 1.5rem;
max-width: 100%;
height: 2.5rem;
width: auto;
display: flex;
justify-content: flex-end;
flex: 0 0 auto;
gap: 1rem;
flex-wrap: wrap;
button {
flex: unset;
span {
font-size: 0.8rem;
}
}
}
}
:global(form .wrapper div) {
margin-bottom: 0.5rem;
}
</style>

View File

@@ -1,71 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Input from '$lib/components/Input.svelte';
import ColorInput from '$lib/components/ColorInput.svelte';
import Id from '$lib/icons/id.svelte';
import TextColor from '$lib/icons/text-color.svelte';
import TextSize from '$lib/icons/text-size.svelte';
import Window from '$lib/icons/window.svelte';
import Quill from '$lib/icons/quill.svelte';
import Tag from '$lib/icons/tag.svelte';
import Link from '$lib/icons/link.svelte';
import PaintRoller from '$lib/icons/paint-roller.svelte';
import Bucket from '$lib/icons/bucket.svelte';
import Picture from '$lib/icons/picture.svelte';
const dispatch = createEventDispatcher();
const close = () => dispatch('close');
</script>
<form method="POST">
<div class="wrapper">
<Input label="Name" icon={TextSize} placeholder="Website name" required />
<Input label="Link" icon={Link} placeholder="https://site.tld" required />
<Input label="Image" icon={Picture} placeholder="/images/site.png" required />
<ColorInput label="Color" icon={PaintRoller} placeholder="#21ADF6" required />
<ColorInput label="Background" icon={Bucket} />
</div>
<footer>
<button on:click={close} aria-disabled="false" type="button" tabindex="0"
><span tabindex="-1">Cancel</span></button
>
<button class="affirmative" type="submit" tabindex="-1">
<span tabindex="-1">Add connection</span>
</button>
</footer>
</form>
<style lang="scss">
form {
.wrapper {
display: flex;
flex-direction: column;
}
footer {
padding: 0.75rem 1.5rem;
max-width: 100%;
height: 2.5rem;
width: auto;
display: flex;
justify-content: flex-end;
flex: 0 0 auto;
gap: 1rem;
flex-wrap: wrap;
button {
flex: unset;
span {
font-size: 0.8rem;
}
}
}
}
:global(form .wrapper div) {
margin-bottom: 0.5rem;
}
</style>

View File

@@ -15,22 +15,12 @@
</div> </div>
<style lang="scss"> <style lang="scss">
.tab {
&:not(&:first-of-type) {
margin-left: 0.75rem;
}
button { button {
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
background: none; background: none;
opacity: 0.6;
margin: 0;
padding-bottom: 0.3rem;
border-radius: 0;
border: none; border: none;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
border-radius: 0;
margin: 0;
letter-spacing: 0.2px; letter-spacing: 0.2px;
&.selected { &.selected {
@@ -40,5 +30,20 @@
font-weight: 600 !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> </style>

View File

@@ -1,282 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { formatBytes, formatDuration } from '$lib/utils/conversion';
import Cpu from '$lib/icons/cpu.svelte';
import Section from '../Section.svelte';
import type { Node, VM } from '$lib/interfaces/proxmox';
import FloppyDisk from '$lib/icons/floppy-disk.svelte';
import HardDisk from '$lib/icons/hard-disk.svelte';
import Clock from '$lib/icons/clock.svelte';
import InsertDown from '$lib/icons/insert-down.svelte';
import ExtractUp from '$lib/icons/extract-up.svelte';
import Table from '../Table.svelte';
import Power from '$lib/icons/power.svelte';
const { vm, node }: { vm: VM, node: Node } = $props();
console.log(node)
const drives = Object.entries(vm.config)
.filter(([key, _]) => {
return key.startsWith('scsi');
})
?.map(([key, value]) => {
const { disk, backup, discard, iothread, size } =
/(?<disk>[\w\:\-]+),(backup=(?<backup>[\w|\d]+))?[,]?(discard=(?<discard>[\w|\d]+))?[,]?(iothread=(?<iothread>[\w|\d]+))?[,]?(size=(?<size>[\w|\d]+)\w)?[,]?/.exec(
value
)?.groups || {};
if (!disk) return;
return {
device: key,
mount: disk,
backup: backup === '1' ? 'enabled' : 'disabled',
size: Number(size || 1) * 1024
};
})
.filter((d) => d)
.sort((a, b) =>
Number(a?.device.match(/\d+/)[0]) > Number(b?.device.match(/\d+/)[0]) ? 1 : -1
);
const pcieDevices = Object.entries(vm.config)
.filter(([key, _]) => {
return key.startsWith('hostpci');
})
?.map(([key, value]) => {
return {
device: key,
name: value
};
});
</script>
<div>
<Section title="Resources" description="">
<div class="section-row">
<div class="section-element">
<label>ID</label>
<span>{vm.vmid}</span>
</div>
<div class="section-element">
<label>Memory usage</label>
<span
><span class="icon"><FloppyDisk /></span>{formatBytes(vm.mem).match(/\d+(\.\d+)?/)?.[0]} /
{formatBytes(vm.maxmem)}</span
>
</div>
<div class="section-element">
<label>CPUs</label>
<span>
<span class="icon"><Cpu /></span>
{vm.cpus} cores ({vm.config.sockets})
</span>
</div>
<div class="section-element">
<label>Disk</label>
<span>
<span class="icon"><HardDisk /></span>
{formatBytes(vm.maxdisk)}
</span>
</div>
<div class="section-element">
<label>Uptime</label>
<span>
<span class="icon"><Clock /></span>
{formatDuration(vm.uptime)}
</span>
</div>
</div>
</Section>
<Section title="Status" description="">
<div class="section-row">
<div class="section-element">
<label>State</label>
<span>
<span class="icon"><Power /></span>
{vm.status}
</span>
</div>
<div class="section-element">
<label>CPU load</label>
<span>{Math.floor(vm.cpu * 10000) / 100} %</span>
</div>
<div class="section-element">
<label>Memory usage</label>
<span>{Math.floor((vm.mem / vm.maxmem) * 1000) / 10} %</span>
</div>
<div class="section-element">
<label>Netout</label>
<span>
<span class="icon"><ExtractUp /></span>
{formatBytes(vm.netout)}
</span>
</div>
<div class="section-element">
<label>Netin</label>
<span>
<span class="icon"><InsertDown /></span>
{formatBytes(vm.netin)}
</span>
</div>
<div class="section-element">
<label>Disk read</label>
<span>
<span class="icon"><InsertDown /></span>
{vm.diskread}
</span>
</div>
<div class="section-element">
<label>Disk write</label>
<span>
<span class="icon"><InsertDown /></span>
{vm.diskwrite}
</span>
</div>
</div>
</Section>
<Section title="Operating System" description="">
<div class="section-row">
<div class="section-element">
<label>Name</label>
<span>{vm.os?.name}</span>
</div>
<div class="section-element">
<label>Name</label>
<span>{vm.os?.['pretty-name']}</span>
</div>
<div class="section-element">
<label>Version</label>
<span>{vm.os?.version}</span>
</div>
<div class="section-element">
<label>Kernel</label>
<span>{vm.os?.['kernel-release']}</span>
</div>
</div>
</Section>
<Section title="Filesystem" description="">
{#if drives?.length > 0}
<Table title="FS Devices" columns={['name', 'bus', 'mountpoint', 'type', 'space', 'size']}>
<tbody slot="tbody">
{#each vm.fs as device (device)}
<tr>
<td>{device.name}</td>
<td>
{#each device['disk'] as device (device)}
<span>{device?.['bus-type']}{device?.target}</span>
{/each}
</td>
<td>{device.mountpoint}</td>
<td>{device.type}</td>
<td>{Math.floor((device['used-bytes'] / device['total-bytes']) * 10000) / 100} %</td>
<td>{formatBytes(device['total-bytes'])}</td>
</tr>
{/each}
</tbody>
</Table>
{/if}
</Section>
<Section title="Devices" description="">
{#if vm.network?.length > 0}
<Table title="Network interfaces" columns={['name', 'hardward address', 'ip addresses']}>
<tbody slot="tbody">
{#each vm.network.sort((a,b) => a < b ? 1 : -1) as net_interface (net_interface.name)}
<tr>
<td>{net_interface.name}</td>
<td>{net_interface['hardware-address']}</td>
<td class="ip-addresses">
{#each net_interface['ip-addresses'] as ip (ip['ip-address'])}
<span>{ip['ip-address']}/{ip.prefix}</span>
{/each}
</td>
</tr>
{/each}
</tbody>
</Table>
{/if}
{#if drives?.length > 0}
<Table title="Hard drives" columns={['device', 'mount', 'backup', 'size']}>
<div slot="actions">
<p>Total drives: {drives.length}</p>
<p>Total capacity: {formatBytes(drives.reduce((acc, obj) => acc + obj.size, 0))}</p>
</div>
<tbody slot="tbody">
{#each drives as drive (drive)}
<tr>
<td>{drive.device}</td>
<td>{drive.mount}</td>
<td>{drive.backup}</td>
<td>{formatBytes(drive.size)}</td>
</tr>
{/each}
</tbody>
</Table>
{/if}
{#if pcieDevices?.length > 0}
<Table title="PCIe devices" columns={Object.keys(pcieDevices[0])}>
<tbody slot="tbody">
{#each pcieDevices as pcie (pcie)}
<tr>
<td>{pcie.device}</td>
<td>{pcie.name}</td>
</tr>
{/each}
</tbody>
</Table>
{/if}
</Section>
</div>
<style lang="scss">
:global(article.main-container:not(:first-child)) {
margin-top: 1rem;
}
.section-element {
.icon {
display: inline-block;
--size: 1.3rem;
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);
}
}
}
tbody .ip-addresses {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -1,5 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M671.9 418c0.6-9.2-2.8-18.1-9.3-24.6l-143.1-143.2c17.1-27.9 30.8-55.3 40.5-81.4 23.9-64.7 20.9-116.6-8.5-146.3-14.9-14.9-33.2-22.5-54.4-22.5v0c-31.1 0-68.8 16.6-115.2 50.8-10.9 8.1-22.4 17.2-34.5 27.3l-4.8-4.8c-6.5-6.5-15.5-9.9-24.6-9.3-9.2 0.6-17.6 5.1-23.3 12.3l-280.7 361.8c-19.7 25.4-17.4 61.7 5.3 84.5l194.1 194.1c12.4 12.4 28.8 18.7 45.3 18.7 13.8 0 27.6-4.4 39.2-13.4l361.7-280.7c7.3-5.7 11.8-14.1 12.3-23.3zM497.1 32v0c12.7 0 22.8 4.2 31.7 13.1 19.8 20 20.3 61 1.2 112.6-8.2 22.2-19.6 45.5-33.7 69.2l-126.3-126.1c47.6-39.6 93.5-68.8 127.1-68.8zM258.7 671.4l-194.1-194.1 30.6-39.5 202.9 202.9-39.4 30.7zM323.6 621l-208.6-208.6 208-268.1 136.8 136.7c-11.3 15.1-23.6 30.1-36.6 44.9-6.9-3.8-14.8-6-23.1-6-26.5 0-48 21.5-48 48s21.5 48 48 48c26.5 0 48-21.5 48-48 0-6.3-1.2-12.4-3.5-17.9 13.5-15.2 26.3-30.7 38.1-46.2l109 109.2-268.1 208zM416 368c0 8.8-7.2 16-16 16s-16-7.2-16-16 7.2-16 16-16 16 7.2 16 16z"></path>
<path d="M730.3 584.5c-12.3-21.9-23.9-42.6-26.4-58.9-1.2-7.8-7.9-13.6-15.8-13.6s-14.6 5.8-15.8 13.6c-2.5 16.4-14.1 37-26.4 58.9-17.9 31.5-37.9 67.2-37.9 103.4 0 44.1 35.9 80 80 80s80-35.9 80-80c0-36.2-20-71.9-37.7-103.4zM688 735.9c-26.5 0-48-21.5-48-48 0-27.8 17.1-58.3 33.6-87.7 5-9 10-17.7 14.4-26.3 4.4 8.6 9.3 17.4 14.4 26.3 16.5 29.4 33.6 59.9 33.6 87.7 0 26.5-21.5 48-48 48z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,5 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M480 160c-35.3 0-64 28.7-64 64s28.7 64 64 64 64-28.7 64-64-28.7-64-64-64zM480 256c-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="M764.8 390.4l-103.6-138.1-19.1-64.8c-0.5-1.7-1.2-3.2-2.1-4.5v-55c0-35.3-28.7-64-64-64h-512c-35.3 0-64 28.7-64 64v192c0 35.3 28.7 64 64 64h11.5l53.1 180.5c2.1 7 8.4 11.5 15.3 11.5 1.5 0 3-0.2 4.5-0.7l94.2-27.7 112.5 150c3.1 4.2 7.9 6.4 12.8 6.4 3.3 0 6.7-1 9.6-3.2l384-288c7.2-5.3 8.6-15.3 3.3-22.4zM64 128c0 0 0 0 0 0h512v192h-512v-192zM108.9 384h467.1c35.3 0 64-28.7 64-64v-26.5l28.1 95.6-513.3 151-45.9-156.1zM371.2 665.6l-95.7-127.6 417-122.7c4.1-1.2 7.5-4 9.5-7.7s2.5-8.1 1.3-12.2l-16.3-55.3 42.6 56.7-358.4 268.8z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 869 B

View File

@@ -2,8 +2,8 @@
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="100%" width="768"
height="100%" height="768"
viewBox="0 0 768 768" viewBox="0 0 768 768"
> >
<g id="icomoon-ignore"> </g> <g id="icomoon-ignore"> </g>

Before

Width:  |  Height:  |  Size: 818 B

After

Width:  |  Height:  |  Size: 816 B

View File

@@ -1,10 +1,12 @@
<!-- Generated by IcoMoon.io -->
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="100%" width="768"
height="100%" height="768"
viewBox="0 0 768 768" viewBox="0 0 768 768"
> >
<g id="icomoon-ignore"> </g>
<path <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" 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>

Before

Width:  |  Height:  |  Size: 805 B

After

Width:  |  Height:  |  Size: 866 B

View File

@@ -1,4 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M704 64h-640c-35.3 0-64 28.7-64 64v352c0 35.3 28.7 64 64 64h288v64c0 15.1-12.4 30.6-34 42.6-24.9 13.8-58.3 21.4-94 21.4v32h320v-32c-35.8 0-69.2-7.6-94-21.4-21.6-12-34-27.5-34-42.6v-64h288c35.3 0 64-28.7 64-64v-352c0-35.3-28.7-64-64-64zM423.8 672h-79.6c24.9-16.9 39.8-39.2 39.8-64 0 24.8 14.9 47.1 39.8 64zM64 128h640v288h-640v-288c-0.1 0 0 0 0 0zM64 480v-32h640v32h-640z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 556 B

View File

@@ -1,5 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M656 352h-208v32h192v320h-512v-320h192v-32h-208c-8.8 0-16 7.2-16 16v352c0 8.8 7.2 16 16 16h544c8.8 0 16-7.2 16-16v-352c0-8.8-7.2-16-16-16z"></path>
<path d="M352 141.3v402.7h64v-402.7l81.4 81.4 45.3-45.3-136-136c-12.5-12.5-32.8-12.5-45.3 0l-136 136 45.3 45.3 81.3-81.4z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 455 B

View File

@@ -8,6 +8,7 @@
> >
<g <g
transform="translate(0.000000,108.000000) scale(0.100000,-0.100000)" transform="translate(0.000000,108.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none" stroke="none"
> >
<path <path

Before

Width:  |  Height:  |  Size: 879 B

After

Width:  |  Height:  |  Size: 896 B

View File

@@ -1,10 +1,12 @@
<!-- Generated by IcoMoon.io -->
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="100%" width="768"
height="100%" height="768"
viewBox="0 0 768 768" viewBox="0 0 768 768"
> >
<g id="icomoon-ignore"> </g>
<path <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" 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>

Before

Width:  |  Height:  |  Size: 699 B

After

Width:  |  Height:  |  Size: 760 B

View File

@@ -1,10 +1,12 @@
<!-- Generated by IcoMoon.io -->
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="100%" width="768"
height="100%" height="768"
viewBox="0 0 768 768" viewBox="0 0 768 768"
> >
<g id="icomoon-ignore"> </g>
<path <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" 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>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,9 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" height="100%" width="100%">
<path d="M704 128h-640c-35.3 0-64 28.7-64 64v384c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64v-384c0-35.3-28.7-64-64-64zM704 576h-640v-384h640v384c0.1 0 0 0 0 0z"></path>
<path d="M416 256h256v32h-256v-32z"></path>
<path d="M416 320h192v32h-192v-32z"></path>
<path d="M416 384h256v32h-256v-32z"></path>
<path d="M416 448h256v32h-256v-32z"></path>
<path d="M298.3 402.5c-4.7-2.8-9.6-5.2-14.6-7.4 22.1-17.6 36.3-44.7 36.3-75.1 0-52.9-43.1-96-96-96s-96 43.1-96 96c0 30.4 14.2 57.5 36.3 75.1-5 2.1-9.9 4.6-14.6 7.4-20.2 11.9-37 29.3-51.4 53.3-4.5 7.4-2.2 17.1 5.1 21.8 36.6 23.1 76.1 34.4 120.6 34.4s84-11.3 120.6-34.5c7.3-4.7 9.6-14.3 5.1-21.8-14.4-23.9-31.2-41.3-51.4-53.2zM160 320c0-35.3 28.7-64 64-64s64 28.7 64 64-28.7 64-64 64-64-28.7-64-64zM224 480c-32.6 0-61.9-7-89.2-21.3 22.1-29.4 50.2-42.7 89.2-42.7s67.1 13.3 89.2 42.7c-27.3 14.3-56.6 21.3-89.2 21.3z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,5 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M656 320h-208v32h192v320h-512v-320h192v-32h-208c-8.8 0-16 7.2-16 16v352c0 8.8 7.2 16 16 16h544c8.8 0 16-7.2 16-16v-352c0-8.8-7.2-16-16-16z"></path>
<path d="M270.6 417.4l-45.3 45.3 136 136c6.2 6.2 14.4 9.4 22.6 9.4s16.4-3.1 22.6-9.4l136-136-45.3-45.3-81.2 81.3v-434.7h-64v434.7l-81.4-81.3z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 475 B

View File

@@ -1,5 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M656 64h-544c-8.8 0-16 7.2-16 16v352c0 8.8 7.2 16 16 16h208v-32h-192v-320h512v320h-192v32h208c8.8 0 16-7.2 16-16v-352c0-8.8-7.2-16-16-16z"></path>
<path d="M497.4 350.6l45.3-45.3-136-136c-12.5-12.5-32.8-12.5-45.3 0l-136 136 45.3 45.3 81.4-81.4v434.8h64v-434.7l81.3 81.3z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 456 B

View File

@@ -1,5 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M672 96h-32v-32c0-35.3-28.7-64-64-64h-416c-35.3 0-64 28.7-64 64v96c0 35.3 28.7 64 64 64h416c35.3 0 64-28.7 64-64v-32h32c17.6 0 32 14.4 32 32v96c0 17.6-14.4 32-32 32h-256c-35.3 0-64 28.7-64 64v1.6c-36.5 7.4-64 39.8-64 78.4v256c0 44.1 35.9 80 80 80s80-35.9 80-80v-256c0-38.6-27.5-71-64-78.4v-1.6c0-17.6 14.4-32 32-32h256c35.3 0 64-28.7 64-64v-96c0-35.3-28.7-64-64-64zM128 160v-96c0-17.6 14.4-32 32-32h416c17.6 0 32 14.4 32 32v32c0 17.6-14.4 32-32 32-14.2 0-21-7.9-32.8-23.1-13.4-17.3-31.7-40.9-71.2-40.9-33.3 0-49.3 20.6-62.2 37.2-13.3 17.1-21.8 26.8-41.8 26.8-26.8 0-45.1-9-62.8-17.6-21.2-10.4-43.1-21.1-71.4-9.1-18.1 7.6-29.4 16.4-35.7 27.5-6.1 10.7-6.1 21.4-6.1 30v1.2c0 17.6-14.4 32-32 32s-32-14.4-32-32zM224 160v-1.3c0-6.9 0.1-11 1.9-14.2 2.5-4.5 9.6-9.2 20.3-13.8 14.3-6 24.7-1.6 44.9 8.4 19 9.3 42.6 20.9 76.9 20.9 36.7 0 53.5-21.7 67.1-39.2 12.7-16.3 20.1-24.8 36.9-24.8 23.8 0 33.6 12.6 45.9 28.5 12.3 15.8 27.6 35.5 58.1 35.5h-352zM384 688c0 8.8-7.2 16-16 16s-16-7.2-16-16v-256c0-8.8 7.2-16 16-16s16 7.2 16 16v256z"></path>
<path d="M159.9 301.8c-1.1-7.9-7.9-13.8-15.9-13.8s-14.8 5.9-15.9 13.8c-1.2 8.4-6.7 19.8-12.7 31.9-9.1 18.6-19.5 39.7-19.5 61.7 0 29 21.5 52.6 48 52.6s48-23.6 48-52.6c0-22-10.3-43.1-19.5-61.7-5.8-12.1-11.4-23.4-12.5-31.9zM144 416c-8.7 0-16-9.4-16-20.6 0-14.4 8.1-31.1 16-47.2 7.9 16.1 16 32.7 16 47.2 0 11.2-7.3 20.6-16 20.6z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,9 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M716.8 352.6c-25.9-18.5-57.8-29.9-85.9-40-15.3-5.5-29.8-10.7-40.8-16.1-9.8-4.8-13.1-8.1-14.1-9.3 0-1.9 0.3-2.9 0.4-3.4 2.7-2.2 12.2-5.2 18-7 27.1-8.6 77.6-24.6 77.6-100.8 0-45.8-29.3-85.2-82.5-111.1-43.8-21.3-103.3-33-167.4-33-102.3 0-203.8 29.1-278.4 79.8-94 63.9-143.7 158-143.7 272.3 0 51.2 9.4 99.5 28 143.5 18.1 42.8 44.4 80.4 78.1 111.9 33.2 31 72.7 55.1 117.2 71.6 44.8 16.6 93.4 25 144.6 25 50.4 0 99.9-6.7 147.1-20 47.2-13.2 89.7-32.3 126.5-56.8 81.6-54 126.5-129 126.5-211.2 0-39-17.2-71-51.2-95.4zM606.3 605.9c-63.2 42-150.1 66.1-238.3 66.1-179 0-304-118.4-304-288 0-93.4 38.9-167.2 115.6-219.4 64.3-43.7 152.7-68.8 242.5-68.8 104.2 0.1 185.9 35.3 185.9 80.2 0 29.4-8.3 32-32.9 39.8-12.4 3.9-26.5 8.4-38.6 18-11.2 8.9-24.5 25.5-24.5 54.1 0 24.2 12.9 44.3 38.3 59.7 16.7 10.1 37.3 17.4 59 25.2 57.2 20.4 94.7 36.7 94.7 75.1 0 60.1-34.7 116.2-97.7 158z"></path>
<path d="M224 400c0-26.5-21.5-48-48-48s-48 21.5-48 48c0 26.5 21.5 48 48 48s48-21.5 48-48zM176 416c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16z"></path>
<path d="M240 480c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48zM240 544c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16z"></path>
<path d="M240 224c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48zM240 288c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16z"></path>
<path d="M368 160c-26.5 0-48 21.5-48 48s21.5 48 48 48c26.5 0 48-21.5 48-48s-21.5-48-48-48zM368 224c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16z"></path>
<path d="M542.1 417c-23.2-3.2-48.3 1.4-70.7 12.9-27.1 13.9-46.4 36.1-53 60.7-4.9 18.2-2.3 36.5 7.3 51.5 11.4 17.9 31.4 29.6 56.1 32.9 4.8 0.7 9.7 1 14.6 1 18.9 0 38.3-4.8 56.1-13.9 27.1-13.9 46.4-36.1 53-60.7 4.9-18.2 2.3-36.5-7.3-51.5-11.4-17.9-31.4-29.6-56.1-32.9zM574.7 493.1c-4.2 15.7-18 30.9-36.8 40.6-32.4 16.7-71.4 12.6-85.2-8.8-6-9.4-5.2-19.3-3.4-25.9 4.2-15.7 18-30.9 36.8-40.6 13.5-6.9 28.1-10.3 41.6-10.3 18.9 0 35.6 6.6 43.6 19.1 6 9.4 5.2 19.3 3.4 25.9z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,18 +0,0 @@
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 768 768"
>
<path
d="M704 0h-640c-35.3 0-64 28.7-64 64v640c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64v-640c0-35.3-28.7-64-64-64zM704 704h-640l-0.1-640c0 0 0 0 0.1 0h640v640z"
></path>
<path
d="M112 544h544c8.8 0 16-7.2 16-16v-416c0-8.8-7.2-16-16-16h-544c-8.8 0-16 7.2-16 16v416c0 8.8 7.2 16 16 16zM338.7 512l214.7-214.7c12.4-12.4 32.8-12.4 45.3 0l41.3 41.3v173.4h-301.3zM640 128v165.4l-18.7-18.7c-25-24.9-65.6-24.9-90.5 0l-237.3 237.3h-146.8l150.7-150.7c12.4-12.4 32.8-12.4 45.3 0l29.3 29.3 22.6-22.6-29.3-29.3c-25-24.9-65.6-24.9-90.5 0l-146.8 146.8v-357.5h512z"
></path>
<path
d="M224 288c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zM224 192c17.6 0 32 14.4 32 32s-14.4 32-32 32-32-14.4-32-32 14.4-32 32-32z"
></path>
<path d="M256 608h256v32h-256v-32z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 903 B

View File

@@ -2,8 +2,8 @@
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="100%" width="768"
height="100%" height="768"
viewBox="0 0 768 768" viewBox="0 0 768 768"
> >
<g id="icomoon-ignore"> </g> <g id="icomoon-ignore"> </g>

Before

Width:  |  Height:  |  Size: 649 B

After

Width:  |  Height:  |  Size: 647 B

View File

@@ -8,6 +8,7 @@
> >
<g <g
transform="translate(0.000000,151.000000) scale(0.100000,-0.100000)" transform="translate(0.000000,151.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none" stroke="none"
> >
<path <path

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -8,6 +8,7 @@
> >
<g <g
transform="translate(0.000000,157.000000) scale(0.100000,-0.100000)" transform="translate(0.000000,157.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none" stroke="none"
> >
<path <path

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -8,6 +8,7 @@
> >
<g <g
transform="translate(0.000000,151.000000) scale(0.100000,-0.100000)" transform="translate(0.000000,151.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none" stroke="none"
> >
<path <path

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -8,6 +8,7 @@
> >
<g <g
transform="translate(0.000000,156.000000) scale(0.100000,-0.100000)" transform="translate(0.000000,156.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none" stroke="none"
> >
<path <path

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,4 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="100%" height="100%">
<path d="M22.866 1.503c-0.178-0.312-0.506-0.503-0.866-0.503-2.319 0-4.803 0.503-7.178 1.45-2.463 0.984-4.803 2.45-6.769 4.237-2.163 1.969-3.866 4.294-5.066 6.906-1.319 2.881-1.987 6.047-1.987 9.406h2c0-2.288 0.328-4.319 0.869-6.113 1.131-1.9 2.109-1.906 3.856-1.919 1.644-0.012 3.687-0.028 6.244-1.688 2.822-1.831 5.731-5.359 8.894-10.778 0.181-0.309 0.184-0.691 0.003-1zM18.225 6.041c-1.266 0.112-3.844 0.525-6.012 2.050l-0.409 0.288 0.575 0.819 0.409-0.288c1.534-1.078 3.341-1.547 4.609-1.753-1.572 2.037-3.069 3.506-4.516 4.447-2.066 1.341-3.644 1.353-5.169 1.366-0.756 0.006-1.522 0.012-2.275 0.2 1.181-2.137 2.631-3.784 3.966-5 3.056-2.784 6.972-4.603 10.766-5.056-0.663 1.066-1.309 2.044-1.944 2.928z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 881 B

View File

@@ -8,6 +8,7 @@
> >
<g <g
transform="translate(0.000000,122.000000) scale(0.100000,-0.100000)" transform="translate(0.000000,122.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none" stroke="none"
> >
<path <path

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 746 B

View File

@@ -1,6 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="100%" height="100%">
<path d="M24 5c0-2.756-2.244-5-5-5-2.416 0-4.434 1.722-4.9 4h-2.091c-0.534 0-1.034 0.209-1.413 0.588l-10 10c-0.781 0.781-0.781 2.050 0 2.831l5.991 5.988c0.391 0.391 0.903 0.584 1.416 0.584s1.025-0.194 1.416-0.584l10-10c0.378-0.378 0.588-0.881 0.588-1.413v-2.094c2.275-0.466 3.994-2.487 3.994-4.9zM16 9v0c0 0.55-0.45 1-1 1s-1-0.45-1-1 0.45-1 1-1c0 0 0 0 0 0 0.281 0.375 0.616 0.712 1 1zM18.003 11.994l-10 10-5.994-5.994 10-10h2.091c0.075 0.372 0.194 0.734 0.35 1.075-0.837 0.237-1.453 1.009-1.453 1.925 0 1.103 0.897 2 2 2s2-0.897 2-2c0-0.897-0.594-1.659-1.413-1.913-0.206-0.337-0.363-0.703-0.462-1.088h2.878c0.003 0 0.003 0.003 0.003 0.006v5.988zM20.003 8.872v-2.878c-0.006-1.1-0.906-1.994-2.003-1.994h-2.875c0.444-1.722 2.013-3 3.875-3 2.206 0 4 1.794 4 4 0 1.859-1.275 3.425-2.997 3.872z"></path>
<path d="M4.793 16.5l4.707-4.707 0.707 0.707-4.707 4.707-0.707-0.707z"></path>
<path d="M6.794 18.499l5.708-5.708 0.707 0.707-5.708 5.708-0.707-0.707z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -8,6 +8,7 @@
> >
<g <g
transform="translate(0.000000,182.000000) scale(0.100000,-0.100000)" transform="translate(0.000000,182.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none" stroke="none"
> >
<path <path

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -8,6 +8,7 @@
> >
<g <g
transform="translate(0.000000,181.000000) scale(0.100000,-0.100000)" transform="translate(0.000000,181.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none" stroke="none"
> >
<path <path

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,5 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M64 736h640v32h-640v-32z"></path>
<path d="M245.5 416h276.9l96.4 213.2 58.3-26.4-264-584c-5.1-11.4-16.5-18.8-29.1-18.8s-24 7.4-29.2 18.8l-264 584 58.3 26.4 96.4-213.2zM384 109.7l109.5 242.3h-219l109.5-242.3z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 393 B

View File

@@ -1,5 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M768 64h-576v64h256v576h64v-576h256z"></path>
<path d="M0 416h128v288h64v-288h128v-64h-320z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 277 B

View File

@@ -1,4 +1,5 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 768 768"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 768 768">
</g>
<path d="M721.4 97.4l-368 368c-21.9 21.9-42.8 16.6-55.1 4.3s-17.6-33.2 4.3-55.1l368-368-45.2-45.2-368 368c-28.3 28.3-33.9 57.7-33.6 77.4 0.2 14.7 3.8 29 10.3 42l-153.5 153.4c-5.3-1.4-10.9-2.2-16.6-2.2-17.1 0-33.2 6.7-45.3 18.7-25 25-25 65.6 0 90.5 12.1 12.1 28.2 18.8 45.3 18.8s33.2-6.7 45.3-18.7c16.7-16.7 22.2-40.4 16.6-61.8l153.4-153.5c13 6.5 27.3 10.1 42 10.3 0.5 0 1 0 1.5 0 19.6 0 48.3-6 75.9-33.6l368-368-45.3-45.3zM86.6 726.6c-6 6-14.1 9.4-22.6 9.4s-16.6-3.3-22.6-9.4c-12.5-12.5-12.5-32.8 0-45.3 0 0 0 0 0 0 6-6 14.1-9.4 22.6-9.4s16.6 3.3 22.6 9.4c12.5 12.5 12.5 32.8 0 45.3z"></path> <path d="M721.4 97.4l-368 368c-21.9 21.9-42.8 16.6-55.1 4.3s-17.6-33.2 4.3-55.1l368-368-45.2-45.2-368 368c-28.3 28.3-33.9 57.7-33.6 77.4 0.2 14.7 3.8 29 10.3 42l-153.5 153.4c-5.3-1.4-10.9-2.2-16.6-2.2-17.1 0-33.2 6.7-45.3 18.7-25 25-25 65.6 0 90.5 12.1 12.1 28.2 18.8 45.3 18.8s33.2-6.7 45.3-18.7c16.7-16.7 22.2-40.4 16.6-61.8l153.4-153.5c13 6.5 27.3 10.1 42 10.3 0.5 0 1 0 1.5 0 19.6 0 48.3-6 75.9-33.6l368-368-45.3-45.3zM86.6 726.6c-6 6-14.1 9.4-22.6 9.4s-16.6-3.3-22.6-9.4c-12.5-12.5-12.5-32.8 0-45.3 0 0 0 0 0 0 6-6 14.1-9.4 22.6-9.4s16.6 3.3 22.6 9.4c12.5 12.5 12.5 32.8 0 45.3z"></path>
<path d="M448 544v32c68.4 0 132.7-26.6 181-75s75-112.6 75-181h-32c0 123.5-100.5 224-224 224z"></path> <path d="M448 544v32c68.4 0 132.7-26.6 181-75s75-112.6 75-181h-32c0 123.5-100.5 224-224 224z"></path>
<path d="M224 320c0-123.5 100.5-224 224-224v-32c-68.4 0-132.7 26.6-181 75s-75 112.6-75 181h32z"></path> <path d="M224 320c0-123.5 100.5-224 224-224v-32c-68.4 0-132.7 26.6-181 75s-75 112.6-75 181h32z"></path>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,4 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M704 64h-640c-35.3 0-64 28.7-64 64v352c0 35.3 28.7 64 64 64h288v64c0 15.1-12.4 30.6-34 42.6-24.9 13.8-58.3 21.4-94 21.4v32h320v-32c-35.8 0-69.2-7.6-94-21.4-21.6-12-34-27.5-34-42.6v-64h288c35.3 0 64-28.7 64-64v-352c0-35.3-28.7-64-64-64zM423.8 672h-79.6c24.9-16.9 39.8-39.2 39.8-64 0 24.8 14.9 47.1 39.8 64zM64 128h640v288h-640v-288c-0.1 0 0 0 0 0zM64 480v-32h640v32h-640z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 556 B

View File

@@ -1,7 +0,0 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="100%" height="100%">
<path d="M22 1h-20c-1.103 0-2 0.897-2 2v18c0 1.103 0.897 2 2 2h20c1.103 0 2-0.897 2-2v-18c0-1.103-0.897-2-2-2zM22 3v4h-20v-4h20zM22 8v11h-20v-11h20zM22 21h-20v-1h20.003l-0.003 1c0.003 0 0 0 0 0z"></path>
<path d="M5 5c0 0.552-0.448 1-1 1s-1-0.448-1-1c0-0.552 0.448-1 1-1s1 0.448 1 1z"></path>
<path d="M8 5c0 0.552-0.448 1-1 1s-1-0.448-1-1c0-0.552 0.448-1 1-1s1 0.448 1 1z"></path>
<path d="M11 5c0 0.552-0.448 1-1 1s-1-0.448-1-1c0-0.552 0.448-1 1-1s1 0.448 1 1z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 637 B

View File

@@ -1,8 +0,0 @@
export interface Record {
name: string;
ttl: number;
class: string;
type: string;
data: string;
available?: boolean;
}

View File

@@ -66,45 +66,6 @@ interface NodeStatus {
bootInfo: BootInfo; bootInfo: BootInfo;
} }
export interface VM {
cpu: number;
cpus: number;
disk: number;
diskread: number;
diskwrite: number;
maxdisk: number;
maxmem: number;
mem: number;
name: string;
netin: number;
netout: number;
pid: number;
status: string;
uptime: number;
vmid: number;
}
export interface LXC {
cpu: number;
cpus: number;
disk: number;
diskread: number;
diskwrite: number;
maxdisk: number;
maxmem: number;
maxswap: number;
mem: number;
name: string;
netin: number;
netout: number;
pid: number;
status: string;
swap: number;
type: string;
uptime: number;
vmid: number;
}
export interface Node { export interface Node {
info: NodeStatus; info: NodeStatus;
online: number; online: number;
@@ -115,6 +76,4 @@ export interface Node {
type: string; type: string;
ip: string; ip: string;
level: string; level: string;
vms: Array<VM>;
lxcs: Array<LXC>;
} }

View File

@@ -1,9 +0,0 @@
export interface Site {
name: string;
link: string;
image: string;
color: string;
background: string;
created?: number;
updated?: number;
}

View File

@@ -1,5 +1,91 @@
import { currentFilament } from './filament';
import pg from 'pg';
import { env } from '$env/dynamic/private';
import type { Filament } from '$lib/interfaces/printer'; import type { Filament } from '$lib/interfaces/printer';
import { getDb } from '../database';
const { Pool } = pg;
let pool: InstanceType<typeof Pool> | undefined;
async function initDb() {
if (pool) return pool;
pool = new Pool({
connectionString: env.DATABASE_URL // e.g. postgres://user:pass@localhost:5432/mydb
});
const client = await pool.connect();
try {
await client.query('BEGIN');
for (const stmt of schemas) {
await client.query(stmt);
}
await client.query('COMMIT');
} catch (err: any) {
console.error('Failed to create tables:', err.message);
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
return pool;
}
const schemas = [
`
CREATE TABLE IF NOT EXISTS filament (
id SERIAL PRIMARY KEY,
hex TEXT NOT NULL,
color TEXT NOT NULL,
material TEXT,
weight REAL,
link TEXT,
added INTEGER, -- epoch seconds
updated INTEGER, -- epoch seconds
UNIQUE (hex, updated)
)
`
];
async function seedData(pool: InstanceType<typeof Pool>) {
const baseTimestamp = Math.floor(new Date('2025-04-01T05:47:01+00:00').getTime() / 1000);
const filaments = currentFilament();
const client = await pool.connect();
try {
await client.query('BEGIN');
for (const f of filaments) {
await client.query(
`INSERT INTO filament (hex, color, material, weight, link, added, updated)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (hex, updated) DO NOTHING`,
[f.hex, f.color, f.material, f.weight, f.link, baseTimestamp, baseTimestamp]
);
}
await client.query('COMMIT');
} catch (err: any) {
console.error('Failed to seed data:', err.message);
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
// Export helper to use db elsewhere
async function getDb() {
if (pool) return pool;
const p = await initDb();
await seedData(p);
console.log('Database setup and seeding complete!');
return p;
}
export async function getAllFilament(): Promise<Array<Filament>> { export async function getAllFilament(): Promise<Array<Filament>> {
const pool = await getDb(); const pool = await getDb();

View File

@@ -1,100 +0,0 @@
import pg from 'pg';
import { env } from '$env/dynamic/private';
import type { Filament } from '$lib/interfaces/printer';
const { Pool } = pg;
let pool: InstanceType<typeof Pool> | undefined;
async function initDb() {
if (pool) return pool;
pool = new Pool({
connectionString: env.DATABASE_URL // e.g. postgres://user:pass@localhost:5432/mydb
});
const client = await pool.connect();
try {
await client.query('BEGIN');
for (const stmt of schemas) {
await client.query(stmt);
}
await client.query('COMMIT');
} catch (err: any) {
console.error('Failed to create tables:', err.message);
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
return pool;
}
const schemas = [
`
CREATE TABLE IF NOT EXISTS filament (
id SERIAL PRIMARY KEY,
hex TEXT NOT NULL,
color TEXT NOT NULL,
material TEXT,
weight REAL,
link TEXT,
added INTEGER, -- epoch seconds
updated INTEGER, -- epoch seconds
UNIQUE (hex, updated)
)
`,
`
CREATE TABLE IF NOT EXISTS site (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
color TEXT NOT NULL,
image TEXT,
link TEXT,
background TEXT,
created INTEGER, -- epoch seconds
updated INTEGER, -- epoch seconds
UNIQUE (name, updated)
)
`
];
async function seedFilament(pool: InstanceType<typeof Pool>) {
const baseTimestamp = Math.floor(new Date('2025-04-01T05:47:01+00:00').getTime() / 1000);
const filaments: Filament[] = []; // disables seed
const client = await pool.connect();
try {
await client.query('BEGIN');
for (const f of filaments) {
await client.query(
`INSERT INTO filament (hex, color, material, weight, link, added, updated)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (hex, updated) DO NOTHING`,
[f.hex, f.color, f.material, f.weight, f.link, baseTimestamp, baseTimestamp]
);
}
await client.query('COMMIT');
} catch (err: any) {
console.error('Failed to seed data:', err.message);
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
// Export helper to use db elsewhere
export async function getDb() {
if (pool) return pool;
const p = await initDb();
await seedFilament(p);
console.log('Database setup and seeding complete!');
return p;
}

View File

@@ -1,21 +0,0 @@
import { getDb } from '../database';
import type { Site } from '$lib/interfaces/site';
export async function allSites(): Promise<Array<Site>> {
const pool = await getDb();
const query = 'SELECT * FROM site';
const result = await pool.query(query);
return result.rows || [];
}
export async function addSite(site: Site) {
const timestamp = Math.floor(new Date().getTime() / 1000);
const query = `INSERT INTO site (name, link, image, color, background, updated)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id`;
const { name, link, image, color, background } = site;
const pool = await getDb();
const result = await pool.query(query, [name, link, image, color, background, timestamp]);
return { id: result.rows[0].id };
}

View File

@@ -77,7 +77,7 @@ export async function fetchP1P(): Promise<PrinterState> {
let hassStates = await fetchHassStates(); let hassStates = await fetchHassStates();
hassStates = hassStates.filter( hassStates = hassStates.filter(
(el: Entity) => el.attributes.friendly_name?.toLowerCase()?.includes('p1p') === true (el: Entity) => el.attributes.friendly_name?.includes('P1P') === true
); );
return printerState(hassStates); return printerState(hassStates);
} catch (error) { } catch (error) {

View File

@@ -57,64 +57,15 @@ async function getClusterInfo() {
}); });
} }
export async function vmInfo(nodeName: string, vmId: string) {
const r = buildProxmoxRequest();
r.url += `nodes/${nodeName}/qemu/${vmId}/config`;
return fetch(r.url, r?.options)
.then(resp => resp.json())
.then((response) => response.data)
}
export async function vmCloudInit(nodeName: string, vmId: string) {
const r = buildProxmoxRequest();
vmId = 121
r.url += `nodes/${nodeName}/qemu/${vmId}/cloudinit`;
return fetch(r.url, r?.options)
.then(resp => resp.json())
.then((response) => response.data)
}
export async function vmAgentOS(nodeName: string, vmId: string) {
const r = buildProxmoxRequest();
vmId = 121
r.url += `nodes/${nodeName}/qemu/${vmId}/agent/get-osinfo`;
return fetch(r.url, r?.options)
.then(resp => resp.json())
.then((response) => response.data?.result)
}
export async function vmAgentFS(nodeName: string, vmId: string) {
const r = buildProxmoxRequest();
vmId = 121
r.url += `nodes/${nodeName}/qemu/${vmId}/agent/get-fsinfo`;
return fetch(r.url, r?.options)
.then(resp => resp.json())
.then((response) => response.data?.result)
}
export async function vmAgentNetwork(nodeName: string, vmId: string) {
const r = buildProxmoxRequest();
vmId = 121
r.url += `nodes/${nodeName}/qemu/${vmId}/agent/network-get-interfaces`;
return fetch(r.url, r?.options)
.then(resp => resp.json())
.then((response) => response.data?.result)
}
export async function fetchNodes(): Promise<{ nodes: Node[]; cluster: Cluster | null }> { export async function fetchNodes(): Promise<{ nodes: Node[]; cluster: Cluster | null }> {
try { try {
const { nodes, cluster } = await getClusterInfo(); const { nodes, cluster } = await getClusterInfo();
const infoBulk = Promise.all(nodes.map((node: Node) => fetchNodeInfo(node))); const infoP = Promise.all(nodes.map((node: Node) => fetchNodeInfo(node)));
const vmsBulk = Promise.all(nodes.map((node: Node) => fetchNodeVMs(node))); const vmsP = Promise.all(nodes.map((node: Node) => fetchNodeVMs(node)));
const lxcsBulk = Promise.all(nodes.map((node: Node) => fetchNodeLXCs(node))); const lxcsP = Promise.all(nodes.map((node: Node) => fetchNodeLXCs(node)));
const [info, vms, lxcs] = await Promise.all([infoBulk, vmsBulk, lxcsBulk]); const [info, vms, lxcs] = await Promise.all([infoP, vmsP, lxcsP]);
return { return {
cluster, cluster,

View File

@@ -1,175 +0,0 @@
@keyframes pulse-live {
0% {
box-shadow: 0 0 0 0 var(--pulse);
}
70% {
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
}
100% {
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 {
box-shadow:
0px 0px 2px #22242714,
0px 1px 4px #2224271f,
0px 4px 8px #22242729;
pointer-events: all;
cursor: auto;
}
:global(body.dark .card) {
box-shadow:
0px 0px 2px #eaddd514,
0px 1px 4px #eaddd515,
0px 4px 6px #eaddd520;
}
:global(body.dark .card .header, body.dark .card .footer) {
background-color: var(--highlight);
}
.header {
display: flex;
padding: 0.75rem;
background-color: white;
align-items: center;
font-size: 16px;
.icon {
height: 24px;
width: 24px;
margin-right: 0.75rem;
}
.subtle {
margin-left: 0.25rem;
opacity: 0.4;
font-weight: 500;
font-size: 0.9rem;
}
.status {
height: 1rem;
width: 1rem;
border-radius: 50%;
margin-left: auto;
position: relative;
@include pulse-dot;
&.ok {
--color: var(--positive);
--pulse: var(--pulse-positive);
}
&.warning {
--color: var(--warning);
--pulse: var(--pulse-warning);
}
&.error {
--color: var(--negative);
--pulse: var(--pulse-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;
max-width: 330px;
> div,
span {
display: flex;
padding: 0 0.5rem;
}
}
:global(.resource .title svg) {
height: 1rem;
width: 1rem;
}
.footer {
display: flex;
align-items: center;
justify-content: space-evenly;
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;
}

View File

@@ -1,266 +0,0 @@
import dns from 'dns';
import net from 'net';
let timeout = 0;
const axfrReqProloge =
'\x00\x00' + // Size
'\x00\x00' + // Transaction ID
'\x00\x20' + // Flags: Standard Query
'\x00\x01' + // Number of questions
'\x00\x00' + // Number of answers
'\x00\x00' + // Number of Authority RRs
'\x00\x00'; // Number of Additional RRs
const axfrReqEpiloge =
'\x00' + // End of name
'\x00\xfc' + // Type: AXFR
'\x00\x01'; // Class: IN
function inet_ntoa(num: number): string {
const nbuffer = new ArrayBuffer(4);
const ndv = new DataView(nbuffer);
ndv.setUint32(0, num);
const a: number[] = [];
for (let i = 0; i < 4; i++) {
a[i] = ndv.getUint8(i);
}
return a.join('.');
}
interface DecompressedLabel {
len: number;
name: string;
next?: number;
}
function decompressLabel(data: Buffer, offset: number): DecompressedLabel {
const res: DecompressedLabel = { len: 0, name: '' };
let loffset = offset;
let tmpoff = 0;
while (data[loffset] !== 0x00) {
// Check for pointers
if ((data[loffset] & 0xc0) === 0xc0) {
const newoffset = data.readUInt16BE(loffset) & 0x3fff;
const label = decompressLabel(data, newoffset + 2);
res.name += label.name;
loffset += 1;
break;
} else {
// Normal label
tmpoff = loffset + 1;
res.name += data.toString('utf8', tmpoff, tmpoff + data[loffset]);
res.name += '.';
loffset += data[loffset] + 1;
}
}
res.next = loffset + 1;
return res;
}
interface DNSResult {
questions: any[];
answers: any[];
}
function parseResponse(response: Buffer, result: DNSResult): DNSResult | number {
let offset = 14;
let len = response.readUInt16BE(0);
if (response.length !== len + 2) return -1; // Invalid length
if ((response[4] & 0x80) !== 0x80) return -2; // Not a query response
if ((response[5] & 0x0f) !== 0) return -3; // Error code present
const questions = response.readUInt16BE(6);
const answers = response.readUInt16BE(8);
const authRRs = response.readUInt16BE(10);
const aditRRs = response.readUInt16BE(12);
// Parse queries
for (let x = 0; x < questions; x++) {
const entry = decompressLabel(response, offset);
result.questions.push({
name: entry.name,
type: 'AXFR'
});
offset = (entry.next ?? 0) + 4;
}
// Parse answers
for (let x = 0; x < answers; x++) {
let entry: any = {};
entry = decompressLabel(response, offset);
offset = entry.next!;
entry.name = entry.name;
const type = response.readUInt16BE(offset);
const rclass = response.readUInt16BE(offset + 2);
entry.ttl = response.readUInt32BE(offset + 4);
const rlen = response.readUInt16BE(offset + 8);
// Skip non-INET
if (rclass !== 0x01) {
offset += rlen + 10;
continue;
}
switch (type) {
case 0x01: // A
entry.type = 'A';
entry.a = inet_ntoa(response.readUInt32BE(offset + 10));
break;
case 0x02: // NS
entry.type = 'NS';
entry.ns = decompressLabel(response, offset + 10).name;
break;
case 0x05: // CNAME
entry.type = 'CNAME';
entry.cname = decompressLabel(response, offset + 10).name;
break;
case 0x06: {
// SOA
entry.type = 'SOA';
let tentry = decompressLabel(response, offset + 10);
entry.dns = tentry.name;
tentry = decompressLabel(response, tentry.next!);
entry.mail = tentry.name;
entry.serial = response.readUInt32BE(tentry.next!);
entry.refreshInterval = response.readUInt32BE(tentry.next! + 4);
entry.retryInterval = response.readUInt32BE(tentry.next! + 8);
entry.expireLimit = response.readUInt32BE(tentry.next! + 12);
entry.minTTL = response.readUInt32BE(tentry.next! + 16);
break;
}
case 0x0f: // MX
entry.type = 'MX';
entry.pref = response.readUInt16BE(offset + 10);
entry.mx = decompressLabel(response, offset + 12).name;
break;
case 0x10: // TXT
entry.type = 'TXT';
len = response[offset + 10];
entry.txt = response.toString('utf8', offset + 11, offset + 11 + len);
break;
case 0x1c: {
// AAAA
entry.type = 'AAAA';
// const byteArr = new Uint8Array(response.slice(offset + 10, offset + 26));
// entry.aaaa = fromByteArray(byteArr).toString();
console.warn('unable to parse AAAA DNS records');
entry.aaaa = '::';
break;
}
case 0x63: // SPF
entry.type = 'SPF';
len = response[offset + 10];
entry.txt = response.toString('utf8', offset + 11, offset + 11 + len);
break;
case 0x21: // SRV
entry.type = 'SRV';
entry.priority = response.readUInt16BE(offset + 10);
entry.weight = response.readUInt16BE(offset + 12);
entry.port = response.readUInt16BE(offset + 14);
entry.target = decompressLabel(response, offset + 16).name;
break;
}
delete entry.len;
delete entry.next;
result.answers.push(entry);
offset += rlen + 10;
}
return result;
}
(dns as any).resolveAxfrTimeout = (milis: number): void => {
timeout = milis;
};
(dns as any).resolveAxfr = (
server: string,
domain: string,
callback: (code: number, result: any) => void
): void => {
const buffers: Buffer[] = [];
const split = domain.split('.');
const results: DNSResult = { questions: [], answers: [] };
let responses: Buffer[] = [];
let len = 0;
let tlen = 0;
const [hostname, portStr] = server.split(':');
const port = Number(portStr) || 53;
// Build request
buffers.push(Buffer.from(axfrReqProloge, 'binary'));
split.forEach((elem) => {
const label = Buffer.from('\x00' + elem, 'utf8');
label.writeUInt8(elem.length, 0);
buffers.push(label);
});
buffers.push(Buffer.from(axfrReqEpiloge, 'binary'));
const buffer = Buffer.concat(buffers);
// Set size and transaction ID
buffer.writeUInt16BE(buffer.length - 2, 0);
buffer.writeUInt16BE(Math.floor(Math.random() * 65535 + 1), 2);
// Connect and send request
const socket = net.connect(port, hostname, () => {
socket.write(buffer.toString('binary'), 'binary');
socket.end();
});
if (timeout) socket.setTimeout(timeout);
socket.on('data', (data: Buffer) => {
if (len === 0) len = data.readUInt16BE(0);
responses.push(data);
tlen += data.length;
if (tlen >= len + 2) {
const buf = Buffer.concat(responses, tlen);
const tmpBuf = buf.slice(0, len + 2);
const res = parseResponse(tmpBuf, results);
if (typeof res !== 'object') {
socket.destroy();
callback(res, 'Error on response');
}
}
if (tlen > len + 2) {
const buf = Buffer.concat(responses, tlen);
const tmpBuf = buf.slice(len + 2);
len = tmpBuf.readUInt16BE(0);
tlen = tmpBuf.length;
responses = [tmpBuf];
}
});
socket.on('timeout', () => {
socket.destroy();
callback(-5, 'Timeout');
});
socket.on('end', () => {
callback(0, results);
});
socket.on('error', () => {
callback(-4, 'Error connecting');
});
};
export default dns;

View File

@@ -1,8 +1,6 @@
<script lang="ts"> <script lang="ts">
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
import Sidebar from '$lib/components/Sidebar.svelte'; import Sidebar from '$lib/components/Sidebar.svelte';
import DarkmodeToggle from '$lib/components/DarkmodeToggle.svelte';
import GlobalSearch from '$lib/components/GlobalSearch.svelte';
</script> </script>
<div class="page"> <div class="page">
@@ -15,9 +13,6 @@
<slot></slot> <slot></slot>
</main> </main>
</div> </div>
<DarkmodeToggle />
<GlobalSearch />
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte'; import PageHeader from '$lib/components/PageHeader.svelte';
import PageElement from '$lib/components/PageElement.svelte'; import PageElement from '$lib/components/PageElement.svelte';
import { onMount } from 'svelte';
let elems = []; let elems = [];
let counter = 0; let counter = 0;
@@ -29,10 +28,10 @@
return { return {
bgColor: colors[counter - 1][0], bgColor: colors[counter - 1][0],
color: colors[counter - 1][1], color: colors[counter - 1][1],
name: title, title,
header: null, header: null,
description: description ? description : '', description: description ? description : '',
link: `/${title}` link: title
}; };
} }
@@ -60,13 +59,11 @@
elems = elems.concat(createPageElement('cluster ')); elems = elems.concat(createPageElement('cluster '));
elems = elems.concat(createPageElement('health ')); elems = elems.concat(createPageElement('health '));
onMount(() => window.elements = elems)
</script> </script>
<PageHeader>Welcome to schleppe.cloud infra overview</PageHeader> <PageHeader>Welcome to schleppe.cloud infra overview</PageHeader>
<p class="site-desc"> <p>
This site is a local-first dashboard for monitoring the state of digital and physical tools in a This site is a local-first dashboard for monitoring the state of digital and physical tools in a
workshop environment. It currently tracks servers (IP, cores, memory, uptime), 3D printers workshop environment. It currently tracks servers (IP, cores, memory, uptime), 3D printers
(status, history, filament stock), and other connected devices. Each device or system has its own (status, history, filament stock), and other connected devices. Each device or system has its own
@@ -74,7 +71,7 @@
general monitoring tools, IoT integrations, and project overviews. general monitoring tools, IoT integrations, and project overviews.
</p> </p>
<p class="site-desc"> <p>
The system is intended for hybrid spaces where digital infrastructure coexists with hands-on work. The system is intended for hybrid spaces where digital infrastructure coexists with hands-on work.
Alongside real-time monitoring, Schleppe is expanding to reflect the broader physical Alongside real-time monitoring, Schleppe is expanding to reflect the broader physical
workspace—covering areas like tool usage, material stocks, and workstations for welding, workspace—covering areas like tool usage, material stocks, and workstations for welding,
@@ -83,11 +80,11 @@
</p> </p>
<div class="shortcut-grid"> <div class="shortcut-grid">
{#each elems as shortcut (shortcut.name)} {#each elems as shortcut (shortcut.title)}
<PageElement <PageElement
bgColor={shortcut.bgColor} bgColor={shortcut.bgColor}
color={shortcut.color} color={shortcut.color}
title={shortcut.name} title={shortcut.title}
header={shortcut.header} header={shortcut.header}
description={shortcut.description} description={shortcut.description}
link={shortcut.link} link={shortcut.link}
@@ -96,23 +93,18 @@
</div> </div>
<style lang="scss"> <style lang="scss">
p.site-desc { p {
font-size: 1.1rem; font-size: 1.1rem;
line-height: 1.4; line-height: 1.4;
line-height: 1.7; line-height: 1.7;
color: #333; color: #333;
background-color: #fafafa; background-color: #fafafa; /* Subtle background to separate it from the rest */
padding: 2rem; padding: 2rem;
border-radius: 1rem; /* Soft edges */ border-radius: 1rem; /* Soft edges */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); /* Light shadow for depth */ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); /* Light shadow for depth */
} }
:global(body.dark p.site-desc) {
background-color: var(--highlight);
color: var(--color);
}
.shortcut-grid { .shortcut-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import Node from '$lib/components/Node.svelte'; import Node from '$lib/components/Node.svelte';
import Deploy from '$lib/components/Deploy.svelte'; import Deploy from '$lib/components/Deploy.svelte';
import Daemon from '$lib/components/Daemon.svelte'; import Daemon from '$lib/components/Daemon.svelte';
@@ -15,22 +14,10 @@
const rawDaemons: V1DaemonSet[] = data?.daemons; const rawDaemons: V1DaemonSet[] = data?.daemons;
const rawNodes: V1Node[] = data?.nodes; const rawNodes: V1Node[] = data?.nodes;
let filterLC = $derived(filterValue.toLowerCase()); let filterLC = $derived(filterValue.toLowerCase())
let deployments = $derived(rawDeployments.filter((d) => d.metadata.name.includes(filterLC))); let deployments = $derived(rawDeployments.filter((d) => d.metadata.name.includes(filterLC)));
let daemons = $derived(rawDaemons.filter((d) => d.metadata.name.includes(filterLC))); let daemons = $derived(rawDaemons.filter((d) => d.metadata.name.includes(filterLC)));
let nodes = $derived(rawNodes.filter((n) => n.metadata.name.includes(filterLC))); let nodes = $derived(rawNodes.filter((n) => n.metadata.name.includes(filterLC)));
onMount(() => {
window.elements = deployments
.map((d) => {
return {
name: d.metadata?.name || undefined,
link: `/cluster/deployment/${d.metadata?.uid}`,
...d
};
})
.filter((d) => d.name);
});
</script> </script>
<PageHeader>Cluster overview</PageHeader> <PageHeader>Cluster overview</PageHeader>

View File

@@ -1,63 +0,0 @@
import { exec } from 'child_process';
import dns from '$lib/utils/dns';
import type { Record } from '$lib/interfaces/DNS';
import type { PageServerLoad } from './$types';
// Regex for IPv4 validation
const ipv4Regex = /^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}$/;
function ping(data: string): Promise<boolean> {
if (!ipv4Regex.test(data)) return;
const ip = data;
console.log(`Starting ping for ip: ${ip}`);
const resolver = '10.0.0.72';
return new Promise((resolve, reject) => {
exec(
`ping -c 1 -t 1.5 -W 1 ${ip} > /dev/null 2>&1 && exit 0 || exit 1`,
(error, stdout, stderr) => {
if (error) {
console.error(`❌ Exec error: ${error.message}`);
reject(error);
}
if (stderr) {
console.error(`⚠️ Stderr: ${stderr}`);
reject(error);
}
console.log('✅ Ping response received:', stdout);
resolve(true);
}
);
});
}
function axfrAsync(server: string, domain: string) {
return new Promise((resolve, reject) => {
dns.resolveAxfr(server, domain, (err, resp) => {
if (err) reject(err);
resolve(resp?.answers);
});
});
}
export const load: PageServerLoad = async () => {
const server = '10.0.0.72';
const domain = 'schleppe';
const records = await axfrAsync(server, domain);
// console.log('records::', records);
const ARecords = records?.filter((r) => r?.type === 'A');
for (let i = 0; i < ARecords.length; i++) {
try {
// const a = await ping(ARecords[i].a);
records[i]['available'] = true;
} catch (_) { }
}
return {
records
};
};

View File

@@ -1,163 +0,0 @@
<script lang="ts">
import Search from '$lib/icons/search.svelte';
import Input from '$lib/components/Input.svelte';
import Table from '$lib/components/Table.svelte';
import Dialog from '$lib/components/Dialog.svelte';
import FormDNS from '$lib/components/forms/FormDNS.svelte';
import type { Record } from '$lib/interfaces/DNS';
import type { PageData } from '../$types';
let { data }: { data: PageData } = $props();
let recordsFilter = $state('');
let recordsSort = $state('type');
let open = $state(false);
const rawRecords: Record[] = data?.records || [];
let records = $derived(
rawRecords
?.filter(
(r: Record) =>
r.name?.toLowerCase()?.includes(recordsFilter) ||
r.data?.toLowerCase()?.includes(recordsFilter)
)
.sort((a, b) => (a?.[recordsSort] < b?.[recordsSort] ? -1 : 1))
);
</script>
<h1>DNS</h1>
<Table
title="Zonal records"
description="schleppe colors are currently in stock. Overview of currently stocked filament."
columns={['Ping', 'Details', 'TTL']}
data={records}
footer="Last updated on ~some date~"
>
<div slot="actions" class="filament-table-inputs">
<div>
<Input
placeholder={`${records?.[0]?.name || 'record'}`}
icon={Search}
bind:value={recordsFilter}
label="Records filter"
/>
</div>
<button class="affirmative" on:click={() => (open = true)}><span>Add new</span></button>
</div>
<tbody slot="tbody">
{#each records as row (row)}
<tr>
<td>{row?.available === true ? 'up' : 'down'}</td>
<td class="info">
<div class="meta">
<span>name: {row.name}</span>
<span>type: {row.type}</span>
<span>addr: {row.a}</span>
</div>
</td>
<td>{row.ttl}</td>
</tr>
{/each}
</tbody>
</Table>
{#if open}
<Dialog
title="Add DNS record"
description="You can select anything deployed in <b>Belgium (europe-west1) datacenter</b> and create an internal connection with your service."
close={() => (open = false)}
>
<FormDNS close={() => (open = false)} />
</Dialog>
{/if}
<style lang="scss">
.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;
}
}
.filament-table-inputs {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
> div {
max-width: 450px;
}
> button {
flex: unset;
height: 2.6rem;
}
}
/* filament table */
tr td {
&:first-of-type {
}
&.info {
display: table-cell;
vertical-align: middle;
h2 {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 0.45em;
font-size: 1.1rem;
font-weight: 300;
color: var(--theme);
}
.meta {
display: flex;
gap: 0.3rem;
flex-direction: column;
opacity: 0.6;
word-break: break-all;
}
}
}
.color {
--size: 4rem;
display: block;
width: var(--size);
height: var(--size);
border-radius: var(--border-radius, 1rem);
}
</style>

View File

@@ -22,7 +22,7 @@
columns={['Domain', 'SSL', 'Status', 'Code']} columns={['Domain', 'SSL', 'Status', 'Code']}
> >
<tbody slot="tbody"> <tbody slot="tbody">
{#each httpHealth as row (row)} {#each httpHealth as row, i (row)}
<tr> <tr>
<td>{row.domain}</td> <td>{row.domain}</td>
<td> <td>
@@ -48,7 +48,7 @@
</Table> </Table>
{#if selectedSSL !== null} {#if selectedSSL !== null}
<Dialog close={() => (selectedSSL = null)} title="SSL Certificate info"> <Dialog on:close={() => (selectedSSL = null)} title="SSL Certificate info">
<JsonViewer json={selectedSSL} /> <JsonViewer json={selectedSSL} />
</Dialog> </Dialog>
{/if} {/if}

View File

@@ -1,6 +1,6 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { fetchP1P } from '$lib/server/homeassistant'; import { fetchP1P } from '$lib/server/homeassistant';
import { getAllFilament } from '$lib/server/database/filament'; import { getAllFilament } from '$lib/server/database';
import type { Filament } from '$lib/interfaces/printer'; import type { Filament } from '$lib/interfaces/printer';
interface PrinterState { interface PrinterState {

View File

@@ -46,7 +46,6 @@
let open = $state(false); let open = $state(false);
let timeLeftInterval: ReturnType<typeof setInterval>; let timeLeftInterval: ReturnType<typeof setInterval>;
console.log("got data:", data)
const rawFilament: Filament[] = data?.filament || []; const rawFilament: Filament[] = data?.filament || [];
let filament = $derived( let filament = $derived(
rawFilament rawFilament
@@ -247,7 +246,7 @@
</div> </div>
<tbody slot="tbody"> <tbody slot="tbody">
{#each filament as row (row)} {#each filament as row, i (row)}
<tr class="link" on:click={() => goto(filamentLink(row))}> <tr class="link" on:click={() => goto(filamentLink(row))}>
<td><span class="color" style={`background: ${row.hex}`} /></td> <td><span class="color" style={`background: ${row.hex}`} /></td>
<td class="info"> <td class="info">
@@ -268,7 +267,7 @@
{#if open} {#if open}
<Dialog <Dialog
close={() => (open = false)} on:close={() => (open = false)}
title="Add new filament" title="Add new filament"
description="You can select anything deployed in <b>Belgium (europe-west1) datacenter</b> and create an internal connection with your service." description="You can select anything deployed in <b>Belgium (europe-west1) datacenter</b> and create an internal connection with your service."
> >
@@ -335,6 +334,7 @@
&.info { &.info {
display: table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
padding-left: 25px;
h2 { h2 {
width: 100%; width: 100%;

View File

@@ -1,4 +1,4 @@
import { addFilament, updateFilament } from '$lib/server/database/filament'; import { addFilament, updateFilament } from '$lib/server/database';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
export const PUT: RequestHandler = async ({ params, request }) => { export const PUT: RequestHandler = async ({ params, request }) => {

View File

@@ -1,5 +1,5 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { getFilamentByColor } from '$lib/server/database/filament'; import { getFilamentByColor } from '$lib/server/database';
export const load = async ({ params }: Parameters<PageServerLoad>[0]) => { export const load = async ({ params }: Parameters<PageServerLoad>[0]) => {
let { id } = params; let { id } = params;

View File

@@ -1,43 +1,12 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import PageHeader from '$lib/components/PageHeader.svelte'; import PageHeader from '$lib/components/PageHeader.svelte';
import ServerComp from '$lib/components/Server.svelte'; import ServerComp from '$lib/components/Server.svelte';
import ServerSummary from '$lib/components/ServerSummary.svelte'; import ServerSummary from '$lib/components/ServerSummary.svelte';
import VMComp from '$lib/components/VM.svelte';
import LXCComp from '$lib/components/LXC.svelte';
import type { VM, LXC } from '$lib/components/VM.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
const { cluster, nodes } = data; const { cluster, nodes } = data;
const allVms: Array<VM> = nodes.flatMap((n) => n.vms).filter((v) => v.template !== 1);
const allLxcs: Array<LXC> = nodes.flatMap((n) => n.lxcs);
onMount(() => {
window.elements = [
...allVms
.map((vm) => {
return {
link: `/servers/vm/${vm.vmid}`,
...vm
};
})
.filter((d) => d.name),
...nodes.map((node) => {
return {
link: `/servers/node/${node.name}`,
...node
};
}),
...allLxcs.map((lxc) => {
return {
link: `/servers/lxc/${lxc.vmid}`,
...lxc
};
})
];
});
</script> </script>
<PageHeader>Servers</PageHeader> <PageHeader>Servers</PageHeader>
@@ -46,63 +15,18 @@
<div class="server-list"> <div class="server-list">
{#each nodes as node (node.name)} {#each nodes as node (node.name)}
<div>
<ServerComp {node} /> <ServerComp {node} />
</div>
{/each} {/each}
</div> </div>
<details open>
<summary>
<h2>VMs</h2>
</summary>
<div class="vm-list">
{#each allVms as vm (vm.vmid)}
<VMComp {vm} />
{/each}
</div>
</details>
<details open>
<summary>
<h2>LXCs</h2>
</summary>
<div class="vm-list">
{#each allLxcs as lxc (lxc)}
<LXCComp {lxc} />
{/each}
</div>
</details>
<style lang="scss"> <style lang="scss">
*:not(:last-child) { .server-list {
margin-bottom: 2rem;
}
.server-list,
.vm-list {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
align-items: left; align-items: left;
gap: 2rem; gap: 2rem;
&:not(:last-of-type) {
margin-bottom: 2rem;
}
}
h2 {
font-weight: 500;
}
details summary::-webkit-details-marker,
details summary::marker {
display: none;
}
details > summary {
list-style: none;
cursor: pointer;
} }
</style> </style>

View File

@@ -1,82 +0,0 @@
import {
fetchNodes,
vmInfo,
vmCloudInit,
vmAgentOS,
vmAgentFS,
vmAgentNetwork
} from '$lib/server/proxmox';
import type { Node, VM } from '$lib/interfaces/proxmox';
import type { PageServerLoad } from './$types';
const AVAILABLE_RESOURCES = ['node', 'vm', 'lxc'];
const filterResources = (resource: Node | VM, id) => {
if (resource?.name && resource.name === id) {
return resource;
}
return null;
};
export const load: PageServerLoad = async ({ params }) => {
const { kind, id } = params;
console.log('KIND', kind);
if (!AVAILABLE_RESOURCES.includes(kind)) {
return {
error: 'No resource ' + kind,
resource: null
};
}
console.log(params.id);
const cluster = await fetchNodes();
let vm = [];
let nodeId;
const resources: Array<Node | VM> = [];
switch (kind) {
case 'node':
vm = cluster?.nodes.find((n) => n.name === id) || resources;
break;
case 'vm':
nodeId = cluster.nodes.find(
(n) => n.vms.filter((vm) => String(vm.vmid) === id)?.length > 0
)?.name;
const clusterVM = cluster.nodes.flatMap((n) => n.vms)?.find((vm) => String(vm.vmid) === id);
if (!nodeId || !clusterVM.vmid) return;
const [cloudInit, os, fs, network] = await Promise.all([
vmCloudInit(nodeId, clusterVM.vmid),
vmAgentOS(nodeId, clusterVM.vmid),
vmAgentFS(nodeId, clusterVM.vmid),
vmAgentNetwork(nodeId, clusterVM.vmid)
]);
vm = {
config: await vmInfo(nodeId, clusterVM.vmid),
cloudInit,
os,
fs,
network,
...clusterVM
};
// resources = [vm]
break;
case 'lxc':
vm = cluster.nodes.flatMap((n) => n.lxcs)?.find((lxc) => String(lxc.vmid) === id);
break;
default:
console.log('no resources found');
}
// console.log('returning', vm);
return {
resource: vm,
kind: params.kind,
id: params.id,
error: null
};
};

View File

@@ -1,53 +0,0 @@
<script lang="ts">
import JsonViewer from '$lib/components/JsonViewer.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import VMDescribe from '$lib/components/prox-describe/VM.svelte';
import External from '$lib/icons/external.svelte';
import Link from '$lib/icons/link.svelte';
import type { Node, VM } from '$lib/interfaces/proxmox';
import type { PageData, PageProps } from './$types';
let { data }: { data: PageData } = $props();
const { error, kind } = data;
const { resource }: { vm: VM; node: Node } = data;
console.log('RESOURCE', resource);
let res = $state(data.kind);
const template = `https://apollo.schleppe:8006/#v1:0:=qemu%2F${resource?.vmid}:4:::::::`
console.log('kind res:', res);
</script>
<PageHeader>{kind || 'Resource'}: {resource?.name || 'not found'}</PageHeader>
{#if error}
<p>{error}</p>
{/if}
{#if resource}
<div class="title">
<a href={template}>View in proxmox</a>
<span class="link"><External /></span>
</div>
{#if kind == 'vm'}
<VMDescribe vm={resource} />
{:else}
<p>{kind} stuffs</p>
<JsonViewer json={resource} />
{/if}
{:else}
<h2>404. '{kind}' resource not found!</h2>
{/if}
<style lang="scss">
.title {
font-size: 1.1rem;
font-family: 'Reckless Neue';
}
.link {
display: inline-block;
height: 1rem;
width: 1rem;
}
</style>

View File

@@ -1,49 +0,0 @@
import { allSites, addSite } from '$lib/server/database/sites';
import type { Site } from '$lib/interfaces/site';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async (): Promise<{ sites: Array<Site> }> => {
let sites: Site[] = [];
try {
sites = await allSites();
console.log('got sites:', sites);
} catch (error) {
console.error('error while fetching sites server props, likely db issue');
console.error(error);
}
return { sites };
};
export const actions = {
default: async ({ request }) => {
try {
const formData = await request.formData();
// Extract values by input `name` attributes
const name = formData.get('Name')?.toString().trim();
const link = formData.get('Link')?.toString().trim();
const image = formData.get('Image')?.toString().trim();
const color = formData.get('Color')?.toString().trim();
const background = formData.get('Background')?.toString().trim();
if (!name || !link || !image || !color || !background) {
return { error: 'All fields are required!', success: false, statusCode: 400 };
}
if (!name || !link) {
return { error: 'name & link are required', success: false, statusCode: 400 };
}
const site: Site = { name, link, image, color, background };
await addSite(site);
return { success: true };
} catch (err: unknown) {
console.log(err);
console.error('Failed to add site:', err.message);
return { error: 'internal server error', success: false, statusCode: 500 };
}
}
} satisfies Actions;

View File

@@ -1,28 +1,103 @@
<script lang="ts"> <script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte'; import PageHeader from '$lib/components/PageHeader.svelte';
import Dialog from '$lib/components/Dialog.svelte';
import Section from '$lib/components/Section.svelte'; import Section from '$lib/components/Section.svelte';
import FormSite from '$lib/components/forms/FormSite.svelte';
import ThumbnailButton from '$lib/components/ThumbnailButton.svelte'; import ThumbnailButton from '$lib/components/ThumbnailButton.svelte';
import type { Site } from '$lib/interfaces/site.ts';
import { onMount } from 'svelte';
let { data }: { data: { site: Site } } = $props(); interface Site {
let open = $state(false); title: string;
image: string;
link: string;
background?: string;
color?: string;
}
const { sites } = data; const sites: Array<Site> = [
{
onMount(() => window.elements = sites) title: 'Grafana',
image: '/images/grafana.png',
link: 'https://grafana.schleppe.cloud',
background: '#F5E3DC',
color: '#F05A24'
},
{
title: 'Prometheus',
image: '/images/prometheus.svg',
link: 'http://prome.schleppe:9090',
background: '#262221',
color: '#F3BFA2'
},
{
title: 'Traefik',
image: '/images/traefik.png',
link: 'https://grafana.schleppe.cloud',
background: '#30A4C2',
color: 'white'
},
{
title: 'Kibana',
image: '/images/kibana.svg',
link: 'https://kibana.schleppe.cloud',
background: '#f6cfdd',
color: '#401C26'
},
{
title: 'HASS',
image: '/images/hass.png',
link: 'http://homeassistant.schleppe:8123',
background: '#1ABCF2',
color: 'white'
},
{
title: 'Vault',
image: '/images/vault.svg',
link: 'http://vault.schleppe:8200',
background: 'white',
color: 'black'
},
{
title: 'Drone',
image: '/images/drone.png',
link: 'https://drone.schleppe.cloud',
background: '#D8E2F0',
color: '#1E375A'
},
{
title: 'Immich',
image: '/images/immich.png',
link: 'http://immich.schleppe:2283',
background: 'white',
color: 'black'
},
{
title: 'Wiki',
image: '/images/xwiki.png',
link: 'https://wiki.schleppe.cloud',
background: 'white',
color: 'black'
},
{
title: 'Gitea',
image: '/images/gitea.png',
link: 'https://git.schleppe.cloud',
background: '#E6E7D7',
color: '#609925'
},
{
title: 'PBS',
image: '/images/proxmox.png',
link: 'https://clio.schleppe:8007',
background: '#EDE1D2',
color: '#E66B00'
}
];
</script> </script>
<PageHeader>Sites <PageHeader>Sites</PageHeader>
<button class="add-site-btn affirmative" on:click={() => (open = true)}><span>Add new site</span></button>
</PageHeader>
<div class="section-wrapper"> <div class="section-wrapper">
{#each sites as site (site)} {#each sites as site}
<ThumbnailButton <ThumbnailButton
title={site.name} title={site.title}
image={site.image} image={site.image}
background={site.background} background={site.background}
color={site.color} color={site.color}
@@ -31,15 +106,27 @@
{/each} {/each}
</div> </div>
{#if open} <div class="section-wrapper full-width">
<Dialog <Section
close={() => (open = false)} title="Expose HTTP traffic"
title="Add new site" 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."
description="You can select anything deployed in <b>Belgium (europe-west1) datacenter</b> and create an internal connection with your service." />
>
<FormSite on:close={() => (open = false)} /> <Section
</Dialog> title="IP restrictions"
{/if} 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>
<style lang="scss"> <style lang="scss">
.section-wrapper { .section-wrapper {
@@ -62,10 +149,4 @@
margin-top: 4rem; margin-top: 4rem;
} }
} }
:global(button.add-site-btn) {
font-size: 1.2rem;
float: right;
height: 2.5rem;
}
</style> </style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
fill="#F4400E" stroke="none">
<path d="M956 1798 c-9 -12 -16 -39 -16 -60 0 -23 -5 -38 -12 -39 -166 -9
-356 -117 -473 -269 -50 -65 -121 -206 -130 -257 -3 -18 -9 -42 -13 -55 -5
-13 -10 -45 -11 -73 -2 -27 -5 -53 -7 -56 -5 -9 31 -40 53 -44 10 -2 30 -4 44
-4 14 -1 26 -7 26 -13 1 -92 68 -295 107 -327 15 -12 69 -12 115 0 8 3 20 -14
31 -47 40 -115 127 -235 230 -317 83 -66 109 -70 166 -28 123 90 227 227 264
349 l13 43 59 -7 c42 -5 62 -3 74 7 28 23 86 173 98 255 12 78 17 93 31 88 7
-3 31 0 53 5 35 10 40 15 44 46 2 19 -2 63 -10 97 -7 35 -15 74 -18 88 -3 14
-27 70 -54 125 -100 205 -282 347 -492 386 l-68 12 0 42 c0 28 -6 47 -18 58
-26 24 -68 21 -86 -5z m159 -223 c59 -10 173 -66 233 -114 109 -87 194 -227
227 -375 l6 -29 -50 6 c-106 13 -244 77 -322 151 -48 45 -102 117 -135 179
-32 62 -64 87 -96 74 -9 -3 -35 -40 -58 -83 -41 -75 -173 -224 -198 -224 -6 0
-12 -4 -14 -8 -3 -10 -109 -61 -142 -69 -61 -15 -136 -26 -142 -20 -12 11 44
166 82 228 88 145 239 258 380 283 15 3 28 7 31 9 6 6 152 1 198 -8z m-56
-371 c29 -36 79 -86 112 -111 50 -39 59 -51 59 -77 0 -43 -28 -120 -66 -182
-30 -47 -149 -174 -165 -174 -4 0 -38 31 -76 69 -80 80 -140 187 -149 266 l-6
50 60 47 c33 26 84 76 113 112 29 36 55 66 59 66 4 0 30 -30 59 -66z m405
-292 c-15 -101 -52 -205 -70 -198 -7 3 -32 10 -56 16 -81 22 -81 23 -49 88 16
31 37 82 45 113 l17 56 59 -19 60 -20 -6 -36z m-801 41 c10 -55 16 -72 46
-132 27 -55 28 -62 13 -70 -14 -7 -119 -41 -128 -41 -4 0 -24 64 -22 70 1 3
-4 14 -10 26 -6 11 -14 34 -17 50 -19 101 -20 99 29 108 16 3 31 8 34 11 3 3
15 5 28 5 16 0 23 -7 27 -27z m198 -330 c50 -48 117 -93 138 -93 23 0 68 30
128 84 43 39 56 46 78 40 23 -6 26 -10 20 -33 -8 -32 -59 -144 -77 -168 -28
-36 -140 -143 -150 -143 -6 0 -43 33 -82 73 -100 102 -171 251 -127 269 27 11
31 9 72 -29z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="InfluxData_Symbol_Only" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" width="900px" height="900px" viewBox="-173 -143 900 900" style="enable-background:new -173 -143 900 900;"
xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#22ADF6;}
</style>
<rect id="Background" x="-173" y="-143" class="st0" width="900" height="900"/>
<path id="Cuboctahedron" class="st1" d="M694.1,394.9l-81-352.7C608.5,22.9,591,3.6,571.7-2L201.5-116.2c-4.6-1.8-10.1-1.8-15.7-1.8
c-15.7,0-32.2,6.4-43.3,15.7l-265.2,246.8c-14.7,12.9-22.1,38.7-17.5,57.1l86.6,377.6c4.6,19.3,22.1,38.7,41.4,44.2l346.2,106.8
c4.6,1.8,10.1,1.8,15.7,1.8c15.7,0,32.2-6.4,43.3-15.7L676.6,453C691.4,439.2,698.7,414.3,694.1,394.9z M240.2-32.4l254.1,78.3
c10.1,2.8,10.1,7.4,0,10.1L360.8,86.4c-10.1,2.8-23.9-1.8-31.3-9.2l-93-100.4C228.2-31.4,230-35.1,240.2-32.4z M398.5,423.5
c2.8,10.1-3.7,15.7-13.8,12.9l-274.4-84.7c-10.1-2.8-12-11.1-4.6-18.4L315.7,138c7.4-7.4,15.7-4.6,18.4,5.5L398.5,423.5z
M-53.6,174.8L169.3-32.4c7.4-7.4,19.3-6.4,26.7,0.9L307.4,89.2c7.4,7.4,6.4,19.3-0.9,26.7L83.6,323.1c-7.4,7.4-19.3,6.4-26.7-0.9
L-54.5,201.6C-61.9,193.3-60.9,181.3-53.6,174.8z M0.8,503.6l-58.9-258.8c-2.8-10.1,1.8-12,8.3-4.6l93,100.4
c7.4,7.4,10.1,22.1,7.4,32.2L10,503.6C7.2,513.7,2.6,513.7,0.8,503.6z M326.7,654.6l-291-89.3c-10.1-2.8-15.7-13.8-12.9-23.9
l48.8-156.6c2.8-10.1,13.8-15.7,23.9-12.9l291,89.3c10.1,2.8,15.7,13.8,12.9,23.9l-48.8,156.6C347,651.9,336.9,657.4,326.7,654.6z
M584.5,442.8L390.3,623.3c-7.4,7.4-11,4.6-8.3-5.5L422.5,487c2.8-10.1,13.8-20.3,23.9-22.1l133.5-30.4
C590.1,431.8,591.9,436.4,584.5,442.8z M605.7,404.2L445.5,441c-10.1,2.8-20.3-3.7-23-13.8l-68.1-296.5c-2.8-10.1,3.7-20.3,13.8-23
l160.2-36.8c10.1-2.8,20.3,3.7,23,13.8l68.1,296.5C622.3,392.2,615.9,402.3,605.7,404.2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -28,9 +28,7 @@
font-style: normal; font-style: normal;
} }
:root, :root {
.light {
color-scheme: light;
--bg: #f9f5f3; --bg: #f9f5f3;
--color: #1c1819; --color: #1c1819;
--highlight: #eaddd5; --highlight: #eaddd5;
@@ -39,63 +37,9 @@
--positive: #00d439; --positive: #00d439;
--negative: #ff5449; --negative: #ff5449;
--warning: #ffa312; --warning: #ffa312;
--pulse-positive: rgba(0, 212, 57, 0.7);
--pulse-negative: rgba(255, 84, 73, 0.7);
--pulse-warning: rgba(255, 163, 18, 0.7);
--border: 1px solid #eaddd5; --border: 1px solid #eaddd5;
--border-radius: 0.75rem; --border-radius: 0.75rem;
--card-bg: rgba(255, 255, 255, 0.7);
--key: #6e80d6;
--muted: #7a6d6e;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: light dark;
--bg: #121010;
--color: #f4eeeb;
--highlight: #2b2325;
--theme: #eaddd5;
--positive: #3ddc84;
--negative: #ff6b63;
--warning: #ffb74d;
--pulse-positive: rgba(61, 220, 132, 0.6);
--pulse-negative: rgba(255, 107, 99, 0.6);
--pulse-warning: rgba(255, 183, 77, 0.6);
--border: 1px solid #2b2325;
--key: #9fb3ff;
--muted: #b0b0b0;
--card-bg: #303037;
}
}
body.dark {
--bg: #121010;
--color: #f4eeeb;
--highlight: #2b2325;
--theme: #eaddd5;
--positive: #3ddc84;
--negative: #ff6b63;
--warning: #ffb74d;
--pulse-positive: rgba(61, 220, 132, 0.6);
--pulse-negative: rgba(255, 107, 99, 0.6);
--pulse-warning: rgba(255, 183, 77, 0.6);
--border: 1px solid #2b2325;
--key: #9fb3ff;
--muted: #b0b0b0;
--card-bg: #303037;
}
svg {
fill: var(--color);
} }
body { body {
@@ -107,9 +51,6 @@ body {
background-color: var(--bg); background-color: var(--bg);
color: var(--color); color: var(--color);
font-size: 14px; font-size: 14px;
transition:
background 0.3s ease,
color 0.3s ease;
} }
a, a,
@@ -155,7 +96,6 @@ button {
border: none; border: none;
position: relative; position: relative;
background: transparent; background: transparent;
color: var(--color);
height: 100%; height: 100%;
border-radius: 0.5rem; border-radius: 0.5rem;
display: inline-block; display: inline-block;
@@ -194,21 +134,6 @@ button.affirmative:hover span {
background-color: var(--highlight); background-color: var(--highlight);
} }
/* dark mode button overrides */
body.dark button span {
background-color: var(--theme);
color: var(--bg);
}
body.dark button.affirmative span {
color: var(--bg);
}
body.dark button.affirmative:hover span {
color: var(--color);
background-color: var(--highlight);
border-color: var(--bg);
}
button:disabled { button:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
@@ -266,7 +191,7 @@ table tr:not(table tr:last-of-type) {
table tr:hover > td { table tr:hover > td {
background-color: var(--highlight); background-color: var(--highlight);
background-color: var(--bg); background-color: #f5ede9;
} }
table tr.link { table tr.link {
@@ -279,14 +204,6 @@ table tr.link {
border: var(--border); border: var(--border);
border-radius: var(--border-radius, 1rem); border-radius: var(--border-radius, 1rem);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
@media screen and (max-width: 750px) {
padding: 0.5rem;
}
}
body.dark .main-container {
background: var(--highlight) !important;
} }
.section-wrapper { .section-wrapper {

View File

@@ -44,9 +44,14 @@ COPY default.vcl.tmpl /etc/varnish/
COPY *.vcl /etc/varnish/ COPY *.vcl /etc/varnish/
COPY includes /etc/varnish/includes COPY includes /etc/varnish/includes
# Create entrypoint script # Set variables for *.tmpl files
COPY docker-entrypoint.sh /usr/local/bin/ ARG PROXY_HOST=$PROXY_HOST
RUN chmod +x /usr/local/bin/docker-entrypoint.sh ARG IMAGE_HOST=$IMAGE_HOST
# Generate VCL
RUN gomplate -f /etc/varnish/default.vcl.tmpl -o /etc/varnish/default.vcl
RUN rm /etc/varnish/default.vcl.tmpl
EXPOSE 6081 EXPOSE 6081
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD ["varnishd", "-F", "-f", "/etc/varnish/default.vcl", "-a", ":6081", "-s", "malloc,512m"]

View File

@@ -46,6 +46,17 @@ sub vcl_recv {
unset req.http.Cookie; unset req.http.Cookie;
} }
// Svelte-kit needs to distinguish between it's own files and the Host header.
// The X-Forwarded-* headers below are to tell svelte-kit where it's local files are,
// and the Host header is included in the returned html & js referencing the external
// domain or proxy requested by client.
// https://svelte.dev/docs/kit/adapter-node#Environment-variables-ORIGIN-PROTOCOL_HEADER-HOST_HEADER-and-PORT_HEADER
sub vcl_backend_fetch {
set bereq.http.X-Forwarded-Host = "localhost";
set bereq.http.X-Forwarded-Port = "3000";
set bereq.http.X-Forwarded-Proto = "http";
}
sub vcl_synth { sub vcl_synth {
if (resp.status == 204) { if (resp.status == 204) {
set resp.http.Access-Control-Allow-Origin = "*"; set resp.http.Access-Control-Allow-Origin = "*";

View File

@@ -1,8 +0,0 @@
#!/bin/sh
set -e
# Generate VCL at runtime
gomplate -f /etc/varnish/default.vcl.tmpl -o /etc/varnish/default.vcl
# Execute startup CMD
exec varnishd -F -f /etc/varnish/default.vcl -a :6081 -s malloc,512m