mirror of
https://github.com/KevinMidboe/infra-map.git
synced 2025-12-08 20:29:05 +00:00
working nice. docker uses bun
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,82 +1,124 @@
|
||||
<script lang="ts">
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import PageElement from '$lib/components/PageElement.svelte';
|
||||
|
||||
let elems = [];
|
||||
let counter = 0;
|
||||
let colors = [
|
||||
['#401C26', '#f6cfdd'],
|
||||
['#213726', '#BDCBB2'],
|
||||
['#EED7CD', '#262221'],
|
||||
['#262221', '#F3BFA2'],
|
||||
['#f6cfdd', '#401C26'],
|
||||
['#BDCBB2', '#213726'],
|
||||
['#FF8FAB', '#401C26'],
|
||||
['#9381FF', '#262221']
|
||||
];
|
||||
const descriptions = [
|
||||
'Røroshotellene, eid av Røros Hotell AS, er fire unike hoteller og spisesteder på Røros. Med røtter i lokalsamfunnet tilbyr vi autentiske opplevelser, enten du vil bo komfortabelt eller nyte lokal mat',
|
||||
'Faglige møter blir enda bedre med frisk høstluft og unike omgivelser. Kampanjepris fra 2115,-',
|
||||
'Planlegg et minneverdig bedriftsarrangement hos oss!',
|
||||
'Lei hele Erzscheidergården for en utforstyrret ramme til ditt neste møtested!'
|
||||
];
|
||||
|
||||
function createPageElement(title: string, description = '', header = '') {
|
||||
if (counter + 1 >= colors.length) counter = 1;
|
||||
else counter += 1;
|
||||
|
||||
console.log(counter);
|
||||
|
||||
return {
|
||||
bgColor: colors[counter - 1][0],
|
||||
color: colors[counter - 1][1],
|
||||
title,
|
||||
header: null,
|
||||
description: description ? description : '',
|
||||
link: title
|
||||
};
|
||||
}
|
||||
|
||||
elems = elems.concat(createPageElement('sites'));
|
||||
elems = elems.concat(createPageElement('servers', 'Overview of proxmox servers'));
|
||||
elems = elems.concat(
|
||||
createPageElement(
|
||||
'printer',
|
||||
'Realtime information on P1P printer and filament overview with current & historical rolls.'
|
||||
)
|
||||
);
|
||||
elems = elems.concat(
|
||||
createPageElement(
|
||||
'network',
|
||||
'View traefik configuration & all defined routes, services & middlewares.'
|
||||
)
|
||||
);
|
||||
elems = elems.concat(
|
||||
createPageElement(
|
||||
'cluster',
|
||||
'View running resources in Kubernetes cluster. View nodes, daemonset & deployments; and get a pods realtime logs, resource usage & view related kubernetes resources.'
|
||||
)
|
||||
);
|
||||
elems = elems.concat(createPageElement('health'));
|
||||
|
||||
elems = elems.concat(createPageElement('cluster '));
|
||||
elems = elems.concat(createPageElement('health '));
|
||||
</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>
|
||||
<PageHeader>Welcome to schleppe.cloud infra overview</PageHeader>
|
||||
|
||||
<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.
|
||||
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.
|
||||
</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.
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="shortcut-grid">
|
||||
{#each elems as shortcut (shortcut.title)}
|
||||
<PageElement
|
||||
bgColor={shortcut.bgColor}
|
||||
color={shortcut.color}
|
||||
title={shortcut.title}
|
||||
header={shortcut.header}
|
||||
description={shortcut.description}
|
||||
link={shortcut.link}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.4;
|
||||
line-height: 1.7;
|
||||
max-width: 80%;
|
||||
color: #333;
|
||||
|
||||
background-color: #fafafa; /* Subtle background to separate it from the rest */
|
||||
padding: 2rem;
|
||||
border-radius: 1rem; /* Soft edges */
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); /* Light shadow for depth */
|
||||
}
|
||||
|
||||
.shortcut-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
||||
gap: 1.2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
:global(.shortcut-grid .shortcut:nth-of-type(odd) h2, .shortcut-grid a:nth-of-type(odd) h2) {
|
||||
font-weight: 600;
|
||||
letter-spacing: 2.6px;
|
||||
font-family: 'Norman' !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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)));
|
||||
</script>
|
||||
|
||||
<PageHeader>Cluster overview</PageHeader>
|
||||
|
||||
<div class="search-section">
|
||||
<Input label="Filter resources" placeholder="Search by name" bind:value={filterValue} />
|
||||
</div>
|
||||
|
||||
<details open>
|
||||
<summary>
|
||||
<h2>Cluster <span>{nodes.length} nodes</span></h2>
|
||||
@@ -65,12 +75,27 @@
|
||||
</details>
|
||||
|
||||
<style lang="scss">
|
||||
.search-section {
|
||||
padding: 1.714rem 0px;
|
||||
}
|
||||
|
||||
.server-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
--grid-tmpl-cols: repeat(2, 1fr);
|
||||
--grid-gap: 0.5rem;
|
||||
grid-template-columns: var(--grid-tmpl-cols, repeat(2, 1fr));
|
||||
gap: var(--grid-gap, 0.5rem);
|
||||
|
||||
margin-bottom: 2rem;
|
||||
|
||||
@media screen and (min-width: 750px) {
|
||||
--grid-tmpl-cols: repeat(2, 1fr);
|
||||
--grid-gap: 1.25rem;
|
||||
}
|
||||
@media screen and (min-width: 1200px) {
|
||||
--grid-tmpl-cols: repeat(3, 1fr);
|
||||
--grid-gap: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.server-list.deploys {
|
||||
@@ -88,7 +113,8 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h2 {
|
||||
:global(.server-list h2) {
|
||||
font-family: 'Reckless Neue';
|
||||
justify-content: unset !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
59
src/routes/cluster/[resource]/[uid]/+page.server.ts
Normal file
59
src/routes/cluster/[resource]/[uid]/+page.server.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
32
src/routes/cluster/[resource]/[uid]/+page.svelte
Normal file
32
src/routes/cluster/[resource]/[uid]/+page.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import PodDescribe from '$lib/components/kube-describe/Pod.svelte';
|
||||
import DeploymentDescribe from '$lib/components/kube-describe/Deployment.svelte';
|
||||
import DaemonSetDescribe from '$lib/components/kube-describe/DaemonSet.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import type { V1Pod } from '@kubernetes/client-node';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const { error, kind } = data;
|
||||
const { resource }: { pod: V1Pod | undefined } = data;
|
||||
</script>
|
||||
|
||||
<PageHeader>{kind || 'Resource'}: {resource?.metadata?.name || 'not found'}</PageHeader>
|
||||
|
||||
{#if error}
|
||||
<p>{error}</p>
|
||||
{/if}
|
||||
|
||||
{#if resource}
|
||||
{#if kind == 'pod'}
|
||||
<PodDescribe pod={resource} />
|
||||
{:else if kind == 'deployment' || kind == 'replicaset'}
|
||||
<DeploymentDescribe deployment={resource} />
|
||||
{:else if kind == 'daemonset'}
|
||||
<DaemonSetDescribe daemonset={resource} />
|
||||
{:else}
|
||||
<PodDescribe pod={resource} />
|
||||
{/if}
|
||||
{:else}
|
||||
<h2>404. '{kind}' resource not found!</h2>
|
||||
{/if}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -1,165 +0,0 @@
|
||||
<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>
|
||||
30
src/routes/health/+page.server.ts
Normal file
30
src/routes/health/+page.server.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -1,13 +1,17 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Dialog from '$lib/components/Dialog.svelte';
|
||||
import JsonViewer from '$lib/components/JsonViewer.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Table from '$lib/components/Table.svelte';
|
||||
import Certificate from '$lib/icons/certificate.svelte';
|
||||
import type { HttpEndpoint } from '$lib/interfaces/health';
|
||||
import { daysUntil } from '$lib/utils/conversion';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
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' }
|
||||
];
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const httpHealth: HttpEndpoint[] = data?.httpHealth;
|
||||
let selectedSSL = $state(null);
|
||||
</script>
|
||||
|
||||
<PageHeader>Health</PageHeader>
|
||||
@@ -15,6 +19,36 @@
|
||||
<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}
|
||||
/>
|
||||
columns={['Domain', 'SSL', 'Status', 'Code']}
|
||||
>
|
||||
<tbody slot="tbody">
|
||||
{#each httpHealth as row, i (row)}
|
||||
<tr>
|
||||
<td>{row.domain}</td>
|
||||
<td>
|
||||
{#if row['ssl']['valid_to']}
|
||||
<div
|
||||
on:click={() => (selectedSSL = row['ssl'])}
|
||||
style="display: flex; cursor: pointer;"
|
||||
>
|
||||
<div style="height: 1.4rem; width: 1.4rem;">
|
||||
<Certificate />
|
||||
</div>
|
||||
<span>{daysUntil(row['ssl']['valid_to'])} days left</span>
|
||||
</div>
|
||||
{:else}
|
||||
<span>(none)</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>{row.status}</td>
|
||||
<td>{row.code}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
{#if selectedSSL !== null}
|
||||
<Dialog on:close={() => (selectedSSL = null)} title="SSL Certificate info">
|
||||
<JsonViewer json={selectedSSL} />
|
||||
</Dialog>
|
||||
{/if}
|
||||
|
||||
37
src/routes/image/[...catchall]/+server.ts
Normal file
37
src/routes/image/[...catchall]/+server.ts
Normal file
@@ -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);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import Table from '$lib/components/Table.svelte';
|
||||
@@ -7,22 +8,11 @@
|
||||
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>
|
||||
@@ -42,7 +32,24 @@
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Table title="Routers" description="Traefik routers available" {columns} data={routers} {links} />
|
||||
<Table
|
||||
title="Routers"
|
||||
description="Traefik routers available"
|
||||
columns={['Entrypoints', 'Name', 'Provider', 'Rule', 'Service', 'Status']}
|
||||
>
|
||||
<tbody slot="tbody">
|
||||
{#each routers as route (route)}
|
||||
<tr on:click={() => goto(`/network/${route.service}`)} class="link">
|
||||
<td>{route.entryPoints}</td>
|
||||
<td>{route.name}</td>
|
||||
<td>{route.provider}</td>
|
||||
<td>{route.rule}</td>
|
||||
<td>{route.service}</td>
|
||||
<td>{route.status}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import JsonViewer from '$lib/components/JsonViewer.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
@@ -9,7 +10,4 @@
|
||||
|
||||
<PageHeader>Network: {router.service}</PageHeader>
|
||||
|
||||
<div>
|
||||
<p>router:</p>
|
||||
<pre><code>{JSON.stringify(router, null, 2)}</code></pre>
|
||||
</div>
|
||||
<JsonViewer json={router} />
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
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 { getAllFilament } from '$lib/server/database';
|
||||
import type { Filament } from '$lib/interfaces/printer';
|
||||
|
||||
export const load: PageServerLoad = async (): Promise<{ p1p: Entity[]; filament: Filament[] }> => {
|
||||
const p1p = await fetchP1P();
|
||||
const filament = currentFilament();
|
||||
interface PrinterState {
|
||||
[key: string]: {
|
||||
value: string;
|
||||
unit?: string;
|
||||
picture?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async (): Promise<{
|
||||
p1p: PrinterState;
|
||||
filament: Filament[];
|
||||
}> => {
|
||||
let p1p;
|
||||
let filament: Filament[];
|
||||
|
||||
try {
|
||||
p1p = await fetchP1P();
|
||||
filament = await getAllFilament();
|
||||
} catch (error) {
|
||||
console.error('error while fetching printer server props');
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return { p1p, filament };
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { capitalizeFirstLetter } from '$lib/utils/string';
|
||||
import { formatTimeLeft } from '$lib/utils/conversion';
|
||||
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 Input from '$lib/components/Input.svelte';
|
||||
import Dialog from '$lib/components/Dialog.svelte';
|
||||
import FormFilament from '$lib/components/forms/FormFilament.svelte';
|
||||
|
||||
import Finished from '$lib/icons/finished.svelte';
|
||||
import Paused from '$lib/icons/paused.svelte';
|
||||
@@ -15,53 +18,55 @@
|
||||
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 Search from '$lib/icons/search.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import type { Entity } from '$lib/interfaces/homeassistant';
|
||||
import type { Filament } from '$lib/interfaces/printer';
|
||||
import Weight from '$lib/icons/weight.svelte';
|
||||
import Speed from '$lib/icons/speed.svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import Time from '$lib/icons/time.svelte';
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import PrinterImage from './section_image.svelte';
|
||||
import PrinterAttributes from './section_printer_attributes.svelte';
|
||||
import { formatDateIntl } from '$lib/utils/conversion';
|
||||
import Length from '$lib/icons/Length.svelte';
|
||||
|
||||
interface PrinterState {
|
||||
[key: string]: {
|
||||
value: string;
|
||||
unit?: string;
|
||||
picture?: string;
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const p1p: Entity[] = data?.p1p;
|
||||
const filament: Filament[] = data?.filament;
|
||||
let printer: PrinterState = $state(data?.p1p);
|
||||
let filamentFilter = $state('');
|
||||
let secondsLeft = $state(0);
|
||||
let open = $state(false);
|
||||
let timeLeftInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
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];
|
||||
const rawFilament: Filament[] = data?.filament || [];
|
||||
let filament = $derived(
|
||||
rawFilament
|
||||
?.filter(
|
||||
(f: Filament) =>
|
||||
f.color.toLowerCase().includes(filamentFilter) ||
|
||||
f.material.toLowerCase().includes(filamentFilter)
|
||||
)
|
||||
.sort((a, b) => (a.updated > b.updated ? -1 : 1))
|
||||
);
|
||||
|
||||
console.log(p1p);
|
||||
function reloadProps() {
|
||||
invalidateAll().then(() => (printer = data?.p1p));
|
||||
}
|
||||
|
||||
let columns = ['Hex', 'Color', 'Material', 'Weight', 'Count', 'Link'];
|
||||
const links = filament.map((f) => `/printer/${f.Color.replaceAll(' ', '-').toLowerCase()}`);
|
||||
// console.log(p1p);
|
||||
|
||||
const filamentLink = (f: Filament) =>
|
||||
`/printer/filament/${f.color.replaceAll(' ', '-').toLowerCase()}`;
|
||||
|
||||
const iconDictState = { running: Printing, pause: Paused, failed: Stopped, finish: Finished };
|
||||
const iconDictStage = {
|
||||
idle: PrinterIdle,
|
||||
printing: PrinterPrinting,
|
||||
@@ -71,7 +76,16 @@
|
||||
cleaning_nozzle_tip: PrinterPrinting,
|
||||
homing_toolhead: PrinterPrinting
|
||||
};
|
||||
const iconDictState = { running: Printing, pause: Paused, failed: Stopped, finish: Finished };
|
||||
|
||||
function isObjKey<T>(key: PropertyKey, obj: T): key is keyof T {
|
||||
return key in obj;
|
||||
}
|
||||
|
||||
const stateToIcon = (key: string) => {
|
||||
if (!isObjKey(key, iconDictStage)) return;
|
||||
|
||||
return iconDictStage[key];
|
||||
};
|
||||
|
||||
interface FilamentUpdated {
|
||||
date: Date;
|
||||
@@ -88,6 +102,32 @@
|
||||
year: 'numeric'
|
||||
})
|
||||
.toLowerCase();
|
||||
|
||||
function updateTimeLeft() {
|
||||
if (secondsLeft <= 0) {
|
||||
clearInterval(timeLeftInterval);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = new Date(printer['end_time']?.value).getTime() - now.getTime();
|
||||
secondsLeft = Math.max(Math.floor(diffMs / 1000), 0);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// only poll status updates if not idle
|
||||
if (printer['print_status']?.value === 'idle') return;
|
||||
updateTimeLeft();
|
||||
|
||||
timeLeftInterval = setInterval(updateTimeLeft, 1000);
|
||||
const refreshStateInterval = setInterval(reloadProps, 5000);
|
||||
|
||||
return () =>
|
||||
Promise.all([clearInterval(timeLeftInterval), clearInterval(refreshStateInterval)]);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(timeLeftInterval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<PageHeader>Printer</PageHeader>
|
||||
@@ -97,110 +137,145 @@
|
||||
title="Printer status"
|
||||
description="Historical printer information, last prints and current status."
|
||||
>
|
||||
<div slot="top-left">
|
||||
<button on:click={reloadProps}><span>Reload</span></button>
|
||||
</div>
|
||||
|
||||
<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
|
||||
<svelte:component this={stateToIcon(printer['current_stage']?.value || '')} />
|
||||
</span>{printer['current_stage']?.value}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Bed temperature</label>
|
||||
<label>Bed temp</label>
|
||||
<span
|
||||
><span class="icon"><BedTemperature /></span>{bedTemp.state}
|
||||
{bedTemp.attributes.unit_of_measurement}</span
|
||||
><span class="icon"><BedTemperature /></span>{printer['bed_temperature']?.value}
|
||||
{printer['bed_temperature']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Nozzle temperature</label>
|
||||
<label>Nozzle temp</label>
|
||||
<span
|
||||
><span class="icon"><NozzleTemperature /></span>{nozzleTemp.state}
|
||||
{nozzleTemp.attributes.unit_of_measurement}</span
|
||||
><span class="icon"><NozzleTemperature /></span>{printer['nozzle_temperature']?.value}
|
||||
{printer['nozzle_temperature']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Speed profile</label>
|
||||
<span
|
||||
><span class="icon"><Speed /></span>{printer['speed_profile']?.value}
|
||||
{printer['speed_profile']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Print weight</label>
|
||||
<span
|
||||
><span class="icon" style="--size: 1.8rem"><Weight /></span>{printer['print_weight']
|
||||
?.value}
|
||||
{printer['print_weight']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Print length</label>
|
||||
<span
|
||||
><span class="icon"><Length /></span>{printer['print_length']?.value}
|
||||
{printer['print_length']?.unit}</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
|
||||
<span class={`icon ${printer['print_status']?.value === 'running' ? 'spin' : ''}`}>
|
||||
<svelte:component this={iconDictState[printer['print_status']?.value]} /></span
|
||||
>
|
||||
{printStatus.state}
|
||||
{printer['print_status']?.value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Time left</label>
|
||||
<span><span class="icon"><Time /></span>{formatTimeLeft(secondsLeft)}</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>
|
||||
<Progress value={printer['print_progress']?.value} />
|
||||
|
||||
{#if printer['current_layer']?.value !== printer['total_layer_count']?.value}
|
||||
<span
|
||||
>Currently printing layer line {printer['current_layer']?.value} of {printer[
|
||||
'total_layer_count'
|
||||
]?.value}</span
|
||||
>
|
||||
{:else}
|
||||
<span>Finished printing {currentLayer.state} of {totalLayer.state} layers!</span>
|
||||
<span
|
||||
>Finished printing {printer['current_layer']?.value} of {printer['total_layer_count']
|
||||
?.value} 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>
|
||||
<PrinterImage data={printer} />
|
||||
|
||||
<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>
|
||||
<PrinterAttributes data={printer} />
|
||||
|
||||
<Table
|
||||
title="Filaments"
|
||||
description={`${filament.length} colors are currently in stock. Overview of currently stocked filament.`}
|
||||
{columns}
|
||||
columns={['Color', 'Details', 'Last bought']}
|
||||
data={filament}
|
||||
{links}
|
||||
footer={`Last updated on ${lastUpdateFilament.title}`}
|
||||
/>
|
||||
>
|
||||
<div slot="actions" class="filament-table-inputs">
|
||||
<div>
|
||||
<Input placeholder="Filter filaments" icon={Search} bind:value={filamentFilter} />
|
||||
</div>
|
||||
|
||||
<button class="affirmative" on:click={() => (open = true)}><span>Add new</span></button>
|
||||
</div>
|
||||
|
||||
<tbody slot="tbody">
|
||||
{#each filament as row, i (row)}
|
||||
<tr class="link" on:click={() => goto(filamentLink(row))}>
|
||||
<td><span class="color" style={`background: ${row.hex}`} /></td>
|
||||
<td class="info">
|
||||
<h2>{row.material} in {row.color}</h2>
|
||||
<div class="meta">
|
||||
<span>Roll:	 {row.weight}</span>
|
||||
<span>Color:	 {row.hex}</span>
|
||||
<span>Last bought:	 {formatTimeLeft(row.updated / 1000)}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>{formatDateIntl(new Date(row.updated * 1000))}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
img {
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
top: 1.2rem;
|
||||
right: 1.2rem;
|
||||
}
|
||||
{#if open}
|
||||
<Dialog
|
||||
on:close={() => (open = false)}
|
||||
title="Add new filament"
|
||||
description="You can select anything deployed in <b>Belgium (europe-west1) datacenter</b> and create an internal connection with your service."
|
||||
>
|
||||
<FormFilament on:close={() => (open = false)} />
|
||||
</Dialog>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.section-element {
|
||||
.icon {
|
||||
display: inline-block;
|
||||
@@ -234,4 +309,58 @@
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.filament-table-inputs {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
> div {
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
> button {
|
||||
flex: unset;
|
||||
height: 2.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* filament table */
|
||||
tr td {
|
||||
&:first-of-type {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
&.info {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
padding-left: 25px;
|
||||
|
||||
h2 {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 0.45em;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 300;
|
||||
color: #1c1b1b;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
flex-direction: column;
|
||||
color: #6a6a6a;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
.color {
|
||||
--size: 4rem;
|
||||
display: block;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: var(--border-radius, 1rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
13
src/routes/printer/filament/[id]/+page.server.ts
Normal file
13
src/routes/printer/filament/[id]/+page.server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getFilamentByColor } from '$lib/server/database';
|
||||
|
||||
export const load = async ({ params }: Parameters<PageServerLoad>[0]) => {
|
||||
let { id } = params;
|
||||
if (id) {
|
||||
id = id.replaceAll('-', ' ');
|
||||
}
|
||||
|
||||
const filament = await getFilamentByColor(id);
|
||||
console.log('fil:', filament);
|
||||
return { id, filament: filament };
|
||||
};
|
||||
@@ -6,11 +6,11 @@
|
||||
const filament = data?.filament;
|
||||
</script>
|
||||
|
||||
{#if filament !== null}
|
||||
<PageHeader>Filament: {filament?.Color}</PageHeader>
|
||||
{#if filament != null}
|
||||
<PageHeader>Filament: {filament?.color}</PageHeader>
|
||||
|
||||
<div class="page">
|
||||
<div class="color-block" style={`background: ${filament?.Hex}`}></div>
|
||||
<div class="color-block" style={`background: ${filament?.hex}`}></div>
|
||||
</div>
|
||||
{:else}
|
||||
<PageHeader>Filament not found!</PageHeader>
|
||||
39
src/routes/printer/section_image.svelte
Normal file
39
src/routes/printer/section_image.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import LiveImage from '$lib/components/LiveImage.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
|
||||
let { data }: { data: any } = $props();
|
||||
</script>
|
||||
|
||||
<Section
|
||||
title="Current print"
|
||||
description="Historical printer information, last prints and current status."
|
||||
>
|
||||
<div class="images">
|
||||
<div>
|
||||
<h2>Camera</h2>
|
||||
<LiveImage imageUrl={`http://homeassistant.schleppe:8123${data['camera']?.picture}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Model</h2>
|
||||
<img src={`http://homeassistant.schleppe:8123${data['cover_image']?.picture}`} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Pick image</h2>
|
||||
<img src={`http://homeassistant.schleppe:8123${data['pick_image']?.picture}`} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<style lang="scss">
|
||||
.images {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
img {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
77
src/routes/printer/section_printer_attributes.svelte
Normal file
77
src/routes/printer/section_printer_attributes.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import { capitalizeFirstLetter } from '$lib/utils/string';
|
||||
|
||||
let { data }: { data: any } = $props();
|
||||
</script>
|
||||
|
||||
<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(data['total_usage']?.value) * 10) / 10}
|
||||
<!-- {formatDuration(totalUsage.value * 3600)} -->
|
||||
{data['total_usage']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Total print length</label>
|
||||
<span>
|
||||
{Math.floor(Number(data['print_length']?.value) * 10) / 10}
|
||||
{data['print_length']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Nozzle Type</label>
|
||||
<span
|
||||
>{capitalizeFirstLetter(data?.['nozzle_type']?.value?.replaceAll('_', ' '))}
|
||||
{data['nozzle_type']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Nozzle Size</label>
|
||||
<span>{data['nozzle_size']?.value} {data['nozzle_size']?.unit}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Bed type</label>
|
||||
<span
|
||||
>{capitalizeFirstLetter(data['print_bed_type']?.value?.replaceAll('_', ' ') || 'not found')}
|
||||
{data['print_bed_type']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>SD Card status</label>
|
||||
<span
|
||||
>{data['sd_card_status']?.value}
|
||||
{data['sd_card_status']?.unit}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>WiFi signal</label>
|
||||
<span>{data['wi_fi_signal']?.value} {data['wi_fi_signal']?.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="printer-image">
|
||||
<img src="/printer.png" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<style lang="scss">
|
||||
.printer-image img {
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
top: 1.2rem;
|
||||
right: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,64 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import ThumbnailButton from '$lib/components/ThumbnailButton.svelte';
|
||||
|
||||
interface Site {
|
||||
title: string;
|
||||
image: string;
|
||||
link: string;
|
||||
background?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const sites: Array<Site> = [
|
||||
{
|
||||
title: 'Grafana',
|
||||
image:
|
||||
'https://www.stackhero.io/assets/src/images/servicesLogos/openGraphVersions/grafana.png',
|
||||
link: 'https://grafana.schleppe.cloud',
|
||||
background: '#082448',
|
||||
color: 'white'
|
||||
},
|
||||
{
|
||||
title: 'Prometheus',
|
||||
image: 'https://prometheus.io/_next/static/media/prometheus-logo.7aa022e5.svg',
|
||||
link: 'http://prome.schleppe:9090'
|
||||
},
|
||||
{
|
||||
title: 'Traefik',
|
||||
image: 'https://storage.googleapis.com/schleppe-files/Traefik.logo_shape_bordered.png',
|
||||
link: 'https://grafana.schleppe.cloud',
|
||||
background: '#30A4C2',
|
||||
color: '#343A40'
|
||||
},
|
||||
{
|
||||
title: 'Kibana',
|
||||
image: 'https://marketplace-assets.digitalocean.com/logos/sharklabs-kibana.svg',
|
||||
link: 'https://kibana.schleppe.cloud'
|
||||
},
|
||||
{
|
||||
title: 'HASS',
|
||||
image:
|
||||
'https://upload.wikimedia.org/wikipedia/en/thumb/4/49/Home_Assistant_logo_%282023%29.svg/2048px-Home_Assistant_logo_%282023%29.svg.png',
|
||||
link: 'http://homeassistant.schleppe:8123'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<PageHeader>Sites</PageHeader>
|
||||
|
||||
<div class="section-wrapper">
|
||||
{#each sites as site}
|
||||
<ThumbnailButton
|
||||
title={site.title}
|
||||
image={site.image}
|
||||
background={site.background}
|
||||
color={site.color}
|
||||
link={site.link}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<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."
|
||||
@@ -26,3 +79,10 @@
|
||||
description="Connected services can communicate with your application over the private network."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.section-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user