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 0000000..06756f0 Binary files /dev/null and b/static/fonts/NormanVariable-Regular.woff2 differ diff --git a/static/fonts/StopSN-Display.woff2 b/static/fonts/StopSN-Display.woff2 new file mode 100644 index 0000000..5c0e5fb Binary files /dev/null and b/static/fonts/StopSN-Display.woff2 differ 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"