From ea9cdb76923afee47e6b38be5a545ed9251035bf Mon Sep 17 00:00:00 2001 From: Kevin Midboe Date: Tue, 12 Aug 2025 23:40:08 +0200 Subject: [PATCH] working nice. docker uses bun --- .drone.yml | 4 +- .kubernetes/deployment.yml | 29 +- Dockerfile | 2 +- README.md | 60 +++- package.json | 3 + src/lib/components/ColorInput.svelte | 88 +++++ src/lib/components/Daemon.svelte | 9 +- src/lib/components/Deploy.svelte | 11 +- src/lib/components/Dialog.svelte | 174 +++++++++ src/lib/components/Dropdown.svelte | 114 ++++++ src/lib/components/Error.svelte | 23 ++ src/lib/components/Header.svelte | 8 + src/lib/components/Input.svelte | 91 +++++ src/lib/components/JsonViewer.svelte | 32 ++ src/lib/components/LiveImage.svelte | 98 +++++ src/lib/components/PageElement.svelte | 113 ++++++ src/lib/components/Pod.svelte | 7 + src/lib/components/Section.svelte | 10 +- src/lib/components/Server.svelte | 39 +- src/lib/components/Sidebar.svelte | 8 +- src/lib/components/Success.svelte | 23 ++ src/lib/components/Table.svelte | 78 +--- src/lib/components/TableWrapper.svelte | 0 src/lib/components/ThumbnailButton.svelte | 126 +++++++ src/lib/components/Warning.svelte | 23 ++ src/lib/components/forms/FormFilament.svelte | 82 +++++ .../components/kube-describe/DaemonSet.svelte | 98 +++++ .../kube-describe/Deployment.svelte | 109 ++++++ .../components/kube-describe/Pod.svelte} | 60 ++-- src/lib/icons/Length.svelte | 14 + src/lib/icons/barcore.svelte | 30 ++ src/lib/icons/certificate.svelte | 23 ++ src/lib/icons/external.svelte | 14 + src/lib/icons/fingerprint.svelte | 15 + src/lib/icons/flower.svelte | 13 + src/lib/icons/helm.svelte | 14 + src/lib/icons/link.svelte | 4 +- src/lib/icons/pencil-ruler.svelte | 17 + src/lib/icons/search.svelte | 8 + src/lib/icons/speed.svelte | 17 + src/lib/icons/sync.svelte | 15 + src/lib/icons/time.svelte | 14 + src/lib/icons/tuning-fork.svg | 9 + src/lib/icons/user.svelte | 16 + src/lib/icons/weight.svelte | 22 ++ src/lib/interfaces/health.ts | 11 + src/lib/interfaces/printer.ts | 14 +- src/lib/server/database.ts | 135 +++++++ src/lib/server/health_http.ts | 54 +++ src/lib/server/homeassistant.ts | 55 ++- src/lib/server/kubernetes.ts | 55 ++- src/lib/server/proxmox.ts | 4 +- src/lib/server/traefik.ts | 3 +- src/lib/utils/conversion.ts | 49 +++ src/lib/utils/hash.ts | 22 ++ src/lib/utils/mouseEvents.ts | 14 + src/lib/utils/staticImageSource.ts | 2 + src/routes/+layout.svelte | 15 +- src/routes/+page.svelte | 188 ++++++---- src/routes/cluster/+page.svelte | 38 +- .../cluster/[resource]/[uid]/+page.server.ts | 59 +++ .../cluster/[resource]/[uid]/+page.svelte | 32 ++ .../{pod => [resource]}/[uid]/logs/+server.ts | 0 src/routes/cluster/pod/[uid]/+page.server.ts | 17 - src/routes/health/+page.server.ts | 30 ++ src/routes/health/+page.svelte | 54 ++- src/routes/image/[...catchall]/+server.ts | 37 ++ src/routes/network/+page.svelte | 31 +- src/routes/network/[id]/+page.svelte | 6 +- src/routes/printer/+page.server.ts | 29 +- src/routes/printer/+page.svelte | 335 ++++++++++++------ src/routes/printer/[id]/+page.server.ts | 12 - .../printer/filament/[id]/+page.server.ts | 13 + .../printer/{ => filament}/[id]/+page.svelte | 6 +- src/routes/printer/section_image.svelte | 39 ++ .../printer/section_printer_attributes.svelte | 77 ++++ src/routes/sites/+page.svelte | 62 +++- static/fonts/NormanVariable-Regular.woff2 | Bin 0 -> 45808 bytes static/fonts/StopSN-Display.woff2 | Bin 0 -> 7288 bytes static/style.css | 97 ++++- svelte.config.js | 1 + varnish/Dockerfile | 29 ++ yarn.lock | 35 ++ 83 files changed, 3005 insertions(+), 422 deletions(-) create mode 100644 src/lib/components/ColorInput.svelte create mode 100644 src/lib/components/Dialog.svelte create mode 100644 src/lib/components/Dropdown.svelte create mode 100644 src/lib/components/Error.svelte create mode 100644 src/lib/components/Input.svelte create mode 100644 src/lib/components/JsonViewer.svelte create mode 100644 src/lib/components/LiveImage.svelte create mode 100644 src/lib/components/PageElement.svelte create mode 100644 src/lib/components/Success.svelte create mode 100644 src/lib/components/TableWrapper.svelte create mode 100644 src/lib/components/ThumbnailButton.svelte create mode 100644 src/lib/components/Warning.svelte create mode 100644 src/lib/components/forms/FormFilament.svelte create mode 100644 src/lib/components/kube-describe/DaemonSet.svelte create mode 100644 src/lib/components/kube-describe/Deployment.svelte rename src/{routes/cluster/pod/[uid]/+page.svelte => lib/components/kube-describe/Pod.svelte} (79%) create mode 100644 src/lib/icons/Length.svelte create mode 100644 src/lib/icons/barcore.svelte create mode 100644 src/lib/icons/certificate.svelte create mode 100644 src/lib/icons/external.svelte create mode 100644 src/lib/icons/fingerprint.svelte create mode 100644 src/lib/icons/flower.svelte create mode 100644 src/lib/icons/helm.svelte create mode 100644 src/lib/icons/pencil-ruler.svelte create mode 100644 src/lib/icons/search.svelte create mode 100644 src/lib/icons/speed.svelte create mode 100644 src/lib/icons/sync.svelte create mode 100644 src/lib/icons/time.svelte create mode 100644 src/lib/icons/tuning-fork.svg create mode 100644 src/lib/icons/user.svelte create mode 100644 src/lib/icons/weight.svelte create mode 100644 src/lib/interfaces/health.ts create mode 100644 src/lib/server/database.ts create mode 100644 src/lib/server/health_http.ts create mode 100644 src/lib/utils/hash.ts create mode 100644 src/lib/utils/mouseEvents.ts create mode 100644 src/lib/utils/staticImageSource.ts create mode 100644 src/routes/cluster/[resource]/[uid]/+page.server.ts create mode 100644 src/routes/cluster/[resource]/[uid]/+page.svelte rename src/routes/cluster/{pod => [resource]}/[uid]/logs/+server.ts (100%) delete mode 100644 src/routes/cluster/pod/[uid]/+page.server.ts create mode 100644 src/routes/health/+page.server.ts create mode 100644 src/routes/image/[...catchall]/+server.ts delete mode 100644 src/routes/printer/[id]/+page.server.ts create mode 100644 src/routes/printer/filament/[id]/+page.server.ts rename src/routes/printer/{ => filament}/[id]/+page.svelte (81%) create mode 100644 src/routes/printer/section_image.svelte create mode 100644 src/routes/printer/section_printer_attributes.svelte create mode 100644 static/fonts/NormanVariable-Regular.woff2 create mode 100644 static/fonts/StopSN-Display.woff2 create mode 100644 varnish/Dockerfile diff --git a/.drone.yml b/.drone.yml index 07f92e0..63b154b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -55,6 +55,7 @@ trigger: - pull_request branch: - main + - update depends_on: - Build @@ -117,6 +118,7 @@ trigger: - pull_request branch: - main + - update depends_on: - Build @@ -127,6 +129,6 @@ volumes: temp: {} --- kind: signature -hmac: bea117f5e4b51c4fc215ae86962a8cfb24993d9e9b7db3498ab5940b10c70d69 +hmac: 83b1ec6458ceddef038000faac2b391d192530ceb681b790ad79c90456f12cca ... diff --git a/.kubernetes/deployment.yml b/.kubernetes/deployment.yml index ff68271..c8613e2 100644 --- a/.kubernetes/deployment.yml +++ b/.kubernetes/deployment.yml @@ -21,32 +21,9 @@ spec: - image: ${IMAGE} imagePullPolicy: IfNotPresent name: infra-map - env: - - name: HOMEASSISTANT_TOKEN - valueFrom: - secretKeyRef: - name: secret-env-values - key: HOMEASSISTANT_TOKEN - - name: HOMEASSISTANT_URL - valueFrom: - secretKeyRef: - name: secret-env-values - key: HOMEASSISTANT_URL - - name: PROXMOX_TOKEN - valueFrom: - secretKeyRef: - name: secret-env-values - key: PROXMOX_TOKEN - - name: PROXMOX_URL - valueFrom: - secretKeyRef: - name: secret-env-values - key: PROXMOX_URL - - name: TRAEFIK_URL - valueFrom: - secretKeyRef: - name: secret-env-values - key: TRAEFIK_URL + envFrom: + - secretRef: + name: secret-env-values resources: limits: cpu: 900m diff --git a/Dockerfile b/Dockerfile index bc8d06e..82d5b10 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,4 +20,4 @@ RUN yarn --production EXPOSE 3000 ENV NODE_ENV=production -CMD [ "node", "build" ] +CMD [ "node", "build/index.js" ] diff --git a/README.md b/README.md index b5b2950..f144204 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,61 @@ -# sv +# infra-map -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). +## Configuration setup -## Creating a project +set the following env variables during runtime or update `.env` file. -If you're seeing this, you've probably already done this step. Congrats! +### Kubernetes + +Required parameters: ```bash -# create a new project in the current directory -npx sv create - -# create a new project in my-app -npx sv create my-app +KUBERNETES_SERVICE_HOST=https://IP_ADDRESS:16443 +KUBERNETES_CA_CART_PATH=kube-ca.crt +KUBERNETES_SA_TOKEN=LKdgk34l... ``` +The `KUBERNETES_SERVICE_HOST` is the api server, [microk8s documentation](https://microk8s.io/docs/services-and-ports#services-binding-to-the-default-host-interface) describes that API server is running at port `16443`. Runtime in a pod this will be set by kubernetes. + +Get ca_cert from the any running pod at: `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`. + +### Proxmox + +Required parameters: + +```bash +PROXMOX_URL=https://apollo.schleppe:8086/api2/json/ +PROXMOX_TOKEN=PVEAPITOKEN=USER@pve!USER=TOKEN_VALUE +``` + +`PROXMOX_TOKEN` should contain the sub-variable `PVEAPITOKEN` that describes a proxmox API token. + +Create api token: + +- Create Users + - user name: infra-map + - realm: pve + - expire: never +- Add API tokens + - user: infra-map@pve + - token ID: infra-map +- Permissions + - add: API Token permissions + - path: / + - api-token: infra-map@pve!infra-map + - role: Administrator + - propagate: true + +## Home Assistant + +Required parameters: + +```bash +HOMEASSISTANT_URL=http://homeassistant.schleppe:8123/api/states +HOMEASSISTANT_TOKEN= +``` + +Follow hass documentation on generating a api token: https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token. + ## Developing Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: diff --git a/package.json b/package.json index c4be258..54983c3 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,9 @@ "dependencies": { "@kubernetes/client-node": "^1.1.0", "@microsoft/fetch-event-source": "^2.0.1", + "@zerodevx/svelte-json-view": "^1.0.11", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", "sveltekit-sse": "^0.13.16" } } diff --git a/src/lib/components/ColorInput.svelte b/src/lib/components/ColorInput.svelte new file mode 100644 index 0000000..84ab146 --- /dev/null +++ b/src/lib/components/ColorInput.svelte @@ -0,0 +1,88 @@ + + +
+ {#if label?.length > 0} + + {/if} + +
+
+ + (focus = true)} + on:blur={() => (focus = false)} + /> +
+
+ + diff --git a/src/lib/components/Daemon.svelte b/src/lib/components/Daemon.svelte index e7b7d87..d49aff5 100644 --- a/src/lib/components/Daemon.svelte +++ b/src/lib/components/Daemon.svelte @@ -8,11 +8,12 @@ const healthy = status?.desiredNumberScheduled && status?.desiredNumberScheduled === status?.numberReady; + const daemonUrl = `/cluster/daemonset/${metadata?.uid}`;
-

