diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e7e736f --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +PROXMOX_URL= +PROXMOX_TOKEN= +HOMEASSISTANT_URL= +HOMEASSISTANT_TOKEN= +TRAEFIK_URL= diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e112690 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM node:22-alpine3.20 AS builder + +WORKDIR /app + +COPY src/ src +COPY static/ static +COPY package.json yarn.lock svelte.config.js tsconfig.json vite.config.ts . + +RUN yarn +RUN yarn build + +FROM node:22-alpine3.20 + +WORKDIR /opt/infra-map +COPY --from=builder /app/build build + +COPY package.json . +RUN yarn + +EXPOSE 3000 +ENV NODE_ENV=production + +CMD [ "node", "build" ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5b2950 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..2612ba0 --- /dev/null +++ b/src/app.html @@ -0,0 +1,13 @@ + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..bc625d8 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,10 @@ +import type { Handle } from '@sveltejs/kit'; +import { dev } from '$app/environment'; + +export const handle: Handle = async ({ event, resolve }) => { + if (dev) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + + return await resolve(event); +}; diff --git a/src/lib/components/Daemon.svelte b/src/lib/components/Daemon.svelte new file mode 100644 index 0000000..e7b7d87 --- /dev/null +++ b/src/lib/components/Daemon.svelte @@ -0,0 +1,49 @@ + + +
+
+

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

+
+ +

heatlthy: {healthy}

+ +
+ {#each daemon?.pods as pod, i (pod)} + + {/each} +
+
+ + diff --git a/src/lib/components/Deploy.svelte b/src/lib/components/Deploy.svelte new file mode 100644 index 0000000..02e5cc3 --- /dev/null +++ b/src/lib/components/Deploy.svelte @@ -0,0 +1,41 @@ + + +
+
+

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

+
+ +
+ {#each pods as pod, i (pod)} + + {/each} +
+
+ + diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte new file mode 100644 index 0000000..074524e --- /dev/null +++ b/src/lib/components/Header.svelte @@ -0,0 +1,115 @@ + + +
+
+ +

schleppe.cloud

+
+ +
+ Home + {#each $breadcrumbs as crumb (crumb.label)} + / + {crumb.label} + {/each} +
+ +
+ User profile +
+
+ + diff --git a/src/lib/components/Lifecycle.svelte b/src/lib/components/Lifecycle.svelte new file mode 100644 index 0000000..fbbbade --- /dev/null +++ b/src/lib/components/Lifecycle.svelte @@ -0,0 +1,63 @@ + + +
+ {#each conditions as condition (condition)} +
+ {dict[condition.type]} + {condition.status === 'True' ? 'yes' : 'no'} +
+ + {/each} +
+ + diff --git a/src/lib/components/Logs.svelte b/src/lib/components/Logs.svelte new file mode 100644 index 0000000..12a380c --- /dev/null +++ b/src/lib/components/Logs.svelte @@ -0,0 +1,150 @@ + + +
+
+
+ + {#each logs as log, i (log + i)} +
{#if lineNumbers}{i + 1}{/if}{log}
+ {/each} +
+
+
+ + diff --git a/src/lib/components/Node.svelte b/src/lib/components/Node.svelte new file mode 100644 index 0000000..6481894 --- /dev/null +++ b/src/lib/components/Node.svelte @@ -0,0 +1,201 @@ + + +
+
+
+ {metadata.name} + + +
+ +
+
+ + Status +
+ {status.phase} + +
+ + IP address +
+ {status.addresses[0].address} + +
+ + Pods +
+ {pods.length} + +
+ + CPUs allocated +
+ {status.capacity.cpu} + +
+ + Memory allocaed +
+ {convertKiToHumanReadable(status.capacity.memory)} + +
+ + +
+ + diff --git a/src/lib/components/PageHeader.svelte b/src/lib/components/PageHeader.svelte new file mode 100644 index 0000000..79c4c8b --- /dev/null +++ b/src/lib/components/PageHeader.svelte @@ -0,0 +1,16 @@ +

+ + diff --git a/src/lib/components/Pod.svelte b/src/lib/components/Pod.svelte new file mode 100644 index 0000000..faa4803 --- /dev/null +++ b/src/lib/components/Pod.svelte @@ -0,0 +1,203 @@ + + +
+
+
+ {name} + + +
+ +
+
+ + Status +
+ {status?.phase} + +
+ + Pod IP address +
+ {status?.podIP} + +
+ + Instances +
+ {i + 1} of {replicas} + +
+ + Running on Node +
+ {spec?.nodeName} + +
+ + Uptime +
+ {formatDuration($uptime / 1000)} + +
+ + +
+ + diff --git a/src/lib/components/Progress.svelte b/src/lib/components/Progress.svelte new file mode 100644 index 0000000..6473fa6 --- /dev/null +++ b/src/lib/components/Progress.svelte @@ -0,0 +1,75 @@ + + + + +{value}% + + diff --git a/src/lib/components/Section.svelte b/src/lib/components/Section.svelte new file mode 100644 index 0000000..8529e17 --- /dev/null +++ b/src/lib/components/Section.svelte @@ -0,0 +1,29 @@ + + +
+
+

{title}

+ +
+ + +
+ + diff --git a/src/lib/components/Server.svelte b/src/lib/components/Server.svelte new file mode 100644 index 0000000..e645b4f --- /dev/null +++ b/src/lib/components/Server.svelte @@ -0,0 +1,259 @@ + + +
+
+
+ {node?.name} + + +
+ +
+
+ + Load +
+ {loadavg} + +
+ + CPU cores +
+ {cpuinfo.cpus} Cores on {cpuinfo.sockets} {cpuinfo.sockets > 1 ? 'Sockets' : 'Socket'} + +
+ + DDoS protection +
+ Enabled + +
+ + IPs +
+ {node?.ip} + +
+ + Memory +
+ {formatBytes(memory?.total)} + +
+ + VMs +
+ {vmsRunning.length} / {vmsTotal.length} + +
+ + LXCs +
+ {lxcsRunning.length} / {lxcsTotal.length} + +
+ + Uptime +
+ {formatDuration(uptime)} +
+ + +
+ + diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte new file mode 100644 index 0000000..418bfcd --- /dev/null +++ b/src/lib/components/Sidebar.svelte @@ -0,0 +1,112 @@ + + + + + diff --git a/src/lib/components/Table.svelte b/src/lib/components/Table.svelte new file mode 100644 index 0000000..6737df4 --- /dev/null +++ b/src/lib/components/Table.svelte @@ -0,0 +1,136 @@ + + +
+
+

{title}

+
{description}
+
+
+ +
+ + + + {#if displayColumns.length > 0} + {#each displayColumns as column (column)} + + {/each} + {:else} + {#each columns as column (column)} + + {/each} + {/if} + + + + {#each data as row, i (row)} + hasLinks && goto(links[i])} class={hasLinks ? 'link' : ''}> + {#each columns as column (column)} + {#if column === 'Link'} + + {:else if column === 'Hex'} + + {:else if Array.isArray(row[column])} + + {:else} + + {/if} + {/each} + + {/each} + +
{column}{column}
Link{row[column].join(', ')}{row[column]}
+ + {#if footer?.length} + + {/if} +
+ + diff --git a/src/lib/components/navigation/Tab.svelte b/src/lib/components/navigation/Tab.svelte new file mode 100644 index 0000000..b159760 --- /dev/null +++ b/src/lib/components/navigation/Tab.svelte @@ -0,0 +1,49 @@ + + +
+ +
+ + diff --git a/src/lib/components/navigation/TabList.svelte b/src/lib/components/navigation/TabList.svelte new file mode 100644 index 0000000..df93a2e --- /dev/null +++ b/src/lib/components/navigation/TabList.svelte @@ -0,0 +1,11 @@ +
+ +
+ + diff --git a/src/lib/components/navigation/TabView.svelte b/src/lib/components/navigation/TabView.svelte new file mode 100644 index 0000000..ad4a908 --- /dev/null +++ b/src/lib/components/navigation/TabView.svelte @@ -0,0 +1,29 @@ + + +{#if $selectedPanel === panel} +
+ + +
+{/if} + + diff --git a/src/lib/components/navigation/Tabs.svelte b/src/lib/components/navigation/Tabs.svelte new file mode 100644 index 0000000..05686d8 --- /dev/null +++ b/src/lib/components/navigation/Tabs.svelte @@ -0,0 +1,63 @@ + + + + +
+ +
+ + diff --git a/src/lib/icons/Logo.svelte b/src/lib/icons/Logo.svelte new file mode 100644 index 0000000..eb00262 --- /dev/null +++ b/src/lib/icons/Logo.svelte @@ -0,0 +1,98 @@ + + + + + + + + + + diff --git a/src/lib/icons/Thermometer.svelte b/src/lib/icons/Thermometer.svelte new file mode 100644 index 0000000..5f4947a --- /dev/null +++ b/src/lib/icons/Thermometer.svelte @@ -0,0 +1,18 @@ + + + + + + diff --git a/src/lib/icons/box.svelte b/src/lib/icons/box.svelte new file mode 100644 index 0000000..ab5aa63 --- /dev/null +++ b/src/lib/icons/box.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/lib/icons/branches.svelte b/src/lib/icons/branches.svelte new file mode 100644 index 0000000..5edd31d --- /dev/null +++ b/src/lib/icons/branches.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/icons/clock.svelte b/src/lib/icons/clock.svelte new file mode 100644 index 0000000..685a75b --- /dev/null +++ b/src/lib/icons/clock.svelte @@ -0,0 +1,18 @@ + + + + + + diff --git a/src/lib/icons/connection.svelte b/src/lib/icons/connection.svelte new file mode 100644 index 0000000..fe6b344 --- /dev/null +++ b/src/lib/icons/connection.svelte @@ -0,0 +1,12 @@ + + + + diff --git a/src/lib/icons/cpu-x86.svelte b/src/lib/icons/cpu-x86.svelte new file mode 100644 index 0000000..269bc1c --- /dev/null +++ b/src/lib/icons/cpu-x86.svelte @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/src/lib/icons/cpu.svelte b/src/lib/icons/cpu.svelte new file mode 100644 index 0000000..6da1701 --- /dev/null +++ b/src/lib/icons/cpu.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/icons/cube-side.svelte b/src/lib/icons/cube-side.svelte new file mode 100644 index 0000000..804c573 --- /dev/null +++ b/src/lib/icons/cube-side.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/icons/database.svelte b/src/lib/icons/database.svelte new file mode 100644 index 0000000..e915229 --- /dev/null +++ b/src/lib/icons/database.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/icons/earth.svelte b/src/lib/icons/earth.svelte new file mode 100644 index 0000000..9ecb0c1 --- /dev/null +++ b/src/lib/icons/earth.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/icons/finished.svelte b/src/lib/icons/finished.svelte new file mode 100644 index 0000000..adc3184 --- /dev/null +++ b/src/lib/icons/finished.svelte @@ -0,0 +1,28 @@ + + + + + + diff --git a/src/lib/icons/floppy-disk.svelte b/src/lib/icons/floppy-disk.svelte new file mode 100644 index 0000000..1740a54 --- /dev/null +++ b/src/lib/icons/floppy-disk.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/icons/hard-disk.svelte b/src/lib/icons/hard-disk.svelte new file mode 100644 index 0000000..f898485 --- /dev/null +++ b/src/lib/icons/hard-disk.svelte @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/src/lib/icons/layers.svelte b/src/lib/icons/layers.svelte new file mode 100644 index 0000000..bac7a20 --- /dev/null +++ b/src/lib/icons/layers.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/icons/link.svelte b/src/lib/icons/link.svelte new file mode 100644 index 0000000..cdc3cd7 --- /dev/null +++ b/src/lib/icons/link.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/icons/map-marker.svelte b/src/lib/icons/map-marker.svelte new file mode 100644 index 0000000..237f507 --- /dev/null +++ b/src/lib/icons/map-marker.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/icons/microsd.svelte b/src/lib/icons/microsd.svelte new file mode 100644 index 0000000..49bc1b8 --- /dev/null +++ b/src/lib/icons/microsd.svelte @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/lib/icons/network.svelte b/src/lib/icons/network.svelte new file mode 100644 index 0000000..f5c7240 --- /dev/null +++ b/src/lib/icons/network.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/icons/paused.svelte b/src/lib/icons/paused.svelte new file mode 100644 index 0000000..2468f0b --- /dev/null +++ b/src/lib/icons/paused.svelte @@ -0,0 +1,30 @@ + + + + + + + diff --git a/src/lib/icons/power.svelte b/src/lib/icons/power.svelte new file mode 100644 index 0000000..e86f5fe --- /dev/null +++ b/src/lib/icons/power.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/icons/printer-idle.svelte b/src/lib/icons/printer-idle.svelte new file mode 100644 index 0000000..b6f745b --- /dev/null +++ b/src/lib/icons/printer-idle.svelte @@ -0,0 +1,31 @@ + + + + + + diff --git a/src/lib/icons/printer-paused.svelte b/src/lib/icons/printer-paused.svelte new file mode 100644 index 0000000..2103fc4 --- /dev/null +++ b/src/lib/icons/printer-paused.svelte @@ -0,0 +1,36 @@ + + + + + + + diff --git a/src/lib/icons/printer-printing.svelte b/src/lib/icons/printer-printing.svelte new file mode 100644 index 0000000..b7eaf00 --- /dev/null +++ b/src/lib/icons/printer-printing.svelte @@ -0,0 +1,30 @@ + + + + + diff --git a/src/lib/icons/printer-stopped.svelte b/src/lib/icons/printer-stopped.svelte new file mode 100644 index 0000000..9e64326 --- /dev/null +++ b/src/lib/icons/printer-stopped.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/src/lib/icons/printing.svelte b/src/lib/icons/printing.svelte new file mode 100644 index 0000000..0fe98c2 --- /dev/null +++ b/src/lib/icons/printing.svelte @@ -0,0 +1,27 @@ + + + + + + diff --git a/src/lib/icons/server.svelte b/src/lib/icons/server.svelte new file mode 100644 index 0000000..db045ef --- /dev/null +++ b/src/lib/icons/server.svelte @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/src/lib/icons/shield.svelte b/src/lib/icons/shield.svelte new file mode 100644 index 0000000..45e7069 --- /dev/null +++ b/src/lib/icons/shield.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/icons/stopped.svelte b/src/lib/icons/stopped.svelte new file mode 100644 index 0000000..0b5fcf1 --- /dev/null +++ b/src/lib/icons/stopped.svelte @@ -0,0 +1,26 @@ + + + + + + diff --git a/src/lib/icons/temperature-bed.svelte b/src/lib/icons/temperature-bed.svelte new file mode 100644 index 0000000..5fa2fee --- /dev/null +++ b/src/lib/icons/temperature-bed.svelte @@ -0,0 +1,38 @@ + + + + + + + diff --git a/src/lib/icons/temperature-nozzle.svelte b/src/lib/icons/temperature-nozzle.svelte new file mode 100644 index 0000000..60921e7 --- /dev/null +++ b/src/lib/icons/temperature-nozzle.svelte @@ -0,0 +1,31 @@ + + + + + + diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/interfaces/homeassistant.ts b/src/lib/interfaces/homeassistant.ts new file mode 100644 index 0000000..754f353 --- /dev/null +++ b/src/lib/interfaces/homeassistant.ts @@ -0,0 +1,235 @@ +export interface Entity { + entity_id: string; + state: string; + attributes: Attributes; + last_changed: Date; + last_reported: Date; + last_updated: Date; + context: Context; +} + +export interface Attributes { + editable?: boolean; + id?: string; + device_trackers?: string[]; + latitude?: number; + longitude?: number; + gps_accuracy?: number; + source?: string; + user_id?: string; + friendly_name?: string; + entity_id?: string[]; + icon?: string; + min_color_temp_kelvin?: number; + max_color_temp_kelvin?: number; + min_mireds?: number; + max_mireds?: number; + supported_color_modes?: ColorMode[]; + color_mode?: ColorMode | null; + brightness?: number | null; + color_temp_kelvin?: number | null; + color_temp?: number | null; + hs_color?: number[] | null; + rgb_color?: number[] | null; + xy_color?: number[] | null; + supported_features?: number; + effect_list?: string[]; + effect?: null; + initial?: null; + min?: number; + max?: number; + step?: number; + mode?: Mode; + unit_of_measurement?: string; + radius?: number; + passive?: boolean; + persons?: string[]; + next_dawn?: Date; + next_dusk?: Date; + next_midnight?: Date; + next_noon?: Date; + next_rising?: Date; + next_setting?: Date; + elevation?: number; + azimuth?: number; + rising?: boolean; + device_class?: string; + has_date?: boolean; + has_time?: boolean; + hour?: number; + minute?: number; + second?: number; + timestamp?: number; + source_type?: string; + 'Fast User Switched'?: boolean; + Idle?: boolean; + Locked?: boolean; + 'Screen Off'?: boolean; + Screensaver?: boolean; + Sleeping?: boolean; + Terminating?: boolean; + 'Battery Provides Time Remaining'?: boolean; + BatteryHealth?: string; + BatteryHealthCondition?: string; + Current?: number; + 'Current Capacity'?: number; + DesignCycleCount?: number; + 'Hardware Serial Number'?: string; + 'Is Charging'?: boolean; + 'Is Present'?: boolean; + 'LPM Active'?: boolean; + 'Max Capacity'?: number; + Name?: string; + 'Optimized Battery Charging Engaged'?: boolean; + 'Power Source ID'?: number; + 'Power Source State'?: string; + 'Time to Empty'?: number; + 'Time to Full Charge'?: number; + 'Transport Type'?: string; + Type?: string; + 'Low Power Mode'?: boolean; + Available?: string; + 'Available (Important)'?: string; + 'Available (Opportunistic)'?: string; + Total?: string; + 'Hardware Address'?: string; + 'Active Camera'?: any[]; + 'All Camera'?: string[]; + 'Active Audio Input'?: any[]; + 'All Audio Input'?: string[]; + 'Active Audio Output'?: any[]; + 'All Audio Output'?: string[]; + 'Display IDs'?: string[]; + 'Display Names'?: string[]; + 'Bundle Identifier'?: string; + 'Is Hidden'?: boolean; + 'Launch Date'?: Date; + 'Owns Menu Bar'?: boolean; + 'Allows VoIP'?: boolean; + 'Carrier ID'?: string; + 'Carrier Name'?: string; + 'ISO Country Code'?: string; + 'Mobile Country Code'?: string; + 'Mobile Network Code'?: string; + battery_level?: number; + altitude?: number; + vertical_accuracy?: number; + 'Current Radio Technology'?: string; + 'Administrative Area'?: string; + 'Areas Of Interest'?: string; + Country?: string; + 'Inland Water'?: string; + Locality?: string; + Location?: number[]; + Ocean?: string; + 'Postal Code'?: string; + 'Sub Administrative Area'?: string; + 'Sub Locality'?: string; + 'Sub Thoroughfare'?: string; + Thoroughfare?: string; + 'Time Zone'?: string; + Zones?: string[]; + temperature?: number | null; + dew_point?: number; + temperature_unit?: string; + humidity?: number; + cloud_coverage?: number; + uv_index?: number; + pressure?: number; + pressure_unit?: string; + wind_bearing?: number; + wind_gust_speed?: number; + wind_speed?: number; + wind_speed_unit?: string; + visibility_unit?: string; + precipitation_unit?: string; + attribution?: string; + access_token?: string; + width?: number; + height?: number; + fps?: number; + bitrate?: number; + channel_id?: number; + entity_picture?: string; + motion_detection?: boolean; + frontend_stream_type?: string; + options?: string[]; + state_class?: StateClass; + auto_update?: boolean; + installed_version?: string; + in_progress?: boolean; + latest_version?: null | string; + release_summary?: null | string; + release_url?: null | string; + skipped_version?: null; + title?: null; + Count?: number; + preset_modes?: null; + percentage?: number; + percentage_step?: number; + preset_mode?: null; + active?: boolean; + color?: string; + k_value?: number; + name?: string; + nozzle_temp_min?: string; + nozzle_temp_max?: string; + type?: string; + modifier?: number; + 'AMS Slot 1'?: number; + remain?: number; + tag_uid?: string; + tray_uuid?: string; + hvac_modes?: string[]; + min_temp?: number; + max_temp?: number; + current_temperature?: number; + hvac_action?: string; + occupied_cooling_setpoint?: number; + occupied_heating_setpoint?: number; + system_mode?: string; + unoccupied_heating_setpoint?: number; + off_with_transition?: boolean; + off_brightness?: number | null; + battery_size?: string; + battery_quantity?: number; + battery_voltage?: number; + measurement_type?: MeasurementType; + device_type?: string; + status?: string; + zcl_unit_of_measurement?: number; + last_triggered?: Date; + current?: number; + restored?: boolean; + source_list?: string[]; +} + +export enum ColorMode { + Brightness = 'brightness', + ColorTemp = 'color_temp', + Onoff = 'onoff', + Xy = 'xy' +} + +export enum MeasurementType { + ActiveMeasurement = 'ACTIVE_MEASUREMENT', + ActiveMeasurementPhaseAMeasurement = 'ACTIVE_MEASUREMENT, PHASE_A_MEASUREMENT' +} + +export enum Mode { + Auto = 'auto', + Box = 'box', + Single = 'single', + Slider = 'slider' +} + +export enum StateClass { + Measurement = 'measurement', + TotalIncreasing = 'total_increasing' +} + +export interface Context { + id: string; + parent_id: null | string; + user_id: null | string; +} diff --git a/src/lib/interfaces/printer.ts b/src/lib/interfaces/printer.ts new file mode 100644 index 0000000..019a63c --- /dev/null +++ b/src/lib/interfaces/printer.ts @@ -0,0 +1,8 @@ +export interface Filament { + Hex: string; + Color: string; + Material: string; + Weight: string; + Count: number; + Link: string; +} diff --git a/src/lib/interfaces/proxmox.ts b/src/lib/interfaces/proxmox.ts new file mode 100644 index 0000000..c32746a --- /dev/null +++ b/src/lib/interfaces/proxmox.ts @@ -0,0 +1,79 @@ +export interface Cluster { + id: string; + quorate: number; + version: number; + type: string; + nodes: number; + name: string; +} + +interface Memory { + used: number; + free: number; + total: number; +} + +interface RootFs { + used: number; + free: number; + avail: number; + total: number; +} + +interface Swap { + total: number; + free: number; + used: number; +} + +interface CpuInfo { + cores: number; + mhz: string; + cpus: number; + model: string; + sockets: number; + user_hz: number; + flags: string; + hvm: string; +} + +interface KernelInfo { + release: string; + sysname: string; + version: string; + machine: string; +} + +interface BootInfo { + secureboot?: number; + mode: string; +} + +interface NodeStatus { + memory: Memory; + kversion: string; + cpu: number; + ksm: { shared: number }; + uptime: number; + currentKernel: KernelInfo; + rootfs: RootFs; + swap: Swap; + idle: number; + cpuinfo: CpuInfo; + pveversion: string; + loadavg: [string, string, string]; + wait: number; + bootInfo: BootInfo; +} + +export interface Node { + info: NodeStatus; + online: number; + nodeid: number; + local: number; + name: string; + id: string; + type: string; + ip: string; + level: string; +} diff --git a/src/lib/server/filament.ts b/src/lib/server/filament.ts new file mode 100644 index 0000000..5a88706 --- /dev/null +++ b/src/lib/server/filament.ts @@ -0,0 +1,92 @@ +import type { Filament } from '$lib/interfaces/printer'; + +const filament: Filament[] = [ + { + Hex: '#DD4344', + Color: 'Scarlet Red', + Material: 'PLA Matte', + Weight: '1kg', + Count: 2, + Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742848731' + }, + { + Hex: '#61C57F', + Color: 'Grass Green', + Material: 'PLA Matte', + Weight: '1kg', + Count: 2, + Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742783195' + }, + { + Hex: '#F7DA5A', + Color: 'Lemon Yellow', + Material: 'PLA Matte', + Weight: '1kg', + Count: 2, + Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742717659' + }, + { + Hex: '#E8DBB7', + Color: 'Desert Tan', + Material: 'PLA Matte', + Weight: '1kg', + Count: 1, + Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=48612736401756' + }, + { + Hex: "url('https://www.transparenttextures.com/patterns/asfalt-dark.png'", + Color: 'White Marble', + Material: 'PLA Marble', + Weight: '1kg', + Count: 1, + Link: 'https://eu.store.bambulab.com/en-no/products/pla-marble?variant=43964050964699' + }, + { + Hex: '#0078C0', + Color: 'Marine Blue', + Material: 'PLA Matte', + Weight: '1kg', + Count: 1, + Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996751073499' + }, + { + Hex: '#000000', + Color: 'Charcoal', + Material: 'PLA Matte', + Weight: '1kg', + Count: 2, + Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742750427' + }, + { + Hex: '#ffffff', + Color: 'Ivory White', + Material: 'PLA Matte', + Weight: '1kg', + Count: 2, + Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742586587' + }, + { + Hex: '#E8AFCE', + Color: 'Sakura Pink', + Material: 'PLA Matte', + Weight: '1kg', + Count: 1, + Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742684891' + }, + { + Hex: '#AE96D5', + Color: 'Lilac Purple', + Material: 'PLA Matte', + Weight: '1kg', + Count: 1, + Link: 'https://eu.store.bambulab.com/en-no/collections/pla/products/pla-matte?variant=42996742914267' + } +]; + +export function filamentByColor(name: string) { + return filament.find((f) => f.Color?.toLowerCase() === name?.toLowerCase()); +} + +export function currentFilament(): Filament[] { + return filament; +} diff --git a/src/lib/server/homeassistant.ts b/src/lib/server/homeassistant.ts new file mode 100644 index 0000000..6877e54 --- /dev/null +++ b/src/lib/server/homeassistant.ts @@ -0,0 +1,36 @@ +import { env } from '$env/dynamic/private'; +import type { Entity } from '$lib/interfaces/homeassistant'; + +function buildHomeassistantRequest() { + const url = env.HOMEASSISTANT_URL || ''; + const token = env.HOMEASSISTANT_TOKEN || ''; + + const options = { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }; + + return { url, options }; +} + +async function fetchHassStates() { + const { url, options } = buildHomeassistantRequest(); + return fetch(url, options).then((resp) => resp.json()); +} + +export async function fetchP1P(): Promise { + try { + let hassStates = await fetchHassStates(); + + hassStates = hassStates.filter( + (el: Entity) => el.attributes.friendly_name?.includes('P1P') === true + ); + return hassStates; + } catch (error) { + console.log('ERROR! from fetchP1P:', error); + return Promise.reject(null); + } +} diff --git a/src/lib/server/kubernetes.ts b/src/lib/server/kubernetes.ts new file mode 100644 index 0000000..6087847 --- /dev/null +++ b/src/lib/server/kubernetes.ts @@ -0,0 +1,116 @@ +import * as k8s from '@kubernetes/client-node'; +import stream from 'stream'; +import { writable } from 'svelte/store'; + +const context = { + name: 'kazan-insecure', + user: 'admin', + cluster: 'kazan-insecure' +}; + +const kc = new k8s.KubeConfig(); +kc.loadFromDefault({ contexts: [context] }); + +const k8sApi = kc.makeApiClient(k8s.CoreV1Api); +const appsV1Api = kc.makeApiClient(k8s.AppsV1Api); + +const k8sLog = new k8s.Log(kc); + +export async function getReplicas() { + try { + const allReplicas = await appsV1Api.listReplicaSetForAllNamespaces(); + return allReplicas.items; + } catch (error) { + console.log('error when getting replicas:', error); + return []; + } +} + +export async function getPods() { + try { + const allPods = await k8sApi.listPodForAllNamespaces(); + return allPods.items; + } catch (error) { + console.log('error when getting k8s resources:', error); + return []; + } +} + +export async function getPod(name: string, namespace: string) { + try { + return await k8sApi.readNamespacedPodTemplate({ name, namespace }); + } catch (error) { + console.log(`error when getting pod:`, error); + return undefined; + } +} + +export async function getDeployments() { + try { + const allDeploys = await appsV1Api.listDeploymentForAllNamespaces(); + return allDeploys.items; + } catch (error) { + console.log('error when getting deployments:', error); + return []; + } +} + +export async function getDaemons() { + try { + const allDaemons = await appsV1Api.listDaemonSetForAllNamespaces(); + return allDaemons.items; + } catch (error) { + console.log('error when getting daemons:', error); + return []; + } +} + +export async function getNodes() { + try { + const nodes = await k8sApi.listNode(); + return nodes.items; + } catch (error) { + console.log('error when getting k8s nodes:', error); + return []; + } +} + +export function createLogStream(podName: string, namespace: string, containerName: string) { + // const logEmitter = new EventTarget(); // use EventTarget or EventEmitter + const logEmitter = writable(); + + const maxLines = 400; + let liveStream: stream.PassThrough | null = null; + let logAbortController; + + async function start() { + // Live logs + liveStream = new stream.PassThrough(); + liveStream.on('data', (chunk) => { + let chunks = chunk.toString().split('\n').filter(Boolean); + + console.log('chynjks length:', chunks?.length); + if (chunks?.length > maxLines) { + chunks = chunks.slice(maxLines); + } + + chunks.forEach((line) => logEmitter.set(line)); + }); + + console.log('setting logAbortController, prev:', logAbortController); + logAbortController = await k8sLog.log(namespace, podName, containerName, liveStream, { + follow: true, + timestamps: false, + pretty: false, + tailLines: maxLines + }); + } + + function stop() { + console.log('ending livestream!!'); + logAbortController?.abort(); + liveStream?.end(); + } + + return { start, stop, logEmitter }; +} diff --git a/src/lib/server/proxmox.ts b/src/lib/server/proxmox.ts new file mode 100644 index 0000000..4d9b258 --- /dev/null +++ b/src/lib/server/proxmox.ts @@ -0,0 +1,86 @@ +import { env } from '$env/dynamic/private'; +import type { Cluster, Node } from '$lib/interfaces/proxmox'; + +function buildProxmoxRequest() { + const url = env.PROXMOX_URL || 'https://10.0.0.50:8006/api2/json/'; + const token = env.PROXMOX_TOKEN || 'REPLACE_WITH_PROXMOX_TOKEN'; + const options = { + method: 'GET', + headers: { + Authorization: token, + 'Content-Type': 'application/json' + } + }; + + return { url, options }; +} + +async function fetchNodeVMs(node: Node) { + const r = buildProxmoxRequest(); + r.url += 'nodes/' + node?.name + '/qemu'; + + return fetch(r.url, r?.options) + .then((resp) => resp.json()) + .then((response) => response.data); +} + +async function fetchNodeLXCs(node: Node) { + const r = buildProxmoxRequest(); + r.url += 'nodes/' + node?.name + '/lxc'; + + return fetch(r.url, r?.options) + .then((resp) => resp.json()) + .then((response) => response.data); +} + +async function fetchNodeInfo(node: Node) { + const r = buildProxmoxRequest(); + r.url += 'nodes/' + node?.name + '/status'; + + return fetch(r.url, r?.options) + .then((resp) => resp.json()) + .then((response) => response.data); +} + +async function getClusterInfo() { + const r = buildProxmoxRequest(); + r.url += 'cluster/status'; + + return fetch(r.url, r?.options) + .then((resp) => resp.json()) + .then((response) => { + const { data } = response; + const cluster = data.filter((d: Node | Cluster) => d?.type === 'cluster')[0]; + const nodes = data.filter((d: Node | Cluster) => d?.type === 'node'); + + return { cluster, nodes }; + }); +} + +export async function fetchNodes(): Promise<{ nodes: Node[]; cluster: Cluster | null }> { + try { + const { nodes, cluster } = await getClusterInfo(); + + const infoP = Promise.all(nodes.map((node: Node) => fetchNodeInfo(node))); + const vmsP = Promise.all(nodes.map((node: Node) => fetchNodeVMs(node))); + const lxcsP = Promise.all(nodes.map((node: Node) => fetchNodeLXCs(node))); + + const [info, vms, lxcs] = await Promise.all([infoP, vmsP, lxcsP]); + + return { + cluster, + nodes: nodes.map((node: Node, i: number) => { + return { + ...node, + info: info[i], + vms: vms[i], + lxcs: lxcs[i] + }; + }) + }; + } catch (error) { + console.log('ERROR from fetchnodes:', error); + + return { nodes: [], cluster: null }; + } +} diff --git a/src/lib/server/traefik.ts b/src/lib/server/traefik.ts new file mode 100644 index 0000000..c355fe6 --- /dev/null +++ b/src/lib/server/traefik.ts @@ -0,0 +1,22 @@ +import { env } from '$env/dynamic/private'; + +const TRAEFIK_HTTP_URL = '/api/http'; + +function buildTraefikRequest(path: string) { + const baseURL = env.TRAEFIK_URL || 'http://localhost:9000'; + const url = `${baseURL}${TRAEFIK_HTTP_URL}/${path}`; + const options = { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }; + + return { url, options }; +} + +export async function getRouters() { + const { url, options } = buildTraefikRequest('routers'); + + return fetch(url, options).then((resp) => resp.json()); +} diff --git a/src/lib/utils/conversion.ts b/src/lib/utils/conversion.ts new file mode 100644 index 0000000..e2b90f1 --- /dev/null +++ b/src/lib/utils/conversion.ts @@ -0,0 +1,46 @@ +export function formatBytes(bytes: number) { + if (bytes < 1024) return '0 KB'; // Ensure we don't show bytes, only KB and above + + const units = ['KB', 'MB', 'GB', 'TB']; + let unitIndex = -1; + let formattedSize = bytes; + + do { + formattedSize /= 1024; + unitIndex++; + } while (formattedSize >= 1024 && unitIndex < units.length - 1); + + return `${formattedSize.toFixed(2)} ${units[unitIndex]}`; +} + +export function formatDuration(seconds: number) { + if (seconds === 0) return 'Uptime: 0 days 00:00:00'; + + const days = Math.floor(seconds / 86400); + seconds %= 86400; + const hours = Math.floor(seconds / 3600); + seconds %= 3600; + const minutes = Math.floor(seconds / 60); + seconds %= 60; + + return `${days} days ${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(Math.floor(seconds)).padStart(2, '0')}`; +} + +export function convertKiToHumanReadable(input: string) { + const match = input.match(/^(\d+)(Ki)$/); + if (!match) return 'Invalid input'; + + const kibibytes = parseInt(match[1], 10); + const bytes = kibibytes * 1024; + + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + let i = 0; + let humanReadable = bytes; + + while (humanReadable >= 1024 && i < sizes.length - 1) { + humanReadable /= 1024; + i++; + } + + return `${humanReadable.toFixed(2)} ${sizes[i]}`; +} diff --git a/src/lib/utils/string.ts b/src/lib/utils/string.ts new file mode 100644 index 0000000..36294ae --- /dev/null +++ b/src/lib/utils/string.ts @@ -0,0 +1,3 @@ +export function capitalizeFirstLetter(text: string) { + return text.replace(/\b\w/g, (char) => char.toUpperCase()); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..50eaa51 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,37 @@ + + +
+
+ +
+ + +
+ +
+
+
+ + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..7710d9c --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,82 @@ + + +Welcome to SvelteKit +

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

+ +

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

+ 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. +

diff --git a/src/routes/cluster/+page.server.ts b/src/routes/cluster/+page.server.ts new file mode 100644 index 0000000..ff2f66d --- /dev/null +++ b/src/routes/cluster/+page.server.ts @@ -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 + }; +}; diff --git a/src/routes/cluster/+page.svelte b/src/routes/cluster/+page.svelte new file mode 100644 index 0000000..3b36ee7 --- /dev/null +++ b/src/routes/cluster/+page.svelte @@ -0,0 +1,94 @@ + + +Cluster overview + +
+ +

Cluster {nodes.length} nodes

+
+
+ {#each nodes as node (node)} + + {/each} +
+
+ +
+ +

+ Daemons {daemons.length} daemons ({daemons.reduce( + (total, item) => total + (item.pods ? item.pods.length : 0), + 0 + )} pods) +

+
+ +
+ {#each daemons as daemon (daemon)} + + {/each} +
+
+ +
+ +

+ Pods {deployments.length} deployments ({deployments.reduce( + (total, item) => total + (item.pods ? item.pods.length : 0), + 0 + )} pods) +

+
+ +
+ {#each deployments as deploy (deploy)} + + {/each} +
+
+ + diff --git a/src/routes/cluster/pod/[uid]/+page.server.ts b/src/routes/cluster/pod/[uid]/+page.server.ts new file mode 100644 index 0000000..3218f20 --- /dev/null +++ b/src/routes/cluster/pod/[uid]/+page.server.ts @@ -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 + }; +}; diff --git a/src/routes/cluster/pod/[uid]/+page.svelte b/src/routes/cluster/pod/[uid]/+page.svelte new file mode 100644 index 0000000..4ef89a7 --- /dev/null +++ b/src/routes/cluster/pod/[uid]/+page.svelte @@ -0,0 +1,165 @@ + + +Pod: {pod?.metadata?.name} + + + + Details + Logs + Metadata + Deployment logs + + + +
+
+
+
+ + {status?.phase} +
+ +
+ + {status?.podIP} +
+ +
+ + {status?.qosClass} +
+ +
+ + {formatDuration($uptime / 1000)} +
+
+
+ +
+
+
+ + {metadata?.namespace} +
+ +
+ + {metadata?.ownerReferences?.[0].kind} +
+
+
+ +
+
+
+ + {spec?.nodeName} +
+ +
+ + {spec?.restartPolicy} +
+ +
+ + {spec?.serviceAccount} +
+ +
+ + {spec?.schedulerName} +
+ +
+ + {spec?.hostNetwork ? 'yes' : 'no'} +
+ +
+ + {spec?.dnsPolicy} +
+
+
+
+
+ + + + + + + + + + + + +
diff --git a/src/routes/cluster/pod/[uid]/logs/+server.ts b/src/routes/cluster/pod/[uid]/logs/+server.ts new file mode 100644 index 0000000..6090cbc --- /dev/null +++ b/src/routes/cluster/pod/[uid]/logs/+server.ts @@ -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; + } + }); +} diff --git a/src/routes/health/+page.svelte b/src/routes/health/+page.svelte new file mode 100644 index 0000000..d9840fe --- /dev/null +++ b/src/routes/health/+page.svelte @@ -0,0 +1,20 @@ + + +Health + + diff --git a/src/routes/network/+page.server.ts b/src/routes/network/+page.server.ts new file mode 100644 index 0000000..558280f --- /dev/null +++ b/src/routes/network/+page.server.ts @@ -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 + }; +}; diff --git a/src/routes/network/+page.svelte b/src/routes/network/+page.svelte new file mode 100644 index 0000000..bd74ae1 --- /dev/null +++ b/src/routes/network/+page.svelte @@ -0,0 +1,56 @@ + + +Network + +
+
+
+
+ + {routers.length} +
+ +
+ + {providers?.join(', ')} +
+
+
+ +
+ + + diff --git a/src/routes/network/[id]/+page.server.ts b/src/routes/network/[id]/+page.server.ts new file mode 100644 index 0000000..dfc9b98 --- /dev/null +++ b/src/routes/network/[id]/+page.server.ts @@ -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 + }; +}; diff --git a/src/routes/network/[id]/+page.svelte b/src/routes/network/[id]/+page.svelte new file mode 100644 index 0000000..2ed3c47 --- /dev/null +++ b/src/routes/network/[id]/+page.svelte @@ -0,0 +1,15 @@ + + +Network: {router.service} + +
+

router:

+
{JSON.stringify(router, null, 2)}
+
diff --git a/src/routes/printer/+page.server.ts b/src/routes/printer/+page.server.ts new file mode 100644 index 0000000..32c9ae2 --- /dev/null +++ b/src/routes/printer/+page.server.ts @@ -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 }; +}; diff --git a/src/routes/printer/+page.svelte b/src/routes/printer/+page.svelte new file mode 100644 index 0000000..99c06c8 --- /dev/null +++ b/src/routes/printer/+page.svelte @@ -0,0 +1,237 @@ + + +Printer + +
+
+
+
+ + + + {currentStage.state} +
+ +
+ + {bedTemp.state} + {bedTemp.attributes.unit_of_measurement} +
+ +
+ + {nozzleTemp.state} + {nozzleTemp.attributes.unit_of_measurement} +
+ +
+ + + + + {printStatus.state} + +
+
+ +
+ + {#if currentLayer.state !== totalLayer.state} + Currently printing layer line {currentLayer.state} of {totalLayer.state} + {:else} + Finished printing {currentLayer.state} of {totalLayer.state} layers! + {/if} +
+
+ +
+
+
+ + + {Math.floor(Number(totalUsage.state) * 10) / 10} + + {totalUsage.attributes.unit_of_measurement} +
+ +
+ + {capitalizeFirstLetter(nozzleType.state.replaceAll('_', ' '))} + {nozzleType.attributes.unit_of_measurement} +
+ +
+ + {nozzleSize?.state} {nozzleSize.attributes.unit_of_measurement} +
+ +
+ + {capitalizeFirstLetter(bedType?.state?.replaceAll('_', ' ') || 'not found')} + {bedType?.attributes.unit_of_measurement} +
+
+ + +
+ +
+ + + diff --git a/src/routes/printer/[id]/+page.server.ts b/src/routes/printer/[id]/+page.server.ts new file mode 100644 index 0000000..a1051b1 --- /dev/null +++ b/src/routes/printer/[id]/+page.server.ts @@ -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 }; +}; diff --git a/src/routes/printer/[id]/+page.svelte b/src/routes/printer/[id]/+page.svelte new file mode 100644 index 0000000..dc77ee6 --- /dev/null +++ b/src/routes/printer/[id]/+page.svelte @@ -0,0 +1,30 @@ + + +{#if filament !== null} + Filament: {filament?.Color} + +
+
+
+{:else} + Filament not found! + +

Unable to find filament {data.id}, no swatch to display.

+{/if} + + diff --git a/src/routes/servers/+page.server.ts b/src/routes/servers/+page.server.ts new file mode 100644 index 0000000..9aeadc7 --- /dev/null +++ b/src/routes/servers/+page.server.ts @@ -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 + }; +}; diff --git a/src/routes/servers/+page.svelte b/src/routes/servers/+page.svelte new file mode 100644 index 0000000..5736377 --- /dev/null +++ b/src/routes/servers/+page.svelte @@ -0,0 +1,29 @@ + + +Servers + +
+ {#each nodes as node (node.name)} +
+ +
+ {/each} +
+ + diff --git a/src/routes/sites/+page.svelte b/src/routes/sites/+page.svelte new file mode 100644 index 0000000..620a8a9 --- /dev/null +++ b/src/routes/sites/+page.svelte @@ -0,0 +1,28 @@ + + +Sites + +
+
+ +
+ +
+ +
+
diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..825b9e6 Binary files /dev/null and b/static/favicon.png differ diff --git a/static/fonts/Inter.woff2 b/static/fonts/Inter.woff2 new file mode 100644 index 0000000..e0cab47 Binary files /dev/null and b/static/fonts/Inter.woff2 differ diff --git a/static/fonts/RecklessNeue-Regular.woff2 b/static/fonts/RecklessNeue-Regular.woff2 new file mode 100644 index 0000000..b6a1cd4 Binary files /dev/null and b/static/fonts/RecklessNeue-Regular.woff2 differ diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000..f148b73 Binary files /dev/null and b/static/logo.png differ diff --git a/static/logo_grey.png b/static/logo_grey.png new file mode 100644 index 0000000..e2d59de Binary files /dev/null and b/static/logo_grey.png differ diff --git a/static/logo_light.png b/static/logo_light.png new file mode 100644 index 0000000..9d6eeb5 Binary files /dev/null and b/static/logo_light.png differ diff --git a/static/printer.png b/static/printer.png new file mode 100644 index 0000000..9c0c526 Binary files /dev/null and b/static/printer.png differ diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..fbe37df --- /dev/null +++ b/static/style.css @@ -0,0 +1,146 @@ +@font-face { + font-family: 'Inter'; + font-style: normal; + src: url('/fonts/Inter.woff2') format('woff2'); + font-weight: 100 900; + font-style: normal; +} + +:root { + --bg: #f9f5f3; + --color: #1c1819; + --highlight: #eaddd5; + + --positive: #00d439; + --negative: #ff5449; + --warning: #ffa312; + + --border: 1px solid #eaddd5; + --border-radius: 0.75rem; +} + +body { + font-family: 'Inter', sans-serif; + font-optical-sizing: auto; + margin: 0; + padding: 0; + + background-color: var(--bg); + color: var(--color); + font-size: 14px; +} + +a, +a:visited { + color: var(--color); + text-decoration-line: none; +} + +h1 { + font-family: 'Reckless Neue'; +} + +h2 { + font-size: 20px; + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--str-space-100, 8px); + flex: 1 1 auto; + /* align-self: center; */ + align-self: left; + /* text-transform: capitalize; */ + flex-wrap: wrap; + font: var( + --str-font-heading-regular-l, + 400 1.429rem/1.4 Inter, + Arial, + -apple-system, + BlinkMacSystemFont, + sans-serif + ); + margin-top: 0; + color: var(--str-color-text-neutral-default); + font-weight: 500; + line-height: 100%; +} + +button { + border: none; + position: relative; + background: transparent; + height: unset; + border-radius: 0.5rem; + display: inline-block; + text-decoration: none; + padding: 0 0.5rem; + flex: 1; +} +button span { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + height: 1.5rem; + padding: 0 0.5rem; + margin-left: -0.5rem; + border: 1px solid #eaddd5; + border-radius: inherit; + white-space: nowrap; + cursor: pointer; + font-weight: 700; + transition: all ease-in-out 0.2s; +} + +button::after { + content: ''; + position: absolute; + right: 0; + top: 0; + border-radius: 0.5rem; + width: 100%; + height: 100%; + transition: transform 0.1s ease; + will-change: box-shadow 0.25s; + pointer-events: none; +} + +button:hover span { + border-color: #cab2aa; + background: #f9f5f3 !important; +} + +.main-container { + background: white; + padding: 1.5rem; + border: var(--border); + border-radius: var(--border-radius, 1rem); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.section-wrapper { + display: flex; + flex-direction: column; + gap: 24px; +} + +.section-row { + display: flex; + gap: 2rem; +} + +.section-element { + display: flex; + flex-direction: column; +} + +.section-element label { + font-weight: 500; + font-size: 15px; +} + +.section-element > span { + display: flex; + align-items: center; + margin-top: 0.5rem; +}