source, static files & Dockerfile

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

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

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

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

@@ -0,0 +1,82 @@
<script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte';
</script>
<PageHeader>Welcome to SvelteKit</PageHeader>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Pariatur laboriosam, necessitatibus
repudiandae quo voluptatem ratione, sit eum dolores enim earum at officia expedita consequuntur
ipsum voluptatum sunt, atque magni minus.
</p>

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
import type { PageServerLoad } from './$types';
import { getPods } from '$lib/server/kubernetes';
import type { V1Pod } from '@kubernetes/client-node';
export const load: PageServerLoad = async ({ params }) => {
const { uid } = params;
console.log(uid);
const podsResp = await getPods();
const pods: V1Pod[] = JSON.parse(JSON.stringify(podsResp));
const pod = pods.find((p) => p.metadata?.uid === uid);
return {
pod
};
};

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
<script>
import PageHeader from '$lib/components/PageHeader.svelte';
import Table from '$lib/components/Table.svelte';
let columns = ['Domain', 'Status'];
let data = [
{ Domain: 'laravel-ucm1d.kinsta.app', Status: 'Live' },
{ Domain: 'laravel-ucm1d.kinsta.app', Status: 'Live' },
{ Domain: 'laravel-ucm1d.kinsta.app', Status: 'Live' }
];
</script>
<PageHeader>Health</PageHeader>
<Table
title="Domains list"
description="All of the verified domains below point to this application and are covered by free Cloudflare SSL certificates for a secure HTTPS connection. The DNS records for the domains must be set up correctly for them to work."
{columns}
{data}
/>