{pods?.length} of {metadata?.name} in {metadata?.namespace}

+

{pods?.length} of {metadata?.name} in {metadata?.namespace}

heatlthy: {healthy}

@@ -28,7 +29,7 @@ .card-container { background-color: #cab2aa40; border-radius: 0.5rem; - width: 100%; + width: calc(100% - 1.5rem); padding: 0.75rem; .namespace { @@ -38,8 +39,8 @@ .card-wrapper { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 2rem; + grid-template-columns: var(--grid-tmpl-cols, repeat(3, 1fr)); + gap: var(--grid-gap, 2rem); } } diff --git a/src/lib/components/Deploy.svelte b/src/lib/components/Deploy.svelte index 02e5cc3..2a33656 100644 --- a/src/lib/components/Deploy.svelte +++ b/src/lib/components/Deploy.svelte @@ -6,11 +6,16 @@ export let deploy: V1Deployment; let { metadata, pods } = deploy; + + const deploymentUrl = `/cluster/deployment/${metadata?.uid}`;
-

{metadata?.name} in {metadata?.namespace}

+

+ {metadata?.name} in + {metadata?.namespace} +

@@ -34,8 +39,8 @@ .card-wrapper { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 2rem; + grid-template-columns: var(--grid-tmpl-cols, repeat(3, 1fr)); + gap: var(--grid-gap, 2rem); } } diff --git a/src/lib/components/Dialog.svelte b/src/lib/components/Dialog.svelte new file mode 100644 index 0000000..c5367c5 --- /dev/null +++ b/src/lib/components/Dialog.svelte @@ -0,0 +1,174 @@ + + + + + diff --git a/src/lib/components/Dropdown.svelte b/src/lib/components/Dropdown.svelte new file mode 100644 index 0000000..e9bdbf8 --- /dev/null +++ b/src/lib/components/Dropdown.svelte @@ -0,0 +1,114 @@ + + + + + diff --git a/src/lib/components/Error.svelte b/src/lib/components/Error.svelte new file mode 100644 index 0000000..4a0f9e2 --- /dev/null +++ b/src/lib/components/Error.svelte @@ -0,0 +1,23 @@ +
+
+ +
+
+ + diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 074524e..596fa5a 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -1,5 +1,6 @@ + +
+ {#if label?.length > 0} + + {/if} + +
+ {#if icon} + + + + {/if} + + (focus = true)} + on:blur={() => (focus = false)} + /> +
+
+ + diff --git a/src/lib/components/JsonViewer.svelte b/src/lib/components/JsonViewer.svelte new file mode 100644 index 0000000..9a5d64e --- /dev/null +++ b/src/lib/components/JsonViewer.svelte @@ -0,0 +1,32 @@ + + +
+
+ +
+
+ + diff --git a/src/lib/components/LiveImage.svelte b/src/lib/components/LiveImage.svelte new file mode 100644 index 0000000..48d1404 --- /dev/null +++ b/src/lib/components/LiveImage.svelte @@ -0,0 +1,98 @@ + + +
+ {#if !fullscreen} + (fullscreen = !fullscreen)} src={String(imageSource)} id="live-image" /> + {:else} + (fullscreen = false)}> + + + + + {/if} + Last update {timestamp}s ago +
+ + diff --git a/src/lib/components/PageElement.svelte b/src/lib/components/PageElement.svelte new file mode 100644 index 0000000..60f1d06 --- /dev/null +++ b/src/lib/components/PageElement.svelte @@ -0,0 +1,113 @@ + + + + {header} + {icon} +

{title}

+ + {description} + +
+ + diff --git a/src/lib/components/Pod.svelte b/src/lib/components/Pod.svelte index faa4803..e5fd24e 100644 --- a/src/lib/components/Pod.svelte +++ b/src/lib/components/Pod.svelte @@ -3,6 +3,7 @@ import Network from '$lib/icons/network.svelte'; import Layers from '$lib/icons/layers.svelte'; import Clock from '$lib/icons/clock.svelte'; + import Sync from '$lib/icons/sync.svelte'; import { formatDuration } from '$lib/utils/conversion'; import { onMount } from 'svelte'; @@ -67,6 +68,12 @@
{i + 1} of {replicas} +
+ + Restarts +
+ {status?.containerStatuses?.[0].restartCount} +
Running on Node diff --git a/src/lib/components/Section.svelte b/src/lib/components/Section.svelte index 8529e17..ca54297 100644 --- a/src/lib/components/Section.svelte +++ b/src/lib/components/Section.svelte @@ -5,7 +5,10 @@
-

{title}

+
+

{title}

+ +
@@ -25,5 +28,10 @@ .header { display: flex; flex-direction: column; + + .title { + display: flex; + justify-content: space-between; + } } diff --git a/src/lib/components/Server.svelte b/src/lib/components/Server.svelte index e645b4f..f0bc702 100644 --- a/src/lib/components/Server.svelte +++ b/src/lib/components/Server.svelte @@ -11,10 +11,21 @@ import { formatBytes, formatDuration } from '$lib/utils/conversion'; import type { Node } from '$lib/interfaces/proxmox'; import { onMount } from 'svelte'; + import { goto } from '$app/navigation'; + import Speed from '$lib/icons/speed.svelte'; + import Fingerprint from '$lib/icons/fingerprint.svelte'; export let node: Node; - const buttons = ['View logs', 'Web terminal', 'graphs']; + const buttons = [ + { name: 'View logs', link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:25::::::` }, + { + name: 'Terminal', + link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:=jsconsole::::::` + }, + { name: 'Graphs', link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:5::::::` }, + { name: 'Web', link: `https://${node.ip}:8006/` } + ]; let { cpuinfo, memory, uptime, loadavg } = node.info; @@ -23,6 +34,9 @@ const lxcsRunning = node.lxcs.filter((l) => l?.template !== 1 && l.status === 'running'); const lxcsTotal = node.lxcs.filter((l) => l?.template !== 1); + const t = cpuinfo.model.match(/(\w+\(\w+\)) (\w+\(\w+\)) (.*)/); + const cpu = t[3].replaceAll(' ', ' '); + onMount(() => { setInterval(() => (uptime += 1), 1000); }); @@ -51,6 +65,18 @@ >{cpuinfo.cpus} Cores on {cpuinfo.sockets} {cpuinfo.sockets > 1 ? 'Sockets' : 'Socket'} +
+ + Model +
+ {cpu} + +
+ + Turbo speed +
+ {Math.floor(node.info.cpuinfo.mhz) / 1000} GHz +
DDoS protection @@ -90,9 +116,11 @@
@@ -186,7 +214,7 @@ background-color: var(--bg); row-gap: 6px; - column-gap: 20px; + max-width: 330px; > div, span { @@ -203,6 +231,7 @@ .footer { display: flex; align-items: center; + justify-content: space-evenly; flex-wrap: wrap; gap: 0.5rem; diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 418bfcd..564d5ff 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -59,8 +59,12 @@ min-width: var(--nav-width); margin-right: 1rem; - @media screen and (max-width: 700px) { - --nav-width: 100px; + @media screen and (max-width: 1460px) { + --nav-width: 220px; + margin-left: 0.5rem; + } + @media screen and (max-width: 1200px) { + --nav-width: 140px; margin-left: 0.5rem; margin-right: 0; } diff --git a/src/lib/components/Success.svelte b/src/lib/components/Success.svelte new file mode 100644 index 0000000..a121499 --- /dev/null +++ b/src/lib/components/Success.svelte @@ -0,0 +1,23 @@ +
+
+ +
+
+ + diff --git a/src/lib/components/Table.svelte b/src/lib/components/Table.svelte index 6737df4..5ee041b 100644 --- a/src/lib/components/Table.svelte +++ b/src/lib/components/Table.svelte @@ -1,5 +1,9 @@ + + +
+
+ +
+

+ {title || 'Grafana'} + +

+
+
+ + diff --git a/src/lib/components/Warning.svelte b/src/lib/components/Warning.svelte new file mode 100644 index 0000000..8adfdd8 --- /dev/null +++ b/src/lib/components/Warning.svelte @@ -0,0 +1,23 @@ +
+
+ +
+
+ + diff --git a/src/lib/components/forms/FormFilament.svelte b/src/lib/components/forms/FormFilament.svelte new file mode 100644 index 0000000..c9477b7 --- /dev/null +++ b/src/lib/components/forms/FormFilament.svelte @@ -0,0 +1,82 @@ + + +
+
+ + + + + +
+ +
+ + +
+
+ + diff --git a/src/lib/components/kube-describe/DaemonSet.svelte b/src/lib/components/kube-describe/DaemonSet.svelte new file mode 100644 index 0000000..f5a6ffc --- /dev/null +++ b/src/lib/components/kube-describe/DaemonSet.svelte @@ -0,0 +1,98 @@ + + + + + Details + Metadata + Spec + Status + + + +
+
+
+
+ + {status?.currentNumberScheduled} +
+ +
+ + {status?.numberAvailable} +
+ +
+ + {status?.numberReady} +
+ +
+ + {status?.numberMisscheduled} +
+
+
+ +
+
+
+ + {spec?.template?.spec?.containers.length} +
+ +
+ + {spec?.template?.spec?.volumes?.length} +
+ +
+ + {spec?.template?.spec?.restartPolicy} +
+ +
+ + {spec?.template?.spec?.hostNetwork ? 'yes' : 'no'} +
+ +
+ + {spec?.dnsPolicy} +
+
+
+
+
+ + + + + + + + + + + + +
diff --git a/src/lib/components/kube-describe/Deployment.svelte b/src/lib/components/kube-describe/Deployment.svelte new file mode 100644 index 0000000..8fcd459 --- /dev/null +++ b/src/lib/components/kube-describe/Deployment.svelte @@ -0,0 +1,109 @@ + + + + + Details + Metadata + Spec + Status + + + +
+
+
+
+ + {status?.readyReplicas} +
+ +
+ + {status?.availableReplicas} +
+ +
+ + {status?.replicas} +
+
+
+ +
+
+
+ + {metadata?.namespace} +
+ + {#if metadata?.ownerReferences?.length || 0 > 0} + + {/if} +
+
+ +
+
+
+ + {spec?.template?.spec?.containers.length} +
+ +
+ + {spec?.schedulerName} +
+ +
+ + {spec?.hostNetwork ? 'yes' : 'no'} +
+ +
+ + {spec?.dnsPolicy} +
+
+
+
+
+ + + + + + + + + + + + +
diff --git a/src/routes/cluster/pod/[uid]/+page.svelte b/src/lib/components/kube-describe/Pod.svelte similarity index 79% rename from src/routes/cluster/pod/[uid]/+page.svelte rename to src/lib/components/kube-describe/Pod.svelte index 4ef89a7..ea1fcff 100644 --- a/src/routes/cluster/pod/[uid]/+page.svelte +++ b/src/lib/components/kube-describe/Pod.svelte @@ -1,39 +1,33 @@ -Pod: {pod?.metadata?.name} - Details Logs Metadata + Spec + Status Deployment logs @@ -108,9 +108,11 @@ {metadata?.namespace}
-
- - {metadata?.ownerReferences?.[0].kind} +
@@ -156,10 +158,18 @@ - + - + + + + + + + + + diff --git a/src/lib/icons/Length.svelte b/src/lib/icons/Length.svelte new file mode 100644 index 0000000..5657983 --- /dev/null +++ b/src/lib/icons/Length.svelte @@ -0,0 +1,14 @@ + + + + diff --git a/src/lib/icons/barcore.svelte b/src/lib/icons/barcore.svelte new file mode 100644 index 0000000..97ae76b --- /dev/null +++ b/src/lib/icons/barcore.svelte @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/icons/certificate.svelte b/src/lib/icons/certificate.svelte new file mode 100644 index 0000000..d4a2182 --- /dev/null +++ b/src/lib/icons/certificate.svelte @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/src/lib/icons/external.svelte b/src/lib/icons/external.svelte new file mode 100644 index 0000000..7edb441 --- /dev/null +++ b/src/lib/icons/external.svelte @@ -0,0 +1,14 @@ + + + + diff --git a/src/lib/icons/fingerprint.svelte b/src/lib/icons/fingerprint.svelte new file mode 100644 index 0000000..3ce7f88 --- /dev/null +++ b/src/lib/icons/fingerprint.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/src/lib/icons/flower.svelte b/src/lib/icons/flower.svelte new file mode 100644 index 0000000..c791ee5 --- /dev/null +++ b/src/lib/icons/flower.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/icons/helm.svelte b/src/lib/icons/helm.svelte new file mode 100644 index 0000000..b239f5b --- /dev/null +++ b/src/lib/icons/helm.svelte @@ -0,0 +1,14 @@ + + + + diff --git a/src/lib/icons/link.svelte b/src/lib/icons/link.svelte index cdc3cd7..9ec6051 100644 --- a/src/lib/icons/link.svelte +++ b/src/lib/icons/link.svelte @@ -2,8 +2,8 @@ diff --git a/src/lib/icons/pencil-ruler.svelte b/src/lib/icons/pencil-ruler.svelte new file mode 100644 index 0000000..5e2e62f --- /dev/null +++ b/src/lib/icons/pencil-ruler.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/src/lib/icons/search.svelte b/src/lib/icons/search.svelte new file mode 100644 index 0000000..291c4da --- /dev/null +++ b/src/lib/icons/search.svelte @@ -0,0 +1,8 @@ + diff --git a/src/lib/icons/speed.svelte b/src/lib/icons/speed.svelte new file mode 100644 index 0000000..749385b --- /dev/null +++ b/src/lib/icons/speed.svelte @@ -0,0 +1,17 @@ + + + + + + + diff --git a/src/lib/icons/sync.svelte b/src/lib/icons/sync.svelte new file mode 100644 index 0000000..efc5ea3 --- /dev/null +++ b/src/lib/icons/sync.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/src/lib/icons/time.svelte b/src/lib/icons/time.svelte new file mode 100644 index 0000000..9c56b47 --- /dev/null +++ b/src/lib/icons/time.svelte @@ -0,0 +1,14 @@ + + + + diff --git a/src/lib/icons/tuning-fork.svg b/src/lib/icons/tuning-fork.svg new file mode 100644 index 0000000..00331ad --- /dev/null +++ b/src/lib/icons/tuning-fork.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/lib/icons/user.svelte b/src/lib/icons/user.svelte new file mode 100644 index 0000000..1879a40 --- /dev/null +++ b/src/lib/icons/user.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/icons/weight.svelte b/src/lib/icons/weight.svelte new file mode 100644 index 0000000..7e68918 --- /dev/null +++ b/src/lib/icons/weight.svelte @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/src/lib/interfaces/health.ts b/src/lib/interfaces/health.ts new file mode 100644 index 0000000..e8df03d --- /dev/null +++ b/src/lib/interfaces/health.ts @@ -0,0 +1,11 @@ +export enum HEALTH_STATUS { + LIVE = 'live', + UNKNOWN = 'unknown', + DOWN = 'down' +} + +export interface HttpEndpoint { + domain: string; + code: number; + status: HEALTH_STATUS.DOWN; +} diff --git a/src/lib/interfaces/printer.ts b/src/lib/interfaces/printer.ts index 019a63c..561ddf2 100644 --- a/src/lib/interfaces/printer.ts +++ b/src/lib/interfaces/printer.ts @@ -1,8 +1,10 @@ export interface Filament { - Hex: string; - Color: string; - Material: string; - Weight: string; - Count: number; - Link: string; + hex: string; + color: string; + material: string; + weight: string; + count: number; + link: string; + created: number; + updated: number; } diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts new file mode 100644 index 0000000..a76241f --- /dev/null +++ b/src/lib/server/database.ts @@ -0,0 +1,135 @@ +import { currentFilament } from './filament'; + +import { open } from 'sqlite'; +import sqlite3 from 'sqlite3'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import type { Filament } from '$lib/interfaces/printer'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const dbPath = resolve(__dirname, '../../../db.sqlite'); + +let db; + +async function initDb() { + const db = await open({ + filename: dbPath, + driver: sqlite3.Database + }); + + // Transaction to run schemas + await db.exec('BEGIN TRANSACTION'); + try { + for (const stmt of schemas) { + await db.run(stmt); + } + await db.exec('COMMIT'); + } catch (err) { + console.error('Failed to create tables:', err.message); + await db.exec('ROLLBACK'); + } + + return db; +} + +const schemas = [ + ` + CREATE TABLE IF NOT EXISTS filament ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hex TEXT NOT NULL, + color TEXT NOT NULL, + material TEXT, + weight REAL, + link TEXT, + added INTEGER, -- epoch seconds + updated INTEGER -- epoch seconds + ) + ` +]; + +async function seedData(db) { + const baseTimestamp = Math.floor(new Date('2025-04-01T05:47:01+00:00').getTime() / 1000); + const filaments = currentFilament(); + + const stmt = await db.prepare(` + INSERT OR IGNORE INTO filament (hex, color, material, weight, link, added, updated) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + + await db.exec('BEGIN TRANSACTION'); + try { + for (const f of filaments) { + const existing = await db.get('SELECT 1 FROM filament WHERE hex = ? AND updated = ?', [ + f.hex, + baseTimestamp + ]); + + if (!existing) { + await db.run( + `INSERT INTO filament (hex, color, material, weight, link, added, updated) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [f.hex, f.color, f.material, f.weight, f.link, baseTimestamp, baseTimestamp] + ); + } + } + + await db.exec('COMMIT'); + } catch (err) { + console.error('Failed to seed data:', err.message); + await db.exec('ROLLBACK'); + } finally { + await stmt.finalize(); + } +} + +// Export helper to use db elsewhere +async function getDb() { + if (db !== undefined) return db; + + db = await initDb(); + await seedData(db); + console.log('Database setup and seeding complete!'); +} + +export async function getAllFilament(): Promise> { + const db = await getDb(); + const result = await db?.all('SELECT * FROM filament'); + return result || []; +} + +export async function getFilamentByColor(name: string) { + const db = await getDb(); + const result = await db?.get('SELECT * FROM filament WHERE LOWER(color) = ?', [name]); + return result || undefined; +} + +export async function addFilament( + hex: string, + color: string, + material: string, + weight: number, + link: string +) { + const timestamp = Math.floor(new Date().getTime() / 1000); + + const db = await getDb(); + const result = await db.run( + `INSERT INTO filament (hex, color, material, weight, link, added, updated) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [hex, color, material, weight, link, timestamp, timestamp] + ); + return { id: result.lastID }; +} + +export async function updatefilament({ id, make, model, year }) { + const db = await getDb(); + await db.run( + 'UPDATE filaments SET make = ?, model = ?, year = ? WHERE id = ?', + make, + model, + year, + id + ); +} diff --git a/src/lib/server/health_http.ts b/src/lib/server/health_http.ts new file mode 100644 index 0000000..b17580d --- /dev/null +++ b/src/lib/server/health_http.ts @@ -0,0 +1,54 @@ +import { request, Agent } from 'https'; +import tls, { type PeerCertificate } from 'tls'; + +const SSL_WEBSERVER = '10.0.0.53'; +export async function getSSLInfo(url: string, port = 443) { + if (new URL(url).protocol !== 'https:') return { raw: 'none' }; + const hostname = new URL(url).hostname; + + return new Promise((resolve, reject) => { + const socket = tls.connect(port, SSL_WEBSERVER, { servername: hostname }, () => { + const cert = socket.getPeerCertificate(true); + + if (!cert || Object.keys(cert).length === 0) { + reject(new Error('No certificate found')); + return; + } + + resolve({ + subject: cert.subject, + issuer: cert.issuer, + valid_from: cert.valid_from, + valid_to: cert.valid_to, + fingerprint: cert.fingerprint, + fingerprint256: cert.fingerprint256, + ca: cert.ca, + nistCurve: cert.nistCurve, + asn1Curve: cert.asn1Curve, + serialNumber: cert.serialNumber, + altNames: cert.subjectaltname, + publicKey: cert?.pubkey?.toString('base64') || '', + infoAccess: cert?.infoAccess || '' + }); + + socket.end(); + }); + + socket.on('error', (err) => { + reject(err); + }); + }); +} + +export async function healthOk(url: string): Promise { + return fetch(url, { signal: AbortSignal.timeout(400) }) + .then((resp) => { + return resp.status; + }) + .catch((error) => { + console.log('got error from health endpoint for url:', url); + console.log(error); + + return 550; + }); +} diff --git a/src/lib/server/homeassistant.ts b/src/lib/server/homeassistant.ts index 6877e54..3f28e93 100644 --- a/src/lib/server/homeassistant.ts +++ b/src/lib/server/homeassistant.ts @@ -16,19 +16,70 @@ function buildHomeassistantRequest() { return { url, options }; } +const attributes = { + current_stage: null, + print_status: null, + bed_temperature: null, + nozzle_temperature: null, + total_usage: null, + nozzle_type: null, + nozzle_size: null, + print_bed_type: null, + current_layer: null, + total_layer_count: null, + print_progress: null, + print_length: null, + print_weight: null, + sd_card_status: null, + speed_profile: null, + wi_fi_signal: null, + end_time: null, + cover_image: null, + pick_image: null, + camera: null +}; + +interface PrinterState { + [key: string]: { + value: string; + unit?: string; + picture?: string; + }; +} + +function printerState(data: object) { + const state: PrinterState = {}; + + const keys = Object.keys(attributes); + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + const value = data?.filter((el) => el.entity_id.includes(k))[0]; + + if (!value) continue; + state[k] = { value: value.state }; + + if (value?.attributes?.unit_of_measurement) + state[k]['unit'] = value.attributes.unit_of_measurement; + + if (value?.attributes?.entity_picture) state[k]['picture'] = value.attributes.entity_picture; + } + + return state; +} + async function fetchHassStates() { const { url, options } = buildHomeassistantRequest(); return fetch(url, options).then((resp) => resp.json()); } -export async function fetchP1P(): Promise { +export async function fetchP1P(): Promise { try { let hassStates = await fetchHassStates(); hassStates = hassStates.filter( (el: Entity) => el.attributes.friendly_name?.includes('P1P') === true ); - return hassStates; + return printerState(hassStates); } catch (error) { console.log('ERROR! from fetchP1P:', error); return Promise.reject(null); diff --git a/src/lib/server/kubernetes.ts b/src/lib/server/kubernetes.ts index 1ea3eec..33fd0a7 100644 --- a/src/lib/server/kubernetes.ts +++ b/src/lib/server/kubernetes.ts @@ -1,9 +1,46 @@ import * as k8s from '@kubernetes/client-node'; import stream from 'stream'; import { writable } from 'svelte/store'; +import fs from 'fs'; +import { env } from '$env/dynamic/private'; + +/* +const kubeCaPath = + env.KUBERNETES_CA_CERT_PATH || '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'; +const kubeCaCert = fs.readFileSync(kubeCaPath, 'utf8'); + +// const kubeSaTokenPath = env.KUBERNETES_SA_TOKEN_PATH || '/var/run/secrets/kubernetes.io/serviceaccount/token'; +const token = fs.readFileSync(kubeSaTokenPath, 'utf8'); +*/ + +const kubeConfig: k8s.KubeConfig = { + clusters: [ + { + name: 'kazan', + server: env.KUBERNETES_SERVICE_HOST || 'https://kubernetes.default.svc', + // caData: kubeCaCert, + // skipTLSVerify: true + skipTLSVerify: true + } + ], + users: [ + { + name: 'pod-user', + token: env.KUBERNETES_SA_TOKEN + } + ], + contexts: [ + { + name: 'default-context', + user: 'pod-user', + cluster: 'kazan' + } + ], + currentContext: 'default-context' +}; const kc = new k8s.KubeConfig(); -kc.loadFromDefault(); +kc.loadFromOptions(kubeConfig); const k8sApi = kc.makeApiClient(k8s.CoreV1Api); const appsV1Api = kc.makeApiClient(k8s.AppsV1Api); @@ -92,12 +129,16 @@ export function createLogStream(podName: string, namespace: string, containerNam }); console.log('setting logAbortController, prev:', logAbortController); - logAbortController = await k8sLog.log(namespace, podName, containerName, liveStream, { - follow: true, - timestamps: false, - pretty: false, - tailLines: maxLines - }); + try { + logAbortController = await k8sLog.log(namespace, podName, containerName, liveStream, { + follow: true, + timestamps: false, + pretty: false, + tailLines: maxLines + }); + } catch (error) { + console.log('ERROR SETTING UP WS', error); + } } function stop() { diff --git a/src/lib/server/proxmox.ts b/src/lib/server/proxmox.ts index 4d9b258..c47a4c3 100644 --- a/src/lib/server/proxmox.ts +++ b/src/lib/server/proxmox.ts @@ -2,8 +2,8 @@ import { env } from '$env/dynamic/private'; import type { Cluster, Node } from '$lib/interfaces/proxmox'; function buildProxmoxRequest() { - const url = env.PROXMOX_URL || 'https://10.0.0.50:8006/api2/json/'; - const token = env.PROXMOX_TOKEN || 'REPLACE_WITH_PROXMOX_TOKEN'; + const url = env.PROXMOX_URL; + const token = env.PROXMOX_TOKEN; const options = { method: 'GET', headers: { diff --git a/src/lib/server/traefik.ts b/src/lib/server/traefik.ts index c355fe6..ddfc745 100644 --- a/src/lib/server/traefik.ts +++ b/src/lib/server/traefik.ts @@ -3,7 +3,7 @@ import { env } from '$env/dynamic/private'; const TRAEFIK_HTTP_URL = '/api/http'; function buildTraefikRequest(path: string) { - const baseURL = env.TRAEFIK_URL || 'http://localhost:9000'; + const baseURL = env.TRAEFIK_URL; const url = `${baseURL}${TRAEFIK_HTTP_URL}/${path}`; const options = { method: 'GET', @@ -12,6 +12,7 @@ function buildTraefikRequest(path: string) { } }; + console.log('making traefik request', url); return { url, options }; } diff --git a/src/lib/utils/conversion.ts b/src/lib/utils/conversion.ts index e2b90f1..902c641 100644 --- a/src/lib/utils/conversion.ts +++ b/src/lib/utils/conversion.ts @@ -26,6 +26,20 @@ export function formatDuration(seconds: number) { return `${days} days ${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(Math.floor(seconds)).padStart(2, '0')}`; } +export function daysUntil(dateString: string) { + const inputDate = new Date(dateString); + const today = new Date(); + + // Clear time components for accurate day comparison + inputDate.setHours(0, 0, 0, 0); + today.setHours(0, 0, 0, 0); + + const diffTime = inputDate - today; + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + return diffDays; +} + export function convertKiToHumanReadable(input: string) { const match = input.match(/^(\d+)(Ki)$/); if (!match) return 'Invalid input'; @@ -44,3 +58,38 @@ export function convertKiToHumanReadable(input: string) { return `${humanReadable.toFixed(2)} ${sizes[i]}`; } + +export function formatTimeLeft(seconds: number, short = false) { + const units = [ + { label: 'mo', value: 2592000 }, // 30 days as an average month + { label: 'd', value: 86400 }, + { label: 'h', value: 3600 }, + { label: 'm', value: 60 }, + { label: 's', value: 1 } + ]; + + let remaining = seconds; + const parts = []; + + for (const unit of units) { + if (remaining >= unit.value) { + const amount = Math.floor(remaining / unit.value); + remaining %= unit.value; + parts.push(`${amount}${unit.label}`); + } + } + + if (short) return parts.slice(' ')[0]; + + // If 0 seconds, still return "0s" + return parts.length > 0 ? parts.join(' ') : '0s'; +} + +export function formatDateIntl(d: Date) { + return new Intl.DateTimeFormat('nb-NO', { + year: 'numeric', + month: 'long', + day: 'numeric', + weekday: 'short' + }).format(d); +} diff --git a/src/lib/utils/hash.ts b/src/lib/utils/hash.ts new file mode 100644 index 0000000..60fed15 --- /dev/null +++ b/src/lib/utils/hash.ts @@ -0,0 +1,22 @@ +const cyrb64 = (str: string, seed = 0) => { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + // For a single 53-bit numeric return value we could return + // 4294967296 * (2097151 & h2) + (h1 >>> 0); + // but we instead return the full 64-bit value: + return [h2 >>> 0, h1 >>> 0]; +}; + +export const digest = (str: string, seed = 0) => { + const [h2, h1] = cyrb64(str, seed); + return h2.toString(36).padStart(7, '0') + h1.toString(36).padStart(7, '0'); +}; diff --git a/src/lib/utils/mouseEvents.ts b/src/lib/utils/mouseEvents.ts new file mode 100644 index 0000000..8c20986 --- /dev/null +++ b/src/lib/utils/mouseEvents.ts @@ -0,0 +1,14 @@ +export function clickOutside(event: MouseEvent, element: Element | undefined) { + if (!element) return false; + + const rect = element.getBoundingClientRect(); + if (!rect) return false; + + const isClickOutside = + event.clientX < rect.left || + event.clientX > rect.right || + event.clientY < rect.top || + event.clientY > rect.bottom; + + return isClickOutside ? true : false; +} diff --git a/src/lib/utils/staticImageSource.ts b/src/lib/utils/staticImageSource.ts new file mode 100644 index 0000000..0951c8a --- /dev/null +++ b/src/lib/utils/staticImageSource.ts @@ -0,0 +1,2 @@ +export const grey400x225 = + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCADhAZADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3aiiipKCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z'; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 50eaa51..9f2f49e 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -30,8 +30,19 @@ main { /* mobile: 1rem */ - margin: 2rem; - width: 100%; + --margin: 2rem; + margin: var(--margin); + width: calc(100% - var(--margin) * 2); + } + + @media screen and (max-width: 750px) { + main { + --margin: 1rem; + } + + :global(> .nav-wrapper) { + display: none; + } } } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7710d9c..8aeceea 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,82 +1,124 @@ -Welcome to SvelteKit -

Visit svelte.dev/docs/kit to read the documentation

+Welcome to schleppe.cloud infra overview

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. + This site is a local-first dashboard for monitoring the state of digital and physical tools in a + workshop environment. It currently tracks servers (IP, cores, memory, uptime), 3D printers + (status, history, filament stock), and other connected devices. Each device or system has its own + page with relevant real-time and historical information. More modules are planned, including + general monitoring tools, IoT integrations, and project overviews.

+

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus - repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur - ipsum voluptatum sunt, atque magni minus. + The system is intended for hybrid spaces where digital infrastructure coexists with hands-on work. + Alongside real-time monitoring, Schleppe is expanding to reflect the broader physical + workspace—covering areas like tool usage, material stocks, and workstations for welding, + woodworking, electronics, and leathercraft. The goal is to make the state of the entire + workshop—both virtual and physical—easily visible in one place.

+ +
+ {#each elems as shortcut (shortcut.title)} + + {/each} +
+ + diff --git a/src/routes/cluster/+page.svelte b/src/routes/cluster/+page.svelte index 3b36ee7..f473587 100644 --- a/src/routes/cluster/+page.svelte +++ b/src/routes/cluster/+page.svelte @@ -3,18 +3,28 @@ import Deploy from '$lib/components/Deploy.svelte'; import Daemon from '$lib/components/Daemon.svelte'; import PageHeader from '$lib/components/PageHeader.svelte'; + import Input from '$lib/components/Input.svelte'; import type { PageData } from './$types'; import type { V1DaemonSet, V1Deployment, V1Node } from '@kubernetes/client-node'; let { data }: { data: PageData } = $props(); + let filterValue = $state(''); - const deployments: V1Deployment[] = data?.deployments; - const daemons: V1DaemonSet[] = data?.daemons; - const nodes: V1Node[] = data?.nodes; + const rawDeployments: V1Deployment[] = data?.deployments; + const rawDaemons: V1DaemonSet[] = data?.daemons; + const rawNodes: V1Node[] = data?.nodes; + + let deployments = $derived(rawDeployments.filter((d) => d.metadata.name.includes(filterValue))); + let daemons = $derived(rawDaemons.filter((d) => d.metadata.name.includes(filterValue))); + let nodes = $derived(rawNodes.filter((n) => n.metadata.name.includes(filterValue))); Cluster overview +
+ +
+

Cluster {nodes.length} nodes

@@ -65,12 +75,27 @@
diff --git a/src/routes/cluster/[resource]/[uid]/+page.server.ts b/src/routes/cluster/[resource]/[uid]/+page.server.ts new file mode 100644 index 0000000..ee2a445 --- /dev/null +++ b/src/routes/cluster/[resource]/[uid]/+page.server.ts @@ -0,0 +1,59 @@ +import type { PageServerLoad } from './$types'; +import { getPods, getDaemons, getReplicas, getDeployments } from '$lib/server/kubernetes'; +import type { V1DaemonSet, V1Deployment, V1Pod } from '@kubernetes/client-node'; + +const AVAILABLE_RESOURCES = [ + 'pod', + 'deployment', + 'daemonset', + 'cronjobs', + 'configmap', + 'replicaset' +]; + +export const load: PageServerLoad = async ({ params }) => { + const { resource, uid } = params; + console.log('PARAMS:', params); + + if (!AVAILABLE_RESOURCES.includes(resource)) { + return { + error: 'No resource ' + resource, + resource: null + }; + } + console.log(uid); + + let resources: V1Pod[]; + + switch (resource) { + case 'pod': + const podsResp: V1Pod[] = await getPods(); + resources = JSON.parse(JSON.stringify(podsResp)); + break; + case 'daemonset': + const daemonsResp: V1DaemonSet[] = await getDaemons(); + resources = JSON.parse(JSON.stringify(daemonsResp)); + break; + case 'deployment': + const deploymentResp: V1Deployment[] = await getDeployments(); + resources = JSON.parse(JSON.stringify(deploymentResp)); + break; + case 'replicaset': + console.log('replicas'); + const replicasResp: V1ReplicaSet[] = await getReplicas(); + console.log('replicas', replicasResp); + resources = JSON.parse(JSON.stringify(replicasResp)); + break; + default: + console.log('no resources found'); + } + + const singleResource = resources?.find((p) => p.metadata?.uid === uid); + delete singleResource?.metadata?.managedFields; + + return { + resource: singleResource, + kind: resource, + error: null + }; +}; diff --git a/src/routes/cluster/[resource]/[uid]/+page.svelte b/src/routes/cluster/[resource]/[uid]/+page.svelte new file mode 100644 index 0000000..e9777a8 --- /dev/null +++ b/src/routes/cluster/[resource]/[uid]/+page.svelte @@ -0,0 +1,32 @@ + + +{kind || 'Resource'}: {resource?.metadata?.name || 'not found'} + +{#if error} +

{error}

+{/if} + +{#if resource} + {#if kind == 'pod'} + + {:else if kind == 'deployment' || kind == 'replicaset'} + + {:else if kind == 'daemonset'} + + {:else} + + {/if} +{:else} +

404. '{kind}' resource not found!

+{/if} diff --git a/src/routes/cluster/pod/[uid]/logs/+server.ts b/src/routes/cluster/[resource]/[uid]/logs/+server.ts similarity index 100% rename from src/routes/cluster/pod/[uid]/logs/+server.ts rename to src/routes/cluster/[resource]/[uid]/logs/+server.ts diff --git a/src/routes/cluster/pod/[uid]/+page.server.ts b/src/routes/cluster/pod/[uid]/+page.server.ts deleted file mode 100644 index 3218f20..0000000 --- a/src/routes/cluster/pod/[uid]/+page.server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { getPods } from '$lib/server/kubernetes'; -import type { V1Pod } from '@kubernetes/client-node'; - -export const load: PageServerLoad = async ({ params }) => { - const { uid } = params; - - console.log(uid); - - const podsResp = await getPods(); - const pods: V1Pod[] = JSON.parse(JSON.stringify(podsResp)); - - const pod = pods.find((p) => p.metadata?.uid === uid); - return { - pod - }; -}; diff --git a/src/routes/health/+page.server.ts b/src/routes/health/+page.server.ts new file mode 100644 index 0000000..e948034 --- /dev/null +++ b/src/routes/health/+page.server.ts @@ -0,0 +1,30 @@ +import type { PageServerLoad } from './$types'; +import { env } from '$env/dynamic/private'; +import { getSSLInfo, healthOk } from '$lib/server/health_http'; +import { HEALTH_STATUS, type HttpEndpoint } from '$lib/interfaces/health'; + +const ENDPOINTS: string[] = env?.HTTP_HEALTH_ENDPOINTS?.split(','); + +export const load: PageServerLoad = async (): Promise<{ httpHealth: HttpEndpoint[] }> => { + const statusPromises = ENDPOINTS?.map(async (endpointUrl) => { + const status = await healthOk(endpointUrl); + const ssl = await getSSLInfo(endpointUrl); + + return { status, ssl }; + }); + const endpointStatuses = await Promise.all(statusPromises); + + const httpHealth = ENDPOINTS.map((domain, i) => { + return { + domain: new URL(domain).hostname, + code: endpointStatuses[i].status, + ssl: endpointStatuses[i].ssl, + status: + String(endpointStatuses[i].status)[0] !== '5' ? HEALTH_STATUS.LIVE : HEALTH_STATUS.DOWN + } as HttpEndpoint; + }); + + return { + httpHealth + }; +}; diff --git a/src/routes/health/+page.svelte b/src/routes/health/+page.svelte index d9840fe..7ebff01 100644 --- a/src/routes/health/+page.svelte +++ b/src/routes/health/+page.svelte @@ -1,13 +1,17 @@ - Health @@ -15,6 +19,36 @@ + columns={['Domain', 'SSL', 'Status', 'Code']} +> + + {#each httpHealth as row, i (row)} + + + + + + + {/each} + +
{row.domain} + {#if row['ssl']['valid_to']} +
(selectedSSL = row['ssl'])} + style="display: flex; cursor: pointer;" + > +
+ +
+ {daysUntil(row['ssl']['valid_to'])} days left +
+ {:else} + (none) + {/if} +
{row.status}{row.code}
+ +{#if selectedSSL !== null} + (selectedSSL = null)} title="SSL Certificate info"> + + +{/if} diff --git a/src/routes/image/[...catchall]/+server.ts b/src/routes/image/[...catchall]/+server.ts new file mode 100644 index 0000000..9b2a504 --- /dev/null +++ b/src/routes/image/[...catchall]/+server.ts @@ -0,0 +1,37 @@ +import type { RequestHandler } from './$types'; + +async function fetchImage(src: string) { + // const url = new URL(src); + const remoteUrl = String(src.match(/http:\/\/[a-z0-9\\.]+:8123\/.*/)); + const url = new URL(remoteUrl); + + const options = { + method: 'GET', + headers: { + 'Content-Type': 'image/jpeg' + } + }; + + if (url === null) { + console.log('url is not valid'); + return null; + } + + return fetch(url.href, options) + .then((resp) => resp.blob()) + .catch(() => null); +} + +export const GET: RequestHandler = async ({ url }) => { + console.log('GET'); + url.pathname = url.pathname.replace('/image/', ''); + + const res = await fetchImage(url.href); + + // something went wrong + if (res === null) { + return new Response(null, { status: 204 }); + } + + return new Response(res); +}; diff --git a/src/routes/network/+page.svelte b/src/routes/network/+page.svelte index bd74ae1..8b44e23 100644 --- a/src/routes/network/+page.svelte +++ b/src/routes/network/+page.svelte @@ -1,4 +1,5 @@ Network @@ -42,7 +32,24 @@
- +
+ + {#each routers as route (route)} + goto(`/network/${route.service}`)} class="link"> + + + + + + + + {/each} + +
{route.entryPoints}{route.name}{route.provider}{route.rule}{route.service}{route.status}
diff --git a/src/routes/printer/[id]/+page.server.ts b/src/routes/printer/[id]/+page.server.ts deleted file mode 100644 index a1051b1..0000000 --- a/src/routes/printer/[id]/+page.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { filamentByColor } from '$lib/server/filament'; - -export const load: PageServerLoad = async ({ params }) => { - let { id } = params; - if (id) { - id = id.replaceAll('-', ' '); - } - - const filament = filamentByColor(id); - return { id, filament }; -}; diff --git a/src/routes/printer/filament/[id]/+page.server.ts b/src/routes/printer/filament/[id]/+page.server.ts new file mode 100644 index 0000000..434b26e --- /dev/null +++ b/src/routes/printer/filament/[id]/+page.server.ts @@ -0,0 +1,13 @@ +import type { PageServerLoad } from './$types'; +import { getFilamentByColor } from '$lib/server/database'; + +export const load = async ({ params }: Parameters[0]) => { + let { id } = params; + if (id) { + id = id.replaceAll('-', ' '); + } + + const filament = await getFilamentByColor(id); + console.log('fil:', filament); + return { id, filament: filament }; +}; diff --git a/src/routes/printer/[id]/+page.svelte b/src/routes/printer/filament/[id]/+page.svelte similarity index 81% rename from src/routes/printer/[id]/+page.svelte rename to src/routes/printer/filament/[id]/+page.svelte index dc77ee6..7c8eb76 100644 --- a/src/routes/printer/[id]/+page.svelte +++ b/src/routes/printer/filament/[id]/+page.svelte @@ -6,11 +6,11 @@ const filament = data?.filament; -{#if filament !== null} - Filament: {filament?.Color} +{#if filament != null} + Filament: {filament?.color}
-
+
{:else} Filament not found! diff --git a/src/routes/printer/section_image.svelte b/src/routes/printer/section_image.svelte new file mode 100644 index 0000000..cbfcead --- /dev/null +++ b/src/routes/printer/section_image.svelte @@ -0,0 +1,39 @@ + + +
+
+
+

Camera

+ +
+
+

Model

+ +
+ +
+

Pick image

+ +
+
+
+ + diff --git a/src/routes/printer/section_printer_attributes.svelte b/src/routes/printer/section_printer_attributes.svelte new file mode 100644 index 0000000..93a9dd4 --- /dev/null +++ b/src/routes/printer/section_printer_attributes.svelte @@ -0,0 +1,77 @@ + + +
+
+
+ + + {Math.floor(Number(data['total_usage']?.value) * 10) / 10} + + {data['total_usage']?.unit} +
+ +
+ + + {Math.floor(Number(data['print_length']?.value) * 10) / 10} + {data['print_length']?.unit} +
+ +
+ + {capitalizeFirstLetter(data?.['nozzle_type']?.value?.replaceAll('_', ' '))} + {data['nozzle_type']?.unit} +
+ +
+ + {data['nozzle_size']?.value} {data['nozzle_size']?.unit} +
+ +
+ + {capitalizeFirstLetter(data['print_bed_type']?.value?.replaceAll('_', ' ') || 'not found')} + {data['print_bed_type']?.unit} +
+ +
+ + {data['sd_card_status']?.value} + {data['sd_card_status']?.unit} +
+ +
+ + {data['wi_fi_signal']?.value} {data['wi_fi_signal']?.unit} +
+
+ +
+ +
+
+ + diff --git a/src/routes/sites/+page.svelte b/src/routes/sites/+page.svelte index 620a8a9..83c97a0 100644 --- a/src/routes/sites/+page.svelte +++ b/src/routes/sites/+page.svelte @@ -1,11 +1,64 @@ - Sites
+ {#each sites as site} + + {/each} +
+ + diff --git a/static/fonts/NormanVariable-Regular.woff2 b/static/fonts/NormanVariable-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..06756f029eb7f5dd61c6828af907773fae915fc6 GIT binary patch literal 45808 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 literal 0 HcmV?d00001 diff --git a/static/fonts/StopSN-Display.woff2 b/static/fonts/StopSN-Display.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..5c0e5fb99df5f50b291c8e4afcfc5e00c5df6f5b GIT binary patch literal 7288 zcmV-;9Eam~Pew9NR8&s@033J#3;+NC06A;`030a*0RR9100000000000000000000 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 SV|HW_$xHA}JU({4B!U4fpw~kH literal 0 HcmV?d00001 diff --git a/static/style.css b/static/style.css index fbe37df..195ef0c 100644 --- a/static/style.css +++ b/static/style.css @@ -1,3 +1,9 @@ +@font-face { + font-family: 'Reckless Neue'; + font-style: normal; + src: url('/fonts/RecklessNeue-Regular.woff2') format('woff2'); +} + @font-face { font-family: 'Inter'; font-style: normal; @@ -6,6 +12,22 @@ font-style: normal; } +@font-face { + font-family: 'Stop'; + font-style: normal; + src: url('/fonts/StopSN-Display.woff2') format('woff2'); + font-weight: 100 900; + font-style: normal; +} + +@font-face { + font-family: 'Norman'; + font-style: normal; + src: url('/fonts/NormanVariable-Regular.woff2') format('woff2'); + font-weight: 100 900; + font-style: normal; +} + :root { --bg: #f9f5f3; --color: #1c1819; @@ -37,7 +59,10 @@ a:visited { } h1 { - font-family: 'Reckless Neue'; + font-family: 'Stop', 'Reckless Neue'; + font-family: 'Norman', 'Reckless Neue'; + letter-spacing: 2.2px; + font-weight: bold !important; } h2 { @@ -69,27 +94,43 @@ button { border: none; position: relative; background: transparent; - height: unset; + height: 100%; border-radius: 0.5rem; display: inline-block; text-decoration: none; + cursor: pointer; padding: 0 0.5rem; flex: 1; } + button span { display: inline-flex; align-items: center; justify-content: center; width: 100%; - height: 1.5rem; + height: 100%; + min-height: 1.5rem; padding: 0 0.5rem; margin-left: -0.5rem; border: 1px solid #eaddd5; border-radius: inherit; white-space: nowrap; - cursor: pointer; font-weight: 700; transition: all ease-in-out 0.2s; + background-color: white; +} + +button.affirmative span { + background-color: #1c1819; + color: white; +} + +button.affirmative:hover span { + background-color: #363132; +} + +button:disabled { + cursor: not-allowed; } button::after { @@ -107,7 +148,49 @@ button::after { button:hover span { border-color: #cab2aa; - background: #f9f5f3 !important; + background: #f9f5f3; +} + +table { + width: 100%; + border-collapse: collapse; + font-family: sans-serif; + border-radius: 8px; + overflow: hidden; +} + +table th, +table td { + padding: 12px; + text-align: left; + transition: background-color 0.25s; +} + +table th { + font-weight: 500; + font-family: 'Inter', sans-serif; + font-size: 14px; + font-stretch: 2px; + border-bottom: 1px solid #eaddd5; +} + +table td { + padding-top: 1rem; + padding-bottom: 1rem; + display: table-cell; +} + +table tr:not(table tr:last-of-type) { + border-bottom: 1px solid #eaddd5; +} + +table tr:hover > td { + background-color: var(--highlight); + background-color: #f5ede9; +} + +table tr.link { + cursor: pointer; } .main-container { @@ -126,7 +209,9 @@ button:hover span { .section-row { display: flex; - gap: 2rem; + flex-wrap: wrap; + column-gap: 2rem; + row-gap: 1rem; } .section-element { diff --git a/svelte.config.js b/svelte.config.js index 2b3caf3..0ce7863 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,4 +1,5 @@ import adapter from '@sveltejs/adapter-node'; +// import adapter from "svelte-adapter-bun"; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ diff --git a/varnish/Dockerfile b/varnish/Dockerfile new file mode 100644 index 0000000..742775a --- /dev/null +++ b/varnish/Dockerfile @@ -0,0 +1,29 @@ +FROM debian:bullseye + +# Install dependencies +RUN apt-get update && apt-get install -y \ + curl gnupg apt-transport-https lsb-release \ + build-essential git autoconf libtool python3-docutils \ + libmhash-dev pkg-config + +# Add Varnish Software repo +RUN curl -fsSL https://packagecloud.io/varnishcache/varnish73/gpgkey | gpg --dearmor -o /usr/share/keyrings/varnish.gpg && \ + echo "deb [signed-by=/usr/share/keyrings/varnish.gpg] https://packagecloud.io/varnishcache/varnish73/debian/ $(lsb_release -cs) main" \ + > /etc/apt/sources.list.d/varnish.list + +# Install Varnish + dev headers +RUN apt-get update && apt-get install -y varnish varnish-dev + +# Build libvmod-digest +RUN git clone https://github.com/varnish/libvmod-digest.git /opt/libvmod-digest && \ + cd /opt/libvmod-digest && \ + ./autogen.sh && \ + ./configure VARNISHSRC=/usr/include/varnish && \ + make && make install + +COPY default.vcl /etc/varnish/default.vcl + +EXPOSE 6081 + +CMD ["varnishd", "-F", "-f", "/etc/varnish/default.vcl", "-a", ":6081", "-s", "malloc,512m"] + diff --git a/yarn.lock b/yarn.lock index 26fa6c9..6ba744e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -685,6 +685,11 @@ "@typescript-eslint/types" "8.29.1" eslint-visitor-keys "^4.2.0" +"@zerodevx/svelte-json-view@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@zerodevx/svelte-json-view/-/svelte-json-view-1.0.11.tgz#39d33dd066f5442ad58b5dffcf8f958a6b80e4ea" + integrity sha512-mIjj0H1al/P4FPlbeDoiey93lNEUqBEAe5LIdD5GttZfEYt3awexD2lHwKNfUeY4jHizOJkoWTPN/2iO0GBqpw== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1224,6 +1229,16 @@ globals@^14.0.0: resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== +globalyzer@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" + integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -1963,6 +1978,11 @@ sprintf-js@^1.1.3: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== +sqlite@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/sqlite/-/sqlite-5.1.1.tgz#26a6a200fdac490643880af2b6cb3940ada59274" + integrity sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q== + stream-buffers@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.3.tgz#9fc6ae267d9c4df1190a781e011634cac58af3cd" @@ -1992,6 +2012,13 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svelte-adapter-bun@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/svelte-adapter-bun/-/svelte-adapter-bun-0.5.2.tgz#92c9984bb1555454e35e7732196cbf1cd5cedf40" + integrity sha512-xEtFgaal6UgrCwwkSIcapO9kopoFNUYCYqyKCikdqxX9bz2TDYnrWQZ7qBnkunMxi1HOIERUCvTcebYGiarZLA== + dependencies: + tiny-glob "^0.2.9" + svelte-check@^4.0.0: version "4.1.5" resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-4.1.5.tgz#afdb3f8050c123064124d5aa7821365c7befa7a4" @@ -2064,6 +2091,14 @@ tar@^7.0.0: mkdirp "^3.0.1" yallist "^5.0.0" +tiny-glob@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" + integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== + dependencies: + globalyzer "0.1.0" + globrex "^0.1.2" + tmp-promise@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7"