chore: more dependency removals, npm: cleanup

This commit is contained in:
hayzamjs
2026-03-06 22:08:38 +01:00
parent 35e3782a0e
commit 1990f644bd
30 changed files with 6040 additions and 7687 deletions
+5 -2
View File
@@ -6,5 +6,8 @@
"window.title": "${rootName}",
"explorer.compactFolders": false,
"workbench.startupEditor": "none",
"editor.formatOnSave": true
}
"editor.formatOnSave": true,
"files.associations": {
"*.css": "tailwindcss"
}
}
+5187 -6138
View File
File diff suppressed because it is too large Load Diff
-6
View File
@@ -45,7 +45,6 @@
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"layerchart": "^2.0.0-next.43",
"mode-watcher": "^1.0.8",
"paneforge": "^1.0.0-next.5",
"prettier": "^3.4.2",
@@ -67,11 +66,6 @@
"@fontsource/noto-sans": "^5.2.7",
"@wuchale/svelte": "^0.17.5",
"@wuchale/vite-plugin": "^0.15.3",
"adze": "^2.2.4",
"axios": "^1.11.0",
"chart.js": "^4.5.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-zoom": "^2.2.0",
"cronstrue": "^3.0.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
-3
View File
@@ -4,9 +4,6 @@
@custom-variant dark (&:is(.dark *));
@plugin "@iconify/tailwind4";
@source '../node_modules/layerchart/dist';
@import '@layerstack/tailwind/core.css';
/* @import '@layerstack/tailwind/themes/basic.css'; */
:root {
--radius: 0.3rem;
+220 -191
View File
@@ -12,254 +12,283 @@ import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { storage } from '$lib';
import type { JWTClaims } from '$lib/types/auth';
import type { APIResponse, Locales } from '$lib/types/common';
import type { APIResponse } from '$lib/types/common';
import { handleAPIError } from '$lib/utils/http';
import { sha256 } from '$lib/utils/string';
import adze from 'adze';
import axios, { AxiosError } from 'axios';
import { toast } from 'svelte-sonner';
async function parseJSONResponse(response: Response): Promise<any> {
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json') && !contentType.includes('+json')) {
return null;
}
try {
return await response.json();
} catch (_e: unknown) {
return null;
}
}
export async function login(
username: string,
password: string,
authType: string,
remember: boolean
username: string,
password: string,
authType: string,
remember: boolean
): Promise<boolean> {
try {
if (username === '' || password === '') {
toast.error('Credentials are required', {
position: 'bottom-center'
});
try {
if (username === '' || password === '') {
toast.error('Credentials are required', {
position: 'bottom-center'
});
return false;
}
return false;
}
if (authType === '') {
toast.error('Authentication type is required', {
position: 'bottom-center'
});
if (authType === '') {
toast.error('Authentication type is required', {
position: 'bottom-center'
});
return false;
}
return false;
}
const response = await axios.post('/api/auth/login', {
username,
password,
authType,
remember
});
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
password,
authType,
remember
})
});
if (response.status === 200 && response.data) {
if (response.data.data?.hostname && response.data.data?.token) {
console.log('Received login data:', response.data.data);
const responseData = await parseJSONResponse(response);
storage.hostname = response.data.data.hostname;
storage.nodeId = response.data.data.nodeId || '';
storage.token = response.data.data.token || '';
storage.clusterToken = response.data.data.clusterToken || '';
if (response.status === 200 && responseData) {
if (responseData.data?.hostname && responseData.data?.token) {
console.log('Received login data:', responseData.data);
console.log('Login response:', response.data);
return true;
} else {
toast.error('Invalid response received', {
position: 'bottom-center'
});
}
} else {
return false;
}
} catch (error) {
console.error('Login error:', error);
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
const data = axiosError.response?.data as APIResponse;
handleAPIError(data);
if (data.error) {
if (data.error.includes('only_admin_allowed')) {
toast.error('Only admin users can log in', {
position: 'bottom-center'
});
} else {
toast.error('Authentication failed', {
position: 'bottom-center'
});
}
}
} else {
toast.error('Fatal error logging in, check logs!', {
position: 'bottom-center'
});
}
return false;
}
storage.hostname = responseData.data.hostname;
storage.nodeId = responseData.data.nodeId || '';
storage.token = responseData.data.token || '';
storage.clusterToken = responseData.data.clusterToken || '';
return false;
console.log('Login response:', responseData);
return true;
} else {
toast.error('Invalid response received', {
position: 'bottom-center'
});
}
return false;
}
const data = (responseData || {}) as APIResponse;
handleAPIError(data);
if (data.error) {
if (data.error.includes('only_admin_allowed')) {
toast.error('Only admin users can log in', {
position: 'bottom-center'
});
} else {
toast.error('Authentication failed', {
position: 'bottom-center'
});
}
} else {
toast.error('Authentication failed', {
position: 'bottom-center'
});
}
return false;
} catch (error) {
console.error('Login error:', error);
toast.error('Fatal error logging in, check logs!', {
position: 'bottom-center'
});
return false;
}
return false;
}
export function getToken(): string | null {
if (browser) {
return storage.token;
}
if (browser) {
return storage.token;
}
return null;
return null;
}
export function getClusterToken(): string | null {
if (browser) {
return storage.clusterToken;
}
if (browser) {
return storage.clusterToken;
}
return null;
return null;
}
export async function isTokenValid(): Promise<boolean> {
if (!storage.token) {
return false;
}
if (!storage.token) {
return false;
}
try {
const response = await axios.get('/api/health/basic', {
headers: {
Authorization: `Bearer ${storage.token}`
}
});
try {
const response = await fetch('/api/health/basic', {
headers: {
Authorization: `Bearer ${storage.token}`
}
});
if (response.status < 400) {
if (response.data?.hostname) {
storage.hostname = response.data.hostname;
}
if (response.data?.nodeId) {
storage.nodeId = response.data.nodeId;
}
return true;
}
} catch (_e: unknown) {
return false;
}
const responseData = await parseJSONResponse(response);
return false;
if (response.status < 400) {
if (responseData?.hostname) {
storage.hostname = responseData.hostname;
}
if (responseData?.nodeId) {
storage.nodeId = responseData.nodeId;
}
return true;
}
} catch (_e: unknown) {
return false;
}
return false;
}
export async function isClusterTokenValid(): Promise<boolean> {
try {
const clusterToken = storage.clusterToken;
if (!clusterToken) {
return true;
}
try {
const clusterToken = storage.clusterToken;
if (!clusterToken) {
return true;
}
const response = await axios.get('/api/health/basic', {
headers: {
Authorization: `Bearer ${clusterToken}`,
'X-Cluster-Token': `Bearer ${clusterToken}`
}
});
const response = await fetch('/api/health/basic', {
headers: {
Authorization: `Bearer ${clusterToken}`,
'X-Cluster-Token': `Bearer ${clusterToken}`
}
});
if (response.status < 400) {
if (response.data?.hostname) {
// setLocalStorage('hostname', response.data.hostname);
storage.hostname = response.data.hostname;
}
if (response.data?.nodeId) {
// setLocalStorage('nodeId', response.data.nodeId);
storage.nodeId = response.data.nodeId;
}
return true;
} else {
localStorage.removeItem('clusterToken');
}
} catch (_e: unknown) {
return false;
}
const responseData = await parseJSONResponse(response);
return false;
if (response.status < 400) {
if (responseData?.hostname) {
// setLocalStorage('hostname', response.data.hostname);
storage.hostname = responseData.hostname;
}
if (responseData?.nodeId) {
// setLocalStorage('nodeId', response.data.nodeId);
storage.nodeId = responseData.nodeId;
}
return true;
} else {
localStorage.removeItem('clusterToken');
}
} catch (_e: unknown) {
return false;
}
return false;
}
export async function logOut(message?: string) {
const token = storage.token;
const token = storage.token;
if (token) {
storage.oldToken = token;
}
if (token) {
storage.oldToken = token;
}
storage.token = '';
storage.clusterToken = '';
storage.hostname = '';
storage.nodeId = '';
storage.token = '';
storage.clusterToken = '';
storage.hostname = '';
storage.nodeId = '';
if (browser) {
localStorage.removeItem('token');
localStorage.removeItem('hostname');
localStorage.removeItem('nodeId');
localStorage.removeItem('clusterToken');
}
if (browser) {
localStorage.removeItem('token');
localStorage.removeItem('hostname');
localStorage.removeItem('nodeId');
localStorage.removeItem('clusterToken');
}
if (message) {
toast.success(message, {
position: 'bottom-center'
});
}
if (message) {
toast.success(message, {
position: 'bottom-center'
});
}
goto('/', {
replaceState: true,
state: {
loggedOut: true
}
});
goto('/', {
replaceState: true,
state: {
loggedOut: true
}
});
}
export async function revokeJWT() {
try {
const oldtoken = storage.oldToken;
if (oldtoken) {
await axios.get('/api/auth/logout', {
headers: {
Authorization: `Bearer ${oldtoken}`
}
});
try {
const oldtoken = storage.oldToken;
if (oldtoken) {
await fetch('/api/auth/logout', {
headers: {
Authorization: `Bearer ${oldtoken}`
}
});
storage.oldToken = '';
}
} catch (_e: unknown) {
adze.error('Failed to revoke JWT');
}
storage.oldToken = '';
}
} catch (_e: unknown) {
console.error('Failed to revoke JWT');
}
}
export function getJWTClaims(): JWTClaims | null {
const token = getToken();
if (token) {
try {
return JSON.parse(atob(token.split('.')[1])) as JWTClaims;
} catch (e) {
return null;
}
}
const token = getToken();
if (token) {
try {
return JSON.parse(atob(token.split('.')[1])) as JWTClaims;
} catch (e) {
return null;
}
}
return null;
return null;
}
export async function getTokenHash(): Promise<string | null> {
const token = getToken();
if (!token) {
return null;
}
const token = getToken();
if (!token) {
return null;
}
return await sha256(token);
return await sha256(token);
}
export async function isInitialized(): Promise<boolean[]> {
try {
const response = await axios.get('/api/health/basic', {
headers: {
Authorization: `Bearer ${storage.token}`
}
});
try {
const response = await fetch('/api/health/basic', {
headers: {
Authorization: `Bearer ${storage.token}`
}
});
if (response.status === 200 && response.data && response.data.data) {
return [response.data.data.initialized === true, response.data.data.restarted === true];
}
} catch (_e: unknown) {
return [false, false];
}
const responseData = await parseJSONResponse(response);
return [false, false];
if (response.status === 200 && responseData && responseData.data) {
return [responseData.data.initialized === true, responseData.data.restarted === true];
}
} catch (_e: unknown) {
return [false, false];
}
return [false, false];
}
+286 -108
View File
@@ -12,135 +12,313 @@ import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { storage } from '$lib';
import type { APIResponse } from '$lib/types/common';
import adze from 'adze';
import axios, { AxiosError, type AxiosInstance, type InternalAxiosRequestConfig } from 'axios';
import { toast } from 'svelte-sonner';
export let ENDPOINT: string;
export let API_ENDPOINT: string;
if (browser) {
ENDPOINT = window.location.origin;
API_ENDPOINT = `${window.location.origin}/api`;
ENDPOINT = window.location.origin;
API_ENDPOINT = `${window.location.origin}/api`;
} else {
ENDPOINT = '';
API_ENDPOINT = '';
ENDPOINT = '';
API_ENDPOINT = '';
}
export const api: AxiosInstance = axios.create({
baseURL: API_ENDPOINT
});
export type APIRequestConfig = {
url: string;
method?: string;
headers?: Record<string, string>;
data?: unknown;
body?: BodyInit | null;
signal?: AbortSignal;
credentials?: RequestCredentials;
validateStatus?: (status: number) => boolean;
};
api.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
if (browser) {
if (storage.token) {
config.headers['Authorization'] = `Bearer ${storage.token}`;
}
export type APIClientResponse<T = unknown> = {
status: number;
data: T;
headers: Record<string, string>;
ok: boolean;
};
if (storage.clusterToken) {
config.headers['X-Cluster-Token'] = `Bearer ${storage.clusterToken}`;
}
type APIClientError = Error & {
response?: APIClientResponse;
request?: { url: string; method: string };
status?: number;
handled?: boolean;
};
const routeHost = window.location.pathname.split('/').filter(Boolean)[0] || '';
const pathBasedHost =
routeHost !== '' && routeHost !== 'datacenter' && routeHost !== 'login' ? routeHost : '';
const fallbackHost = pathBasedHost || storage.hostname || '';
const explicitHost =
typeof config.headers['X-Current-Hostname'] === 'string'
? config.headers['X-Current-Hostname']
: '';
const defaultValidateStatus = (status: number): boolean => status >= 200 && status < 300;
if (explicitHost) {
config.headers['X-Current-Hostname'] = explicitHost;
} else if (
(config.url === '/vm' || config.url === '/jail') &&
config.method?.toLowerCase() === 'post'
) {
try {
if (config.data?.node) {
config.headers['X-Current-Hostname'] = `${config.data.node}`;
} else if (fallbackHost) {
config.headers['X-Current-Hostname'] = `${fallbackHost}`;
}
} catch (e) {
adze.withEmoji.error('Error parsing request data:', e);
if (fallbackHost) {
config.headers['X-Current-Hostname'] = `${fallbackHost}`;
}
}
} else if (fallbackHost) {
config.headers['X-Current-Hostname'] = `${fallbackHost}`;
}
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
function toHeaderRecord(headers: Headers): Record<string, string> {
const record: Record<string, string> = {};
headers.forEach((value, key) => {
record[key] = value;
});
return record;
}
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 && browser) {
toast.error('Session expired, please login again', {
position: 'bottom-center'
});
goto('/login');
return;
}
handleAxiosError(error);
return Promise.reject(error);
}
);
function normalizeURL(url: string): string {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
if (url.startsWith('/')) {
return `${API_ENDPOINT}${url}`;
}
return `${API_ENDPOINT}/${url}`;
}
function applyRequestDefaults(config: APIRequestConfig): APIRequestConfig {
const nextConfig: APIRequestConfig = {
...config,
headers: { ...(config.headers || {}) }
};
if (!browser) {
return nextConfig;
}
if (storage.token) {
nextConfig.headers!.Authorization = `Bearer ${storage.token}`;
}
if (storage.clusterToken) {
nextConfig.headers!['X-Cluster-Token'] = `Bearer ${storage.clusterToken}`;
}
const routeHost = window.location.pathname.split('/').filter(Boolean)[0] || '';
const pathBasedHost =
routeHost !== '' && routeHost !== 'datacenter' && routeHost !== 'login' ? routeHost : '';
const fallbackHost = pathBasedHost || storage.hostname || '';
const explicitHost =
typeof nextConfig.headers!['X-Current-Hostname'] === 'string'
? nextConfig.headers!['X-Current-Hostname']
: '';
if (explicitHost) {
nextConfig.headers!['X-Current-Hostname'] = explicitHost;
} else if (
(nextConfig.url === '/vm' || nextConfig.url === '/jail') &&
nextConfig.method?.toLowerCase() === 'post'
) {
try {
const bodyData = nextConfig.data as { node?: string } | undefined;
if (bodyData?.node) {
nextConfig.headers!['X-Current-Hostname'] = `${bodyData.node}`;
} else if (fallbackHost) {
nextConfig.headers!['X-Current-Hostname'] = `${fallbackHost}`;
}
} catch (e) {
console.error('Error parsing request data:', e);
if (fallbackHost) {
nextConfig.headers!['X-Current-Hostname'] = `${fallbackHost}`;
}
}
} else if (fallbackHost) {
nextConfig.headers!['X-Current-Hostname'] = `${fallbackHost}`;
}
return nextConfig;
}
function isAPIClientError(error: unknown): error is APIClientError {
return typeof error === 'object' && error !== null && 'request' in error;
}
function isBodyInit(value: unknown): value is BodyInit {
return (
typeof value === 'string' ||
value instanceof Blob ||
value instanceof FormData ||
value instanceof URLSearchParams ||
value instanceof ArrayBuffer ||
ArrayBuffer.isView(value) ||
value instanceof ReadableStream
);
}
async function parseResponseBody(response: Response): Promise<unknown> {
if (response.status === 204 || response.status === 205) {
return null;
}
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json') || contentType.includes('+json')) {
try {
return await response.json();
} catch (_e: unknown) {
return null;
}
}
try {
return await response.text();
} catch (_e: unknown) {
return null;
}
}
class FetchAPIClient {
async request<T = unknown>(config: APIRequestConfig): Promise<APIClientResponse<T>> {
const nextConfig = applyRequestDefaults(config);
const method = (nextConfig.method || 'GET').toUpperCase();
const url = normalizeURL(nextConfig.url);
const headers = { ...(nextConfig.headers || {}) };
let body: BodyInit | null = nextConfig.body ?? null;
if (body === null && nextConfig.data !== undefined) {
if (isBodyInit(nextConfig.data)) {
body = nextConfig.data;
} else {
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/json';
}
body = JSON.stringify(nextConfig.data);
}
}
try {
const response = await fetch(url, {
method,
headers,
body,
signal: nextConfig.signal,
credentials: nextConfig.credentials || 'same-origin'
});
const data = (await parseResponseBody(response)) as T;
const normalizedResponse: APIClientResponse<T> = {
status: response.status,
data,
headers: toHeaderRecord(response.headers),
ok: response.ok
};
const validateStatus = nextConfig.validateStatus || defaultValidateStatus;
if (!validateStatus(response.status)) {
const error: APIClientError = new Error(
`Request failed with status code ${response.status}`
);
error.response = normalizedResponse;
error.request = { url: nextConfig.url, method };
error.status = response.status;
if (response.status === 401 && browser) {
toast.error('Session expired, please login again', {
position: 'bottom-center'
});
goto('/login');
}
handleAxiosError(error);
error.handled = true;
throw error;
}
return normalizedResponse;
} catch (error: unknown) {
if (isAPIClientError(error) && error.handled) {
throw error;
}
const normalizedError: APIClientError =
isAPIClientError(error) && error.request
? error
: Object.assign(new Error('Network error'), {
request: { url: nextConfig.url, method }
});
handleAxiosError(normalizedError);
throw normalizedError;
}
}
get<T = unknown>(url: string, config: Omit<APIRequestConfig, 'url' | 'method'> = {}) {
return this.request<T>({ ...config, url, method: 'GET' });
}
post<T = unknown>(
url: string,
data?: unknown,
config: Omit<APIRequestConfig, 'url' | 'method' | 'data'> = {}
) {
return this.request<T>({ ...config, url, method: 'POST', data });
}
put<T = unknown>(
url: string,
data?: unknown,
config: Omit<APIRequestConfig, 'url' | 'method' | 'data'> = {}
) {
return this.request<T>({ ...config, url, method: 'PUT', data });
}
patch<T = unknown>(
url: string,
data?: unknown,
config: Omit<APIRequestConfig, 'url' | 'method' | 'data'> = {}
) {
return this.request<T>({ ...config, url, method: 'PATCH', data });
}
delete<T = unknown>(url: string, config: Omit<APIRequestConfig, 'url' | 'method'> = {}) {
return this.request<T>({ ...config, url, method: 'DELETE' });
}
}
export const api = new FetchAPIClient();
export function handleAxiosError(error: unknown): void {
if (!browser) return;
if (!browser) return;
if (!axios.isAxiosError(error)) {
toast.error('An unexpected error occurred', {
position: 'bottom-center'
});
adze.withEmoji.error('An unexpected error occurred');
return;
}
if (!isAPIClientError(error)) {
toast.error('An unexpected error occurred', {
position: 'bottom-center'
});
console.error('An unexpected error occurred');
return;
}
const axiosError = error as AxiosError<{ message?: string }>;
if (axiosError.response) {
const errorMessage =
axiosError.response.data?.message || axiosError.message || 'An error occurred';
adze.withEmoji.error(
JSON.stringify({
status: axiosError.response.status,
data: axiosError.response.data,
message: errorMessage
})
);
} else if (axiosError.request) {
adze.withEmoji.error('No response:', axiosError.request);
}
const axiosError = error as APIClientError;
if (axiosError.response) {
const responseData = axiosError.response.data as { message?: string } | undefined;
const errorMessage =
responseData?.message || axiosError.message || 'An error occurred';
console.error(
JSON.stringify({
status: axiosError.response.status,
data: axiosError.response.data,
message: errorMessage
})
);
} else if (axiosError.request) {
console.error('No response:', axiosError.request);
}
}
export function handleAPIResponse(
response: APIResponse,
messages: {
success?: string;
error?: string;
info?: string;
warn?: string;
}
response: APIResponse,
messages: {
success?: string;
error?: string;
info?: string;
warn?: string;
}
): void {
if (response.status === 'error') {
adze.withEmoji.error(response);
toast.error(messages.error || 'Operation failed', {
position: 'bottom-center'
});
}
if (response.status === 'error') {
console.error(response);
toast.error(messages.error || 'Operation failed', {
position: 'bottom-center'
});
}
if (response.status === 'success') {
toast.success(messages.success || 'Operation successful', {
position: 'bottom-center'
});
}
if (response.status === 'success') {
toast.success(messages.success || 'Operation successful', {
position: 'bottom-center'
});
}
}
+18 -4
View File
@@ -10,7 +10,19 @@
import { storage } from '$lib';
import { reload } from '$lib/stores/api.svelte';
import axios from 'axios';
async function parseJSONResponse(response: Response): Promise<any> {
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json') && !contentType.includes('+json')) {
return null;
}
try {
return await response.json();
} catch (_e: unknown) {
return null;
}
}
type SSETokenResponse = {
token: string;
@@ -55,14 +67,16 @@ async function fetchSSEToken(): Promise<string | null> {
}
try {
const response = await axios.get('/api/auth/sse-token', {
const response = await fetch('/api/auth/sse-token', {
headers: {
Authorization: `Bearer ${storage.token}`
}
});
if (response.status < 400 && response.data?.data) {
const data = response.data.data as SSETokenResponse;
const responseData = await parseJSONResponse(response);
if (response.status < 400 && responseData?.data) {
const data = responseData.data as SSETokenResponse;
if (data.token) {
return data.token;
}
+95 -56
View File
@@ -1,5 +1,13 @@
<script lang="ts">
import { Arc, ArcChart, Group, LinearGradient, Text } from 'layerchart';
import { Chart } from 'svelte-echarts';
import { init, use } from 'echarts/core';
import { GaugeChart } from 'echarts/charts';
import { GraphicComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import { mode } from 'mode-watcher';
import type { EChartsOption } from 'echarts';
use([GaugeChart, GraphicComponent, CanvasRenderer]);
interface Props {
title: string;
@@ -9,62 +17,93 @@
let { title, subtitle, value }: Props = $props();
const ranges = [
{ max: 33, color: 'fill-green-500' },
{ max: 66, color: 'fill-yellow-500' },
{ max: 100, color: 'fill-red-500' }
] as const;
const getColor = (v: number) => {
if (v <= 33) return '#22c55e';
if (v <= 66) return '#eab308';
return '#ef4444';
};
const color = $derived(ranges.find((r) => value <= r.max)?.color ?? 'fill-red-500');
let options: EChartsOption = $derived({
graphic: {
elements: [
{
type: 'text',
left: 'center',
top: 40,
style: {
text: title,
fontSize: 16,
fontWeight: 'bold',
fill: mode.current === 'dark' ? '#ffffff' : '#000000'
}
},
...(subtitle
? [
{
type: 'text',
left: 'center',
top: 65,
style: {
text: subtitle,
fontSize: 12,
fontWeight: 500,
fill: mode.current === 'dark' ? '#a1a1aa' : '#71717a'
}
}
]
: []),
{
type: 'text',
left: 'center',
top: 85,
style: {
text: `${Math.round(value)}%`,
fontSize: 30,
fontWeight: 'bold',
fill: mode.current === 'dark' ? '#ffffff' : '#000000'
}
}
]
},
series: [
{
type: 'gauge',
startAngle: 210,
endAngle: -30,
min: 0,
max: 100,
center: ['50%', '50%'],
radius: '100%',
pointer: { show: false },
progress: {
show: true,
overlap: false,
roundCap: true,
clip: false,
itemStyle: {
color: getColor(value),
opacity: 0.7
}
},
axisLine: {
lineStyle: {
width: 15,
color: [
[1, mode.current === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)']
]
}
},
splitLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
detail: { show: false },
title: { show: false },
data: [{ value: value }]
}
]
});
</script>
<div class="h-[150px] w-[200px] overflow-auto rounded-sm">
<ArcChart>
{#snippet marks()}
<Group y={20}>
<Arc
{value}
domain={[0, 100]}
outerRadius={80}
innerRadius={-15}
cornerRadius={20}
padAngle={0.02}
range={[-120, 120]}
class={`${color} opacity-70`}
track={{ class: 'fill-none stroke-primary/20' }}
>
{#snippet children({ value })}
<Text
value={title}
textAnchor="middle"
verticalAnchor="end"
y={-35}
class="text-md font-bold"
fill="currentColor"
/>
{#if subtitle}
<Text
value={subtitle}
textAnchor="middle"
verticalAnchor="end"
y={-10}
class="text-xs font-medium"
fill="currentColor"
/>
{/if}
<Text
value={Math.round(value) + '%'}
textAnchor="middle"
verticalAnchor="start"
y={10}
class="text-3xl font-bold"
fill="currentColor"
/>
{/snippet}
</Arc>
</Group>
{/snippet}
</ArcChart>
<div class="h-37.5 w-50 overflow-hidden rounded-sm">
<Chart {init} {options} class="h-full w-full" />
</div>
@@ -1,331 +0,0 @@
<script lang="ts">
import Button from '$lib/components/ui/button/button.svelte';
import * as Card from '$lib/components/ui/card/index.js';
import type { AreaChartElement } from '$lib/types/components/chart';
import { switchColor } from '$lib/utils/chart';
import {
CategoryScale,
Chart,
Filler,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip
} from 'chart.js';
import zoomPlugin from 'chartjs-plugin-zoom';
import { format } from 'date-fns';
import humanFormat from 'human-format';
import { onDestroy, onMount } from 'svelte';
interface Props {
title?: string;
description?: string;
icon?: string;
elements: AreaChartElement[];
formatSize?: boolean;
containerClass?: string;
showResetButton?: boolean;
chart: Chart | null;
percentage?: boolean;
}
let {
title = '',
description = '',
icon = '',
elements,
formatSize = false,
containerClass = 'p-5',
showResetButton = true,
chart = $bindable(),
percentage = false
}: Props = $props();
let data = $derived.by(() => {
if (!elements?.length) return [];
const THRESH = 60_000;
const series = elements.map(({ field, data: pts }) => ({
field,
points: pts
.map((p) => ({ t: new Date(p.date).getTime(), v: p.value }))
.sort((a, b) => a.t - b.t)
}));
series.sort((a, b) => a.points.length - b.points.length);
const [base, ...others] = series;
const out = [];
for (const { t: bt, v: bv } of base.points) {
const rec = { date: new Date(bt), [base.field]: bv };
let good = true;
for (const { field, points } of others) {
let bestDiff = Infinity,
bestVal = null;
for (const { t, v } of points) {
const d = Math.abs(t - bt);
if (d < bestDiff) {
bestDiff = d;
bestVal = v;
}
if (t - bt > bestDiff) break;
}
if (bestVal === null || bestDiff > THRESH) {
good = false;
break;
}
rec[field] = bestVal;
}
if (good) out.push(rec);
}
return out;
});
let series = $derived.by(() => {
if (!elements?.length) return [];
return elements.map((element) => ({
key: element.field,
label: element.label,
color: element.color
}));
});
const labels = $derived.by(() => {
return data.map((v) => [
format(new Date(v.date), 'dd/MM/yyyy'),
format(new Date(v.date), 'HH:mm')
]);
});
let datasets = $derived.by(() => {
return series.map((s, i) => ({
label: s.label,
data: data.map((d) => Number(d[s.key])),
borderColor: switchColor(s.color),
backgroundColor: switchColor(s.color, 0.2),
fill: i === 0 ? 'origin' : '-1',
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
order: s.label === 'CPU Usage' ? 2 : 1
}));
});
let canvas: HTMLCanvasElement;
let zoomEnabled = $state(false);
Chart.register(
LineController,
LineElement,
PointElement,
LinearScale,
CategoryScale,
Title,
Tooltip,
Legend,
Filler,
zoomPlugin
);
onMount(() => {
chart = new Chart(canvas, {
type: 'line',
data: {
labels,
datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
transitions: {
zoom: {
animation: {
duration: 1000,
easing: 'easeOutCubic'
}
}
},
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
title: (tooltipItems) => {
try {
const dataIndex = tooltipItems[0].dataIndex;
const date = data[dataIndex]?.date;
if (!date) return 'Invalid Date';
const dateObj = date instanceof Date ? date : new Date(date);
if (isNaN(dateObj.getTime())) return 'Invalid Date';
return [format(dateObj, 'dd/MM/yyyy'), format(dateObj, 'HH:mm:ss')];
} catch (e) {
return 'Invalid Date';
}
},
label: (tooltipItem) => {
const datasetLabel = tooltipItem.dataset.label || '';
const value = Number(tooltipItem.raw);
return `${datasetLabel}: ${
formatSize ? humanFormat(value) : value.toLocaleString()
}`;
}
}
},
zoom: {
zoom: {
wheel: { enabled: zoomEnabled },
pinch: { enabled: zoomEnabled },
mode: 'xy'
},
pan: {
enabled: zoomEnabled,
mode: 'xy'
}
}
},
scales: {
x: {
title: { color: '#ccc', display: true, text: 'Date' },
ticks: {
callback: function (value, index, ticks) {
try {
const date = data[index]?.date;
if (!date) return '';
const dateObj = date instanceof Date ? date : new Date(date);
if (isNaN(dateObj.getTime())) return '';
return [format(date, 'dd/MM/yyyy'), format(date, 'HH:mm')];
} catch (e) {
return '';
}
}
},
grid: {
color: '#333' // Optional: X-axis grid line color
}
},
y: {
min: percentage ? 0 : undefined,
max: percentage ? 100 : undefined,
beginAtZero: true,
title: {
color: '#ccc',
display: true,
text: 'Value'
},
ticks: {
callback: function (value) {
const numValue = Number(value);
return formatSize ? humanFormat(numValue) : numValue.toLocaleString();
}
},
grid: {
color: '#333' // Optional: X-axis grid line color
}
}
}
}
});
setTimeout(() => chart?.resize(), 300);
});
$effect(() => {
if (chart) {
if (
chart.options &&
chart.options.plugins &&
chart.options.plugins.zoom &&
chart.options.plugins.zoom.zoom &&
chart.options.plugins.zoom.zoom.wheel
) {
chart.options.plugins.zoom.zoom.wheel.enabled = zoomEnabled;
}
if (
chart.options &&
chart.options.plugins &&
chart.options.plugins.zoom &&
chart.options.plugins.zoom.zoom &&
chart.options.plugins.zoom.zoom.pinch
) {
chart.options.plugins.zoom.zoom.pinch.enabled = zoomEnabled;
}
if (
chart.options &&
chart.options.plugins &&
chart.options.plugins.zoom &&
chart.options.plugins.zoom.pan
) {
chart.options.plugins.zoom.pan.enabled = zoomEnabled;
}
chart.update('none');
}
});
onDestroy(() => {
if (chart) {
chart.destroy();
chart = null;
}
});
</script>
<Card.Root class={containerClass}>
<Card.Header class="p-0">
<Card.Title class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2">
{#if icon}
<span class={icon}></span>
{/if}
{title}
</div>
<div class="flex items-center gap-2">
<Button
onclick={() => {
zoomEnabled = !zoomEnabled;
}}
variant={zoomEnabled ? 'default' : 'outline'}
size="sm"
class="h-8"
>
<span class="icon-[material-symbols--zoom-in] h-4 w-4"></span>
{zoomEnabled ? 'Disable Zoom' : 'Enable Zoom'}
</Button>
{#if showResetButton && zoomEnabled}
<Button
onclick={() => {
chart?.resetZoom();
}}
variant="outline"
size="sm"
class="h-8"
>
<span class="icon-[carbon--reset] h-4 w-4"></span>
Reset zoom
</Button>
{/if}
</div>
</Card.Title>
{#if description}
<Card.Description>{description}</Card.Description>
{/if}
</Card.Header>
<Card.Content class="h-full min-h-[300px] w-full p-0">
<canvas bind:this={canvas} style="width: 100%; height: 100%; display: block;"></canvas>
</Card.Content>
</Card.Root>
@@ -1,196 +0,0 @@
<script lang="ts">
import Button from '$lib/components/ui/button/button.svelte';
import type { SeriesDataWithBaseline } from '$lib/types/common';
import { switchColor } from '$lib/utils/chart';
import {
BarController,
BarElement,
CategoryScale,
Chart,
Legend,
LinearScale,
Title,
Tooltip,
type ChartConfiguration
} from 'chart.js';
import zoomPlugin from 'chartjs-plugin-zoom';
import { format } from 'date-fns';
import humanFormat from 'human-format';
import { onDestroy, onMount } from 'svelte';
Chart.register(
CategoryScale,
LinearScale,
BarController,
BarElement,
Title,
Tooltip,
Legend,
zoomPlugin
);
type Colors = {
baseline: string;
value: string;
};
interface Props {
data: SeriesDataWithBaseline[];
colors: Colors;
formatter?: 'size-formatter' | 'default';
icon?: string;
title?: string;
showResetButton?: boolean;
chart?: Chart;
}
let {
title,
icon,
data,
colors,
formatter = 'default',
showResetButton = true,
chart = $bindable()
}: Props = $props();
let canvas: HTMLCanvasElement;
const chartConfig: ChartConfiguration<'bar'> = {
type: 'bar',
data: {
labels: data.map((d) => d.name),
datasets: [
{
label: 'Baseline',
data: data.map((d) => d.baseline),
backgroundColor: switchColor(colors.baseline, 0.6),
borderColor: switchColor(colors.baseline, 1),
borderWidth: 1
},
{
label: 'Value',
data: data.map((d) => d.value),
backgroundColor: switchColor(colors.value, 0.6),
borderColor: switchColor(colors.value, 1),
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
},
tooltip: {
callbacks: {
label: function (context) {
const label = context.label || '';
const value = context.raw as number;
const displayValue = formatter === 'size-formatter' ? humanFormat(value) : value;
return `${label}: ${displayValue}`;
}
}
},
zoom: {
pan: { enabled: true, mode: 'xy' },
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: 'xy'
}
}
},
scales: {
x: {
title: { color: '#ccc', display: true, text: 'Date' },
grid: {
color: '#333'
}
},
y: {
beginAtZero: true,
title: {
color: '#ccc',
display: true,
text: 'Value'
},
ticks: {
callback: function (value) {
const numValue = Number(value);
return formatter == 'size-formatter'
? humanFormat(numValue)
: numValue.toLocaleString();
}
},
grid: {
color: '#333'
}
}
},
interaction: {
mode: 'index',
intersect: false
}
}
};
onMount(() => {
if (canvas) {
chart = new Chart(canvas, chartConfig);
}
});
$effect(() => {
if (chart && data && data.length > 0) {
chart.data.labels = data.map((d) => d.name);
chart.data.datasets[0].data = data.map((d) => d.baseline);
chart.data.datasets[0].backgroundColor = switchColor(colors.baseline, 0.6);
chart.data.datasets[0].borderColor = switchColor(colors.baseline, 1);
chart.data.datasets[1].data = data.map((d) => d.value);
chart.data.datasets[1].backgroundColor = switchColor(colors.value, 0.6);
chart.data.datasets[1].borderColor = switchColor(colors.value, 1);
chart.update();
}
});
onDestroy(() => {
chart?.destroy();
});
</script>
<div class="relative min-h-[300px] w-full">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2">
{#if icon}
<span class="icon-[{icon}] h-5 w-5"></span>
{/if}
{title}
</div>
{#if showResetButton}
<div>
<Button
onclick={() => {
chart?.resetZoom();
}}
variant="outline"
size="sm"
class="h-8"
>
<span class="icon-[carbon--reset] h-4 w-4"></span>
Reset zoom
</Button>
</div>
{/if}
</div>
<div class="h-full min-h-[300px] w-full">
<canvas bind:this={canvas} class="h-full w-full"></canvas>
</div>
</div>
@@ -1,80 +0,0 @@
<script lang="ts">
import type { PieChartData } from '$lib/types/common';
import { switchColor } from '$lib/utils/chart';
import { ArcElement, Chart, Legend, PieController, Tooltip } from 'chart.js';
import humanFormat from 'human-format';
import { onDestroy, onMount } from 'svelte';
interface Props {
containerClass: string;
data: PieChartData[];
formatter?: 'size-formatter' | 'default';
}
const { containerClass, data: rawData, formatter = 'default' }: Props = $props();
let data = $derived.by(() => {
if (!rawData || rawData.length === 0) return [];
return rawData.map((item, index) => ({
...item,
color: item.color || `chart-${(index % 5) + 1}`
}));
});
let canvas: HTMLCanvasElement;
let chart: Chart;
Chart.register(ArcElement, PieController, Tooltip, Legend);
onMount(() => {
chart = new Chart(canvas, {
type: 'pie',
data: {
labels: data.map((d) => d.label),
datasets: [
{
data: data.map((d) => d.value),
backgroundColor: data.map((d) => switchColor(d.color, 0.6)),
borderColor: data.map((d) => switchColor(d.color, 1)),
borderWidth: 1
}
]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
callbacks: {
label: function (context) {
const label = context.label || '';
const value = context.raw as number;
const displayValue = formatter === 'size-formatter' ? humanFormat(value) : value;
return `${label}: ${displayValue}`;
}
}
}
}
}
});
});
$effect(() => {
if (chart && data && data.length > 0) {
chart.data.labels = data.map((d) => d.label);
chart.data.datasets[0].data = data.map((d) => d.value);
chart.data.datasets[0].backgroundColor = data.map((d) => switchColor(d.color));
chart.update();
}
});
onDestroy(() => {
chart?.destroy();
});
</script>
<div class={containerClass}>
<canvas bind:this={canvas}></canvas>
</div>
@@ -1,80 +0,0 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import ChartStyle from './chart-style.svelte';
import { setChartContext, type ChartConfig } from './chart-utils.js';
const uid = $props.id();
let {
ref = $bindable(null),
id = uid,
class: className,
children,
config,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
config: ChartConfig;
} = $props();
const chartId = `chart-${id || uid.replace(/:/g, '')}`;
setChartContext({
get config() {
return config;
}
});
</script>
<div
bind:this={ref}
data-chart={chartId}
data-slot="chart"
class={cn(
'flex aspect-video justify-center overflow-visible text-xs',
// Overrides
//
// Stroke around dots/marks when hovering
'[&_.stroke-white]:stroke-transparent',
// override the default stroke color of lines
'[&_.lc-line]:stroke-border/50',
// by default, layerchart shows a line intersecting the point when hovering, this hides that
'[&_.lc-highlight-line]:stroke-0',
// by default, when you hover a point on a stacked series chart, it will drop the opacity
// of the other series, this overrides that
'[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text-svg]:overflow-visible [&_.lc-text]:text-xs',
// We don't want the little tick lines between the axis labels and the chart, so we remove
// the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
// chart.
'[&_.lc-axis-tick]:stroke-0',
// We don't want to display the rule on the x/y axis, as there is already going to be
// a grid line there and rule ends up overlapping the marks because it is rendered after
// the marks
'[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0',
'[&_.lc-grid-x-radial-line]:stroke-border [&_.lc-grid-x-radial-circle]:stroke-border',
'[&_.lc-grid-y-radial-line]:stroke-border [&_.lc-grid-y-radial-circle]:stroke-border',
// Legend adjustments
'[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5',
'[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4',
'[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]',
// Labels
'[&_.lc-labels-text:not([fill])]:fill-foreground [&_text]:stroke-transparent',
// Tick labels on th x/y axes
'[&_.lc-axis-tick-label]:fill-muted-foreground [&_.lc-axis-tick-label]:font-normal',
'[&_.lc-tooltip-rects-g]:fill-transparent',
'[&_.lc-layout-svg-g]:fill-transparent',
'[&_.lc-root-container]:w-full',
className
)}
{...restProps}
>
<ChartStyle id={chartId} {config} />
{@render children?.()}
</div>
@@ -1,36 +0,0 @@
<script lang="ts">
import { THEMES, type ChartConfig } from './chart-utils.js';
let { id, config }: { id: string; config: ChartConfig } = $props();
const colorConfig = $derived(
config ? Object.entries(config).filter(([, config]) => config.theme || config.color) : null
);
const themeContents = $derived.by(() => {
if (!colorConfig || !colorConfig.length) return;
const themeContents = [];
for (let [_theme, prefix] of Object.entries(THEMES)) {
let content = `${prefix} [data-chart=${id}] {\n`;
const color = colorConfig.map(([key, itemConfig]) => {
const theme = _theme as keyof typeof itemConfig.theme;
const color = itemConfig.theme?.[theme] || itemConfig.color;
return color ? `\t--color-${key}: ${color};` : null;
});
content += color.join('\n') + '\n}';
themeContents.push(content);
}
return themeContents.join('\n');
});
</script>
{#if themeContents}
{#key id}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html `<style>${themeContents}</style>`}
{/key}
{/if}
@@ -1,155 +0,0 @@
<script lang="ts">
import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils.js';
import { getTooltipContext, Tooltip as TooltipPrimitive } from 'layerchart';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from './chart-utils.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function defaultFormatter(value: any, _payload: TooltipPayload[]) {
return `${value}`;
}
let {
ref = $bindable(null),
class: className,
hideLabel = false,
indicator = 'dot',
hideIndicator = false,
labelKey,
label,
labelFormatter = defaultFormatter,
labelClassName,
formatter,
nameKey,
color,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
hideLabel?: boolean;
label?: string;
indicator?: 'line' | 'dot' | 'dashed';
nameKey?: string;
labelKey?: string;
hideIndicator?: boolean;
labelClassName?: string;
labelFormatter?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
((value: any, payload: TooltipPayload[]) => string | number | Snippet) | null;
formatter?: Snippet<
[
{
value: unknown;
name: string;
item: TooltipPayload;
index: number;
payload: TooltipPayload[];
}
]
>;
} = $props();
const chart = useChart();
const tooltipCtx = getTooltipContext();
const formattedLabel = $derived.by(() => {
if (hideLabel || !tooltipCtx.payload?.length) return null;
const [item] = tooltipCtx.payload;
const key = labelKey || item?.label || item?.name || 'value';
const itemConfig = getPayloadConfigFromPayload(chart.config, item, key);
const value =
!labelKey && typeof label === 'string'
? chart.config[label as keyof typeof chart.config]?.label || label
: (itemConfig?.label ?? item.label);
if (!value) return null;
if (!labelFormatter) return value;
return labelFormatter(value, tooltipCtx.payload);
});
const nestLabel = $derived(tooltipCtx.payload.length === 1 && indicator !== 'dot');
</script>
{#snippet TooltipLabel()}
{#if formattedLabel}
<div class={cn('font-medium', labelClassName)}>
{#if typeof formattedLabel === 'function'}
{@render formattedLabel()}
{:else}
{formattedLabel}
{/if}
</div>
{/if}
{/snippet}
<TooltipPrimitive.Root variant="none">
<div
class={cn(
'border-border/50 bg-background grid min-w-[9rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl',
className
)}
{...restProps}
>
{#if !nestLabel}
{@render TooltipLabel()}
{/if}
<div class="grid gap-1.5">
{#each tooltipCtx.payload as item, i (item.key + i)}
{@const key = `${nameKey || item.key || item.name || 'value'}`}
{@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)}
{@const indicatorColor = color || item.payload?.color || item.color}
<div
class={cn(
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5',
indicator === 'dot' && 'items-center'
)}
>
{#if formatter && item.value !== undefined && item.name}
{@render formatter({
value: item.value,
name: item.name,
item,
index: i,
payload: tooltipCtx.payload
})}
{:else}
{#if itemConfig?.icon}
<itemConfig.icon />
{:else if !hideIndicator}
<div
style="--color-bg: {indicatorColor}; --color-border: {indicatorColor};"
class={cn('border-(--color-border) bg-(--color-bg) shrink-0 rounded-[2px]', {
'size-2.5': indicator === 'dot',
'h-full w-1': indicator === 'line',
'w-0 border-[1.5px] border-dashed bg-transparent': indicator === 'dashed',
'my-0.5': nestLabel && indicator === 'dashed'
})}
></div>
{/if}
<div
class={cn(
'flex flex-1 shrink-0 justify-between leading-none',
nestLabel ? 'items-end' : 'items-center'
)}
>
<div class="grid gap-1.5">
{#if nestLabel}
{@render TooltipLabel()}
{/if}
<span class="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{#if item.value}
<span class="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
{/if}
</div>
{/if}
</div>
{/each}
</div>
</div>
</TooltipPrimitive.Root>
@@ -1,66 +0,0 @@
import type { Tooltip } from 'layerchart';
import { getContext, setContext, type Component, type ComponentProps, type Snippet } from 'svelte';
export const THEMES = { light: '', dark: '.dark' } as const;
export type ChartConfig = {
[k in string]: {
label?: string;
icon?: Component;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;
export type TooltipPayload = ExtractSnippetParams<
ComponentProps<typeof Tooltip.Root>['children']
>['payload'][number];
// Helper to extract item config from a payload.
export function getPayloadConfigFromPayload(
config: ChartConfig,
payload: TooltipPayload,
key: string
) {
if (typeof payload !== 'object' || payload === null) return undefined;
const payloadPayload =
'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (payload.key === key) {
configLabelKey = payload.key;
} else if (payload.name === key) {
configLabelKey = payload.name;
} else if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
type ChartContextValue = {
config: ChartConfig;
};
const chartContextKey = Symbol('chart-context');
export function setChartContext(value: ChartContextValue) {
return setContext(chartContextKey, value);
}
export function useChart() {
return getContext<ChartContextValue>(chartContextKey);
}
-6
View File
@@ -1,6 +0,0 @@
import ChartContainer from './chart-container.svelte';
import ChartTooltip from './chart-tooltip.svelte';
export { getPayloadConfigFromPayload, type ChartConfig } from './chart-utils.js';
export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip };
+120 -121
View File
@@ -12,169 +12,168 @@ import { storage } from '$lib';
import { api } from '$lib/api/common';
import { reload } from '$lib/stores/api.svelte';
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
import adze from 'adze';
import { z } from 'zod/v4';
import { kvStorage } from '$lib/types/db';
export type APIRequestOptions = {
raw?: boolean;
hostname?: string;
headers?: Record<string, string>;
raw?: boolean;
hostname?: string;
headers?: Record<string, string>;
};
export async function apiRequest<T extends z.ZodType>(
endpoint: string,
schema: T,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
body?: unknown,
options?: APIRequestOptions
endpoint: string,
schema: T,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
body?: unknown,
options?: APIRequestOptions
): Promise<z.infer<T>> {
function setReloadFlag() {
if (method !== 'GET') {
reload.auditLog = true;
}
}
function setReloadFlag() {
if (method !== 'GET') {
reload.auditLog = true;
}
}
try {
const config = {
method,
url: endpoint,
headers: {
...(options?.headers || {}),
...(options?.hostname ? { 'X-Current-Hostname': options.hostname } : {})
},
...(body ? { data: body } : {})
};
try {
const config = {
method,
url: endpoint,
headers: {
...(options?.headers || {}),
...(options?.hostname ? { 'X-Current-Hostname': options.hostname } : {})
},
...(body ? { data: body } : {})
};
const response = await api.request({ ...config, validateStatus: () => true });
const apiResponse = APIResponseSchema.safeParse(response.data);
const response = await api.request({ ...config, validateStatus: () => true });
const apiResponse = APIResponseSchema.safeParse(response.data);
if (apiResponse.data) {
if (apiResponse.data.status && apiResponse.data.status === 'error') {
if (apiResponse.data.error && apiResponse.data.error === 'invalid_cluster_token') {
storage.clusterToken = '';
return apiRequest(endpoint, schema, method, body, options);
}
}
}
if (apiResponse.data) {
if (apiResponse.data.status && apiResponse.data.status === 'error') {
if (apiResponse.data.error && apiResponse.data.error === 'invalid_cluster_token') {
storage.clusterToken = '';
return apiRequest(endpoint, schema, method, body, options);
}
}
}
/* Couldn't parse response data into APIResponse so we'll just return the data? */
if (!apiResponse.success) {
setReloadFlag();
if (apiResponse.data) {
return getDefaultValue(schema, { status: 'error' });
}
/* Couldn't parse response data into APIResponse so we'll just return the data? */
if (!apiResponse.success) {
setReloadFlag();
if (apiResponse.data) {
return getDefaultValue(schema, { status: 'error' });
}
return null as z.infer<T>;
}
return null as z.infer<T>;
}
/* Caller asked for a raw response */
if (options?.raw) {
setReloadFlag();
return apiResponse.data as z.infer<T>;
}
/* Caller asked for a raw response */
if (options?.raw) {
setReloadFlag();
return apiResponse.data as z.infer<T>;
}
if (apiResponse.data.data) {
const parsedResult = schema.safeParse(apiResponse.data.data);
if (parsedResult.success) {
setReloadFlag();
return parsedResult.data;
} else {
adze.withEmoji.warn('Zod Validation Error', parsedResult.error);
setReloadFlag();
return getDefaultValue(schema, apiResponse.data);
}
}
if (apiResponse.data.data) {
const parsedResult = schema.safeParse(apiResponse.data.data);
if (parsedResult.success) {
setReloadFlag();
return parsedResult.data;
} else {
console.warn('Zod Validation Error', parsedResult.error);
setReloadFlag();
return getDefaultValue(schema, apiResponse.data);
}
}
setReloadFlag();
return getDefaultValue(schema, apiResponse.data);
} catch (error) {
setReloadFlag();
adze.withEmoji.error('API Request Error', error);
return getDefaultValue(schema, { status: 'error' });
}
setReloadFlag();
return getDefaultValue(schema, apiResponse.data);
} catch (error) {
setReloadFlag();
console.error('API Request Error', error);
return getDefaultValue(schema, { status: 'error' });
}
}
function getDefaultValue<T extends z.ZodType>(schema: T, response: APIResponse): z.infer<T> {
if (schema instanceof z.ZodArray) {
return [] as z.infer<T>;
}
if (schema instanceof z.ZodArray) {
return [] as z.infer<T>;
}
if (schema instanceof z.ZodObject) {
return response as z.infer<T>;
}
if (schema instanceof z.ZodObject) {
return response as z.infer<T>;
}
return undefined as z.infer<T>;
return undefined as z.infer<T>;
}
export async function cachedFetch<T>(
key: string,
fetchFunction: () => Promise<T>,
duration: number,
onlyCache?: boolean
key: string,
fetchFunction: () => Promise<T>,
duration: number,
onlyCache?: boolean
): Promise<T> {
const now = Date.now();
const entry = await kvStorage.getItem<T>(key);
const now = Date.now();
const entry = await kvStorage.getItem<T>(key);
if (entry && entry.data !== null) {
const isFresh = now - entry.timestamp < duration;
const data = entry.data;
if (entry && entry.data !== null) {
const isFresh = now - entry.timestamp < duration;
const data = entry.data;
const looksLikeError =
typeof data === 'object' &&
data !== null &&
'status' in data &&
(data as any).status === 'error';
const looksLikeError =
typeof data === 'object' &&
data !== null &&
'status' in data &&
(data as any).status === 'error';
if (isFresh && !looksLikeError) {
return data;
}
}
if (isFresh && !looksLikeError) {
return data;
}
}
if (onlyCache) {
return null as T;
}
if (onlyCache) {
return null as T;
}
const data = await fetchFunction();
const data = await fetchFunction();
if (
!data ||
typeof data !== 'object' ||
!('status' in data) ||
(data as any).status !== 'error'
) {
await kvStorage.setItem(key, data);
}
if (
!data ||
typeof data !== 'object' ||
!('status' in data) ||
(data as any).status !== 'error'
) {
await kvStorage.setItem(key, data);
}
return data;
return data;
}
export async function getCache<T>(key: string): Promise<T | null> {
try {
const entry = await kvStorage.getItem<T>(key);
return entry?.data ?? null;
} catch (error) {
console.error(`Failed to read cached data for key "${key}"`, error);
return null;
}
try {
const entry = await kvStorage.getItem<T>(key);
return entry?.data ?? null;
} catch (error) {
console.error(`Failed to read cached data for key "${key}"`, error);
return null;
}
}
export async function updateCache<T>(key: string, obj: T): Promise<void> {
try {
await kvStorage.setItem(key, obj);
} catch (error) {
console.error(`Failed to update cached data for key "${key}"`, error);
}
try {
await kvStorage.setItem(key, obj);
} catch (error) {
console.error(`Failed to update cached data for key "${key}"`, error);
}
}
export function isAPIResponse(obj: any): obj is APIResponse {
return (
obj &&
typeof obj.status === 'string' &&
(typeof obj.message === 'string' || typeof obj.error === 'string')
);
return (
obj &&
typeof obj.status === 'string' &&
(typeof obj.message === 'string' || typeof obj.error === 'string')
);
}
export function handleAPIError(result: APIResponse): void {
adze.withEmoji.error('API Error', result);
console.error('API Error', result);
}
+3 -3
View File
@@ -1,3 +1,3 @@
export const sourceLocale = 'en';
export const otherLocales = ['mal', 'hi', 'zh-CN'];
export const locales = ['en', 'mal', 'hi', 'zh-CN'];
export const sourceLocale = 'en'
export const otherLocales = ['mal','hi','zh-CN']
export const locales = ['en','mal','hi','zh-CN']
+24 -20
View File
@@ -3753,10 +3753,8 @@ msgstr "AShift"
msgid "Select ASHIFT"
msgstr "Select ASHIFT"
#. placeholder {0}: data.ctId
#: src/routes/[node]/jail/[node]/console/+page.svelte
msgid "Jail console connected for jail {0}"
msgstr "Jail console connected for jail {0}"
#~ msgid "Jail console connected for jail {0}"
#~ msgstr "Jail console connected for jail {0}"
#: src/routes/[node]/jail/[node]/console/+page.svelte
msgid "The Jail is currently powered off.<0/> Start the Jail to access its console."
@@ -7309,10 +7307,8 @@ msgstr "<0/> TPM"
msgid "No Password"
msgstr "No Password"
#. placeholder {0}: data.rid
#: src/routes/[node]/vm/[node]/console/+page.svelte
msgid "Serial console connected for VM {0}"
msgstr "Serial console connected for VM {0}"
#~ msgid "Serial console connected for VM {0}"
#~ msgstr "Serial console connected for VM {0}"
#~ msgid "Shell"
#~ msgstr "Shell"
@@ -7347,9 +7343,8 @@ msgstr "Host Console Settings"
#~ msgid "Console Settings"
#~ msgstr "Console Settings"
#: src/routes/[node]/terminal/+page.svelte
msgid "Host console connected"
msgstr "Host console connected"
#~ msgid "Host console connected"
#~ msgstr "Host console connected"
#: src/routes/[node]/terminal/+page.svelte
msgid "The host console has been disconnected.<0/> Click the \"Reconnect\" button to start a new session."
@@ -7479,15 +7474,14 @@ msgstr "Failed to modify QEMU Guest Agent setting"
msgid "Modified QEMU Guest Agent setting"
msgstr "Modified QEMU Guest Agent setting"
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid ""
"Enable this option to provide a QEMU Guest Agent channel via a virtio-console device.\n"
"This improves guest integration for features like shutdown, status, and filesystem operations,\n"
"when the guest agent is installed inside the VM."
msgstr ""
"Enable this option to provide a QEMU Guest Agent channel via a virtio-console device.\n"
"This improves guest integration for features like shutdown, status, and filesystem operations,\n"
"when the guest agent is installed inside the VM."
#~ msgid ""
#~ "Enable this option to provide a QEMU Guest Agent channel via a virtio-console device.\n"
#~ "This improves guest integration for features like shutdown, status, and filesystem operations,\n"
#~ "when the guest agent is installed inside the VM."
#~ msgstr ""
#~ "Enable this option to provide a QEMU Guest Agent channel via a virtio-console device.\n"
#~ "This improves guest integration for features like shutdown, status, and filesystem operations,\n"
#~ "when the guest agent is installed inside the VM."
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid "Enable QEMU Guest Agent"
@@ -9687,3 +9681,13 @@ msgstr "Triggering"
#~ msgid "Trigger Failover 1"
#~ msgstr "Trigger Failover 1"
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid ""
"Enable this option to provide a QEMU Guest Agent channel via a virtio-console device. This\n"
"improves guest integration for features like shutdown, status, and filesystem operations, when\n"
"the guest agent is installed inside the VM."
msgstr ""
"Enable this option to provide a QEMU Guest Agent channel via a virtio-console device. This\n"
"improves guest integration for features like shutdown, status, and filesystem operations, when\n"
"the guest agent is installed inside the VM."
+18 -17
View File
@@ -3733,10 +3733,8 @@ msgstr "एशिफ्ट"
msgid "Select ASHIFT"
msgstr "एशिफ्ट चुनें"
#. placeholder {0}: data.ctId
#: src/routes/[node]/jail/[node]/console/+page.svelte
msgid "Jail console connected for jail {0}"
msgstr "\"जेल कंसोल {0} के लिए कनेक्टेड है\""
#~ msgid "Jail console connected for jail {0}"
#~ msgstr "\"जेल कंसोल {0} के लिए कनेक्टेड है\""
#: src/routes/[node]/jail/[node]/console/+page.svelte
msgid "The Jail is currently powered off.<0/> Start the Jail to access its console."
@@ -7272,10 +7270,8 @@ msgstr ""
msgid "No Password"
msgstr ""
#. placeholder {0}: data.rid
#: src/routes/[node]/vm/[node]/console/+page.svelte
msgid "Serial console connected for VM {0}"
msgstr ""
#~ msgid "Serial console connected for VM {0}"
#~ msgstr ""
#~ msgid "Shell"
#~ msgstr ""
@@ -7310,9 +7306,8 @@ msgstr ""
#~ msgid "Console Settings"
#~ msgstr ""
#: src/routes/[node]/terminal/+page.svelte
msgid "Host console connected"
msgstr ""
#~ msgid "Host console connected"
#~ msgstr ""
#: src/routes/[node]/terminal/+page.svelte
msgid "The host console has been disconnected.<0/> Click the \"Reconnect\" button to start a new session."
@@ -7446,12 +7441,11 @@ msgstr ""
msgid "Modified QEMU Guest Agent setting"
msgstr ""
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid ""
"Enable this option to provide a QEMU Guest Agent channel via a virtio-console device.\n"
"This improves guest integration for features like shutdown, status, and filesystem operations,\n"
"when the guest agent is installed inside the VM."
msgstr ""
#~ msgid ""
#~ "Enable this option to provide a QEMU Guest Agent channel via a virtio-console device.\n"
#~ "This improves guest integration for features like shutdown, status, and filesystem operations,\n"
#~ "when the guest agent is installed inside the VM."
#~ msgstr ""
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid "Enable QEMU Guest Agent"
@@ -9551,3 +9545,10 @@ msgstr ""
#~ msgid "Trigger Failover 1"
#~ msgstr ""
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid ""
"Enable this option to provide a QEMU Guest Agent channel via a virtio-console device. This\n"
"improves guest integration for features like shutdown, status, and filesystem operations, when\n"
"the guest agent is installed inside the VM."
msgstr ""
+5 -5
View File
@@ -1,8 +1,8 @@
import { loadCatalog, loadIDs } from './.wuchale/js.proxy.js';
import { registerLoaders } from 'wuchale/load-utils';
import { loadCatalog, loadIDs } from './.wuchale/js.proxy.js'
import { registerLoaders } from 'wuchale/load-utils'
const key = 'js';
const key = 'js'
// two exports. can be used anywhere
export const getRuntime = registerLoaders(key, loadCatalog, loadIDs);
export const getRuntimeRx = getRuntime;
export const getRuntime = registerLoaders(key, loadCatalog, loadIDs)
export const getRuntimeRx = getRuntime
+6 -6
View File
@@ -1,9 +1,9 @@
import { loadCatalog, loadIDs } from './.wuchale/js.proxy.sync.js';
import { currentRuntime } from 'wuchale/load-utils/server';
import { loadCatalog, loadIDs } from './.wuchale/js.proxy.sync.js'
import { currentRuntime } from 'wuchale/load-utils/server'
export const key = 'js';
export { loadCatalog, loadIDs }; // for loading before runWithLocale
export const key = 'js'
export { loadCatalog, loadIDs } // for loading before runWithLocale
// two exports, same function
export const getRuntime = (/** @type {string} */ loadID) => currentRuntime(key, loadID);
export const getRuntimeRx = getRuntime;
export const getRuntime = (/** @type {string} */ loadID) => currentRuntime(key, loadID)
export const getRuntimeRx = getRuntime
+6 -6
View File
@@ -1,12 +1,12 @@
import { loadCatalog, loadIDs } from './.wuchale/main.proxy.sync.js';
import { currentRuntime } from 'wuchale/load-utils/server';
import { loadCatalog, loadIDs } from './.wuchale/main.proxy.sync.js'
import { currentRuntime } from 'wuchale/load-utils/server'
const key = 'main';
const key = 'main'
export { loadCatalog, loadIDs, key }; // for hooks.server.{js,ts}
export { loadCatalog, loadIDs, key } // for hooks.server.{js,ts}
// for non-reactive
export const getRuntime = (/** @type {string} */ loadID) => currentRuntime(key, loadID);
export const getRuntime = (/** @type {string} */ loadID) => currentRuntime(key, loadID)
// same function, only will be inside $derived when used
export const getRuntimeRx = getRuntime;
export const getRuntimeRx = getRuntime
+6 -6
View File
@@ -1,12 +1,12 @@
import { loadCatalog, loadIDs } from './.wuchale/main.proxy.js';
import { registerLoaders, defaultCollection } from 'wuchale/load-utils';
import { loadCatalog, loadIDs } from './.wuchale/main.proxy.js'
import { registerLoaders, defaultCollection } from 'wuchale/load-utils'
const key = 'main';
const key = 'main'
const runtimes = $state({});
const runtimes = $state({})
// for non-reactive
export const getRuntime = registerLoaders(key, loadCatalog, loadIDs, defaultCollection(runtimes));
export const getRuntime = registerLoaders(key, loadCatalog, loadIDs, defaultCollection(runtimes))
// same function, only will be inside $derived when used
export const getRuntimeRx = getRuntime;
export const getRuntimeRx = getRuntime
+18 -17
View File
@@ -3741,10 +3741,8 @@ msgstr "AShift"
msgid "Select ASHIFT"
msgstr "AShift തിരഞ്ഞെടുക്കുക"
#. placeholder {0}: data.ctId
#: src/routes/[node]/jail/[node]/console/+page.svelte
msgid "Jail console connected for jail {0}"
msgstr "ജയിലിനായുള്ള ജയിൽ കൺസോൾ {0} ബന്ധിപ്പിച്ചു"
#~ msgid "Jail console connected for jail {0}"
#~ msgstr "ജയിലിനായുള്ള ജയിൽ കൺസോൾ {0} ബന്ധിപ്പിച്ചു"
#: src/routes/[node]/jail/[node]/console/+page.svelte
msgid "The Jail is currently powered off.<0/> Start the Jail to access its console."
@@ -7256,10 +7254,8 @@ msgstr ""
msgid "No Password"
msgstr ""
#. placeholder {0}: data.rid
#: src/routes/[node]/vm/[node]/console/+page.svelte
msgid "Serial console connected for VM {0}"
msgstr ""
#~ msgid "Serial console connected for VM {0}"
#~ msgstr ""
#~ msgid "Shell"
#~ msgstr ""
@@ -7294,9 +7290,8 @@ msgstr ""
#~ msgid "Console Settings"
#~ msgstr ""
#: src/routes/[node]/terminal/+page.svelte
msgid "Host console connected"
msgstr ""
#~ msgid "Host console connected"
#~ msgstr ""
#: src/routes/[node]/terminal/+page.svelte
msgid "The host console has been disconnected.<0/> Click the \"Reconnect\" button to start a new session."
@@ -7430,12 +7425,11 @@ msgstr ""
msgid "Modified QEMU Guest Agent setting"
msgstr ""
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid ""
"Enable this option to provide a QEMU Guest Agent channel via a virtio-console device.\n"
"This improves guest integration for features like shutdown, status, and filesystem operations,\n"
"when the guest agent is installed inside the VM."
msgstr ""
#~ msgid ""
#~ "Enable this option to provide a QEMU Guest Agent channel via a virtio-console device.\n"
#~ "This improves guest integration for features like shutdown, status, and filesystem operations,\n"
#~ "when the guest agent is installed inside the VM."
#~ msgstr ""
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid "Enable QEMU Guest Agent"
@@ -9599,3 +9593,10 @@ msgstr ""
#~ msgid "Trigger Failover 1"
#~ msgstr ""
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid ""
"Enable this option to provide a QEMU Guest Agent channel via a virtio-console device. This\n"
"improves guest integration for features like shutdown, status, and filesystem operations, when\n"
"the guest agent is installed inside the VM."
msgstr ""
+18 -17
View File
@@ -3679,10 +3679,8 @@ msgstr "扇区大小(AShift"
msgid "Select ASHIFT"
msgstr "选择块对齐参数(ASHIFT"
#. placeholder {0}: data.ctId
#: src/routes/[node]/jail/[node]/console/+page.svelte
msgid "Jail console connected for jail {0}"
msgstr "已连接 Jail 控制台,Jail 为 {0}"
#~ msgid "Jail console connected for jail {0}"
#~ msgstr "已连接 Jail 控制台,Jail 为 {0}"
#: src/routes/[node]/jail/[node]/console/+page.svelte
msgid "The Jail is currently powered off.<0/> Start the Jail to access its console."
@@ -6426,10 +6424,8 @@ msgstr ""
msgid "No Password"
msgstr ""
#. placeholder {0}: data.rid
#: src/routes/[node]/vm/[node]/console/+page.svelte
msgid "Serial console connected for VM {0}"
msgstr ""
#~ msgid "Serial console connected for VM {0}"
#~ msgstr ""
#~ msgid "Shell"
#~ msgstr ""
@@ -6468,9 +6464,8 @@ msgstr ""
#~ msgid "Console Settings"
#~ msgstr ""
#: src/routes/[node]/terminal/+page.svelte
msgid "Host console connected"
msgstr ""
#~ msgid "Host console connected"
#~ msgstr ""
#: src/routes/[node]/terminal/+page.svelte
msgid "The host console has been disconnected.<0/> Click the \"Reconnect\" button to start a new session."
@@ -6610,12 +6605,11 @@ msgstr ""
msgid "Modified QEMU Guest Agent setting"
msgstr ""
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid ""
"Enable this option to provide a QEMU Guest Agent channel via a virtio-console device.\n"
"This improves guest integration for features like shutdown, status, and filesystem operations,\n"
"when the guest agent is installed inside the VM."
msgstr ""
#~ msgid ""
#~ "Enable this option to provide a QEMU Guest Agent channel via a virtio-console device.\n"
#~ "This improves guest integration for features like shutdown, status, and filesystem operations,\n"
#~ "when the guest agent is installed inside the VM."
#~ msgstr ""
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid "Enable QEMU Guest Agent"
@@ -8795,3 +8789,10 @@ msgstr ""
#~ msgid "Trigger Failover 1"
#~ msgstr ""
#: src/lib/components/custom/VM/Options/QemuGuestAgent.svelte
msgid ""
"Enable this option to provide a QEMU Guest Agent channel via a virtio-console device. This\n"
"improves guest integration for features like shutdown, status, and filesystem operations, when\n"
"the guest agent is installed inside the VM."
msgstr ""
@@ -5,7 +5,6 @@
import type { Jail, JailState } from '$lib/types/jail/jail';
import { updateCache } from '$lib/utils/http';
import { sha256, toHex } from '$lib/utils/string';
import adze from 'adze';
import { resource, useResizeObserver, PersistedState, useDebounce } from 'runed';
import { onMount } from 'svelte';
import { init as initGhostty, Terminal as GhosttyTerminal } from 'ghostty-web';
@@ -233,7 +232,7 @@
if (destroyed || activeConnectionToken !== connectionToken || terminal !== activeTerminal)
return;
adze.info(`Jail console connected for jail ${data.ctId}`);
console.log(`Jail console connected for jail ${data.ctId}`);
if (lastWidth && lastHeight) {
resizeTerminal(lastWidth, lastHeight);
} else if (terminalContainer) {
+1 -2
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import { storage } from '$lib';
import { sha256, toHex } from '$lib/utils/string';
import adze from 'adze';
import { useResizeObserver, PersistedState, useDebounce } from 'runed';
import { onMount, tick } from 'svelte';
import { init as initGhostty, Terminal as GhosttyTerminal } from 'ghostty-web';
@@ -134,7 +133,7 @@
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
adze.info(`Host console connected`);
console.log(`Host console connected`);
if (lastWidth && lastHeight) resizeTerminal(lastWidth, lastHeight);
};
@@ -5,7 +5,7 @@
import type { VM, VMDomain } from '$lib/types/vm/vm';
import { toHex } from '$lib/utils/string';
import { init as initGhostty, Terminal as GhosttyTerminal } from 'ghostty-web';
import { onDestroy, onMount, tick } from 'svelte';
import { onMount, tick } from 'svelte';
import { getVmById, getVMDomain } from '$lib/api/vm/vm';
import { updateCache } from '$lib/utils/http';
import {
@@ -17,7 +17,6 @@
useResizeObserver
} from 'runed';
import { mode } from 'mode-watcher';
import adze from 'adze';
import { fade } from 'svelte/transition';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import CustomValueInput from '$lib/components/ui/custom-input/value.svelte';
@@ -291,7 +290,7 @@
ws.onopen = async () => {
connected = true;
adze.info(`Serial console connected for VM ${data.rid}`);
console.log(`Serial console connected for VM ${data.rid}`);
if (lastWidth && lastHeight) {
resizeTerminal(lastWidth, lastHeight);
} else if (terminalContainer) {
@@ -223,12 +223,11 @@
</div>
</div>
</Card.Content>
<Card.Footer></Card.Footer>
</Card.Root>
</div>
<div class="px-4">
<Card.Root class="gap-2">
<Card.Root class="gap-2 pb-0">
<Card.Header>
<Card.Title>
<div class="flex items-center gap-2">
@@ -274,7 +273,6 @@
{/if}
</div>
</Card.Content>
<Card.Footer></Card.Footer>
</Card.Root>
</div>
@@ -320,7 +318,6 @@
</Table.Body>
</Table.Root>
</Card.Content>
<Card.Footer></Card.Footer>
</Card.Root>
</div>
{/if}