mirror of
https://github.com/KevinMidboe/infra-map.git
synced 2025-12-08 20:29:05 +00:00
source, static files & Dockerfile
This commit is contained in:
37
src/routes/+layout.svelte
Normal file
37
src/routes/+layout.svelte
Normal 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
82
src/routes/+page.svelte
Normal 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>
|
||||
90
src/routes/cluster/+page.server.ts
Normal file
90
src/routes/cluster/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
94
src/routes/cluster/+page.svelte
Normal file
94
src/routes/cluster/+page.svelte
Normal 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>
|
||||
17
src/routes/cluster/pod/[uid]/+page.server.ts
Normal file
17
src/routes/cluster/pod/[uid]/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
165
src/routes/cluster/pod/[uid]/+page.svelte
Normal file
165
src/routes/cluster/pod/[uid]/+page.svelte
Normal 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>
|
||||
27
src/routes/cluster/pod/[uid]/logs/+server.ts
Normal file
27
src/routes/cluster/pod/[uid]/logs/+server.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
20
src/routes/health/+page.svelte
Normal file
20
src/routes/health/+page.svelte
Normal 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}
|
||||
/>
|
||||
26
src/routes/network/+page.server.ts
Normal file
26
src/routes/network/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
56
src/routes/network/+page.svelte
Normal file
56
src/routes/network/+page.svelte
Normal 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>
|
||||
29
src/routes/network/[id]/+page.server.ts
Normal file
29
src/routes/network/[id]/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
15
src/routes/network/[id]/+page.svelte
Normal file
15
src/routes/network/[id]/+page.svelte
Normal 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>
|
||||
11
src/routes/printer/+page.server.ts
Normal file
11
src/routes/printer/+page.server.ts
Normal 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 };
|
||||
};
|
||||
237
src/routes/printer/+page.svelte
Normal file
237
src/routes/printer/+page.svelte
Normal 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>
|
||||
12
src/routes/printer/[id]/+page.server.ts
Normal file
12
src/routes/printer/[id]/+page.server.ts
Normal 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 };
|
||||
};
|
||||
30
src/routes/printer/[id]/+page.svelte
Normal file
30
src/routes/printer/[id]/+page.svelte
Normal 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>
|
||||
43
src/routes/servers/+page.server.ts
Normal file
43
src/routes/servers/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
29
src/routes/servers/+page.svelte
Normal file
29
src/routes/servers/+page.svelte
Normal 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>
|
||||
28
src/routes/sites/+page.svelte
Normal file
28
src/routes/sites/+page.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user