View File

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

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte';
import Section from '$lib/components/Section.svelte';
import Table from '$lib/components/Table.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
const { routers } = data;
const columns = {
entryPoints: 'Entrypoints',
name: 'Name',
provider: 'Provider',
rule: 'Rule',
service: 'Service',
status: 'Status'
};
const links: string[] = routers.map((router) => `/network/${router.service}`);
const providers = [
...new Set(
routers.map((item) => item.provider).filter((provider) => typeof provider === 'string')
)
];
console.log(routers);
</script>
<PageHeader>Network</PageHeader>
<div class="section-wrapper">
<Section title="Traefik" description="Treafik is a network proxy and webserver.">
<div class="section-row">
<div class="section-element">
<label>Number of routers</label>
<span>{routers.length}</span>
</div>
<div class="section-element">
<label>Providers</label>
<span>{providers?.join(', ')}</span>
</div>
</div>
</Section>
<Table title="Routers" description="Traefik routers available" {columns} data={routers} {links} />
</div>
<style lang="scss">
.server-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: left;
gap: 2rem;
}
</style>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,237 @@
<script lang="ts">
import { capitalizeFirstLetter } from '$lib/utils/string';
import PageHeader from '$lib/components/PageHeader.svelte';
import Section from '$lib/components/Section.svelte';
import Table from '$lib/components/Table.svelte';
import Progress from '$lib/components/Progress.svelte';
import Finished from '$lib/icons/finished.svelte';
import Paused from '$lib/icons/paused.svelte';
import Stopped from '$lib/icons/stopped.svelte';
import Printing from '$lib/icons/printing.svelte';
import PrinterIdle from '$lib/icons/printer-idle.svelte';
import PrinterPaused from '$lib/icons/printer-paused.svelte';
import PrinterPrinting from '$lib/icons/printer-printing.svelte';
import PrinterStopped from '$lib/icons/printer-stopped.svelte';
import NozzleTemperature from '$lib/icons/temperature-nozzle.svelte';
import BedTemperature from '$lib/icons/temperature-bed.svelte';
import type { PageData } from './$types';
import type { Entity } from '$lib/interfaces/homeassistant';
import type { Filament } from '$lib/interfaces/printer';
let { data }: { data: PageData } = $props();
const p1p: Entity[] = data?.p1p;
const filament: Filament[] = data?.filament;
const currentStage = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_current_stage'
)[0];
const printStatus = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_print_status'
)[0];
const bedTemp = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_bed_temperature'
)[0];
const nozzleTemp = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_nozzle_temperature'
)[0];
const totalUsage = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_total_usage'
)[0];
const nozzleType = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_nozzle_type'
)[0];
const nozzleSize = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_nozzle_size'
)[0];
const bedType = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_print_bed_type'
)[0];
const currentLayer = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_current_layer'
)[0];
const totalLayer = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_total_layer_count'
)[0];
const progress = p1p.filter(
(el) => el.entity_id === 'sensor.p1p_01s00c370700273_print_progress'
)[0];
console.log(p1p);
let columns = ['Hex', 'Color', 'Material', 'Weight', 'Count', 'Link'];
const links = filament.map((f) => `/printer/${f.Color.replaceAll(' ', '-').toLowerCase()}`);
const iconDictStage = {
idle: PrinterIdle,
printing: PrinterPrinting,
paused: PrinterPaused,
stopped: PrinterStopped,
heatbed_preheating: PrinterPrinting,
cleaning_nozzle_tip: PrinterPrinting,
homing_toolhead: PrinterPrinting
};
const iconDictState = { running: Printing, pause: Paused, failed: Stopped, finish: Finished };
interface FilamentUpdated {
date: Date;
title?: string;
}
const lastUpdateFilament: FilamentUpdated = {
date: new Date('2025-04-01T05:47:01+00:00')
};
lastUpdateFilament.title = lastUpdateFilament.date
.toLocaleDateString('en-US', {
weekday: 'long',
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
.toLowerCase();
</script>
<PageHeader>Printer</PageHeader>
<div class="section-wrapper">
<Section
title="Printer status"
description="Historical printer information, last prints and current status."
>
<div class="section-row">
<div class="section-element">
<label>Current stage</label>
<span
><span class="icon">
<svelte:component this={iconDictStage[currentStage.state]} />
</span>{currentStage.state}</span
>
</div>
<div class="section-element">
<label>Bed temperature</label>
<span
><span class="icon"><BedTemperature /></span>{bedTemp.state}
{bedTemp.attributes.unit_of_measurement}</span
>
</div>
<div class="section-element">
<label>Nozzle temperature</label>
<span
><span class="icon"><NozzleTemperature /></span>{nozzleTemp.state}
{nozzleTemp.attributes.unit_of_measurement}</span
>
</div>
<div class="section-element">
<label>Print status</label>
<span>
<span class={`icon ${printStatus?.state === 'running' ? 'spin' : ''}`}>
<svelte:component this={iconDictState[printStatus.state]} /></span
>
{printStatus.state}
</span>
</div>
</div>
<div class="progress">
<Progress value={progress.state} />
{#if currentLayer.state !== totalLayer.state}
<span>Currently printing layer line {currentLayer.state} of {totalLayer.state}</span>
{:else}
<span>Finished printing {currentLayer.state} of {totalLayer.state} layers!</span>
{/if}
</div>
</Section>
<Section
title="Printer attributes"
description="Historical printer information, last prints and current status."
>
<div class="section-row">
<div class="section-element">
<label>Total print time</label>
<span>
{Math.floor(Number(totalUsage.state) * 10) / 10}
<!-- {formatDuration(totalUsage.state * 3600)} -->
{totalUsage.attributes.unit_of_measurement}</span
>
</div>
<div class="section-element">
<label>Nozzle Type</label>
<span
>{capitalizeFirstLetter(nozzleType.state.replaceAll('_', ' '))}
{nozzleType.attributes.unit_of_measurement}</span
>
</div>
<div class="section-element">
<label>Nozzle Size</label>
<span>{nozzleSize?.state} {nozzleSize.attributes.unit_of_measurement}</span>
</div>
<div class="section-element">
<label>Bed type</label>
<span
>{capitalizeFirstLetter(bedType?.state?.replaceAll('_', ' ') || 'not found')}
{bedType?.attributes.unit_of_measurement}</span
>
</div>
</div>
<img src="/printer.png" />
</Section>
<Table
title="Filaments"
description={`${filament.length} colors are currently in stock. Overview of currently stocked filament.`}
{columns}
data={filament}
{links}
footer={`Last updated on ${lastUpdateFilament.title}`}
/>
</div>
<style lang="scss">
img {
width: 120px;
position: absolute;
top: 1.2rem;
right: 1.2rem;
}
.section-element {
.icon {
display: inline-block;
--size: 2rem;
height: var(--size);
width: var(--size);
padding-right: 0.5rem;
&.spin {
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
animation: rotate 6s linear infinite;
transform-origin: calc((var(--size) / 2) - 2px) calc(var(--size) / 2);
}
}
}
.progress {
display: flex;
flex-direction: column;
width: 100%;
span {
margin-top: 0.5rem;
}
}
</style>

View File

@@ -0,0 +1,12 @@
import type { PageServerLoad } from './$types';
import { filamentByColor } from '$lib/server/filament';
export const load: PageServerLoad = async ({ params }) => {
let { id } = params;
if (id) {
id = id.replaceAll('-', ' ');
}
const filament = filamentByColor(id);
return { id, filament };
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
<script>
import PageHeader from '$lib/components/PageHeader.svelte';
import Section from '$lib/components/Section.svelte';
</script>
<PageHeader>Sites</PageHeader>
<div class="section-wrapper">
<Section
title="Expose HTTP traffic"
description="You can reach your Application on a specific Port you configure, redirecting all your domains to it. You can make it Private by disabling HTTP traffic."
/>
<Section
title="IP restrictions"
description="Restrict or block access to your application based on specific IP addresses or CIDR blocks."
/>
<Section
title="Expose HTTP traffic"
description="You can reach your Application on a specific Port you configure, redirecting all your domains to it. You can make it Private by disabling HTTP traffic."
/>
<Section
title="Connected services"
description="Connected services can communicate with your application over the private network."
/>
</div>