adds a section for showing the state of bg services

This commit is contained in:
Shahrad Elahi 2024-01-08 16:23:08 +03:30
parent a2f644b163
commit 2629204d76
11 changed files with 298 additions and 29 deletions

View File

@ -1,14 +1,11 @@
<script lang="ts">
import Logo from '$lib/assets/logo.png';
import { buttonVariants } from '$lib/components/ui/button';
export let showLogout: boolean = false;
</script>
<header class={'w-full py-3 px-2'}>
<nav class={'w-full flex items-center justify-between'}>
<div class={'flex items-center gap-x-2 text-3xl font-medium'}>
<img src={Logo} alt="WireAdmin" width="40" height="40" class="max-sm:w-8" />
<img src={'/logo.png'} alt="WireAdmin" width="40" height="40" class="max-sm:w-8" />
<h1 class="max-sm:text-lg">WireAdmin</h1>
</div>

View File

@ -1,6 +1,5 @@
<script lang="ts">
import '../app.css';
import '$lib/assets/fontawesome/index.css';
import { Toaster } from 'svelte-french-toast';
</script>

View File

@ -4,9 +4,10 @@
import type { PageData } from './$types';
import CreateServerDialog from './CreateServerDialog.svelte';
import Server from './Server.svelte';
import { Card, CardHeader, CardTitle } from '$lib/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import fetchAction from '$lib/fetch-action';
import { Empty } from '$lib/components/empty';
import Service from './Service.svelte';
export let data: PageData;
@ -49,15 +50,33 @@
<CardHeader>
<CardTitle>Servers</CardTitle>
</CardHeader>
{#each data.servers as server}
<Server
{server}
on:rename={({ detail }) => {
handleRename(server.id.toString(), detail);
}}
/>
{/each}
<CardContent>
{#each data.servers as server}
<Server
{server}
on:rename={({ detail }) => handleRename(server.id.toString(), detail)}
/>
{/each}
</CardContent>
{/if}
</Card>
<Card>
<CardHeader>
<CardTitle>Services</CardTitle>
</CardHeader>
<CardContent>
<Service name="Tor" slug="tor">
<svelte:fragment slot="icon">
<i class={'fa-solid fa-onion text-purple-700 text-xl'} />
</svelte:fragment>
</Service>
<Service name="Redis" slug="redis">
<svelte:fragment slot="icon">
<i class={'fa-solid fa-database text-red-700 text-xl'} />
</svelte:fragment>
</Service>
</CardContent>
</Card>
</div>
</BasePage>

View File

@ -14,18 +14,21 @@
const dispatch = createEventDispatcher();
</script>
<div class="flex items-center justify-between p-4 gap-x-4">
<div class="flex items-center justify-between py-4 gap-x-4">
<div class="w-full md:w-2/3 flex items-center gap-x-2">
<div class="flex grow">
<div
class={'w-12 aspect-square flex items-center justify-center mr-4 rounded-full bg-gray-200 max-md:hidden'}
class={cn(
'relative w-12 aspect-square',
'flex items-center justify-center mr-4 ',
'bg-gray-200 max-md:hidden',
'rounded-full',
)}
>
<i
class={cn(
server.tor ? 'fa-solid fa-onion text-purple-700' : 'fa-solid fa-server text-gray-400',
'text-xl',
)}
/>
<i class={'fa-solid fa-server text-gray-400 text-xl'} />
{#if server.tor}
<i class="absolute bottom-3.5 right-2 w-3 h-3 fa-solid fa-onion text-purple-700" />
{/if}
</div>
<div class="h-full flex flex-col justify-between col-span-4 gap-y-1.5">
@ -41,7 +44,7 @@
asChild
>
<a href={`/${server.id}`} title="Manage the Server" class={cn({ hidden: editMode })}>
<span class="text-lg md:text-base hover:text-primary hover:font-medium">
<span class="text-lg font-medium md:text-base hover:text-primary hover:font-medium">
{server.name}
</span>
</a>
@ -52,11 +55,7 @@
</div>
</div>
<div class={'flex col-span-4 justify-end'}>
{#if server.status === 'up'}
<Badge variant="success">Online</Badge>
{:else}
<Badge variant="destructive">Offline</Badge>
{/if}
<Badge variant={server.status === 'up' ? 'success' : 'destructive'} />
</div>
</div>

View File

@ -0,0 +1,50 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
import { onMount } from 'svelte';
export let name: string;
export let slug: string;
let healthy: boolean;
const getHealth = async (slug: string): Promise<void> => {
const res = await fetch(`/api/health/${slug}`);
if (res.ok) {
const data = (await res.json()) as { healthy: boolean };
healthy = data.healthy;
}
};
onMount(() => {
getHealth(slug);
});
</script>
<div class="flex items-center justify-between py-4 gap-x-4">
<div class="w-full md:w-2/3 flex items-center gap-x-2">
<div class="flex grow">
<div
class={'w-12 aspect-square flex items-center justify-center mr-4 rounded-full bg-gray-200 max-md:hidden'}
>
<slot name="icon" />
</div>
<a href={`/service/${slug}`} title="Manage" class="my-auto">
<span class="text-lg font-medium md:text-base hover:text-primary hover:font-medium">
{name}
</span>
</a>
</div>
<div class={'flex col-span-4 items-center justify-end'}>
{#if healthy === undefined}
<i class="fas fa-spinner animate-spin" />
{:else}
<Badge variant={healthy ? 'success' : 'destructive'} />
{/if}
</div>
</div>
<a href={`/service/${slug}`} title="Manage the Server" class="hidden md:block">
<Button variant="ghost" size="sm">Manage</Button>
</a>
</div>

View File

@ -0,0 +1,9 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { execa } from 'execa';
export const GET: RequestHandler = async ({ params }) => {
const { stdout } = await execa('screen', ['-ls'], { shell: true });
const isRunning = stdout.includes(params.serviceName!);
return json({ healthy: isRunning });
};

View File

@ -1,6 +1,6 @@
<script lang="ts">
import PageFooter from '$lib/components/page/PageFooter.svelte';
import Logo from '$lib/assets/logo.png';
import Logo from '../../../static/logo.png';
</script>
<div class={'w-full min-h-screen flex justify-center px-2 md:px-6 py-2'}>

View File

@ -0,0 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
throw redirect(303, '/');
};

View File

@ -0,0 +1,78 @@
import { type Actions, error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import logger from '$lib/logger';
import { execa } from 'execa';
import { promises } from 'node:fs';
const services: Record<string, Service> = {
tor: {
name: 'Tor',
command: {
restart: 'screen -L -Logfile /var/vlogs/tor -dmS "tor" tor -f /etc/tor/torrc',
logs: 'logs tor',
},
},
redis: {
name: 'Redis',
command: {
restart:
'screen -L -Logfile /var/vlogs/redis -dmS "redis" bash -c "redis-server --port 6479 --daemonize no --dir /data --appendonly yes"',
logs: 'logs redis',
},
},
};
interface Service {
name: string;
command: {
restart: string;
logs: string;
};
}
export const load: PageServerLoad = async ({ params }) => {
if (!params.serviceName || !services[params.serviceName]) {
throw error(404, 'Not Found');
}
return {
slug: params.serviceName,
title: services[params.serviceName].name,
};
};
export const actions: Actions = {
clearLogs: async ({ request, params }) => {
const { serviceName } = params;
try {
await promises.writeFile(`/var/vlogs/${serviceName}`, '');
return {};
} catch (e) {
logger.error(e);
throw error(500, 'Unhandled Exception');
}
},
logs: async ({ request, params }) => {
const { serviceName } = params;
try {
const { stdout } = await execa(services[serviceName!].command.logs, { shell: true });
return { logs: stdout };
} catch (e) {
logger.error(e);
throw error(500, 'Unhandled Exception');
}
},
restart: async ({ request, params }) => {
const { serviceName } = params;
try {
await execa(services[serviceName!].command.restart, { shell: true });
return {};
} catch (e) {
logger.error(e);
throw error(500, 'Unhandled Exception');
}
},
};

View File

@ -0,0 +1,88 @@
<script lang="ts">
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import fetchAction from '$lib/fetch-action';
import { Button } from '$lib/components/ui/button';
import { CardFooter } from '$lib/components/ui/card/index.js';
import toast from 'svelte-french-toast';
import Layout from './Layout.svelte';
export let data: PageData;
let logs: string | undefined;
const getServiceLogs = async (): Promise<void> => {
const resp = await fetchAction({
action: '?/logs',
method: 'POST',
form: {},
});
if (resp.ok) {
const { data: jsonStr } = await resp.json();
const data = JSON.parse(jsonStr);
logs = data[1].toString().trim();
} else {
console.error('failed to get logs');
logs = undefined;
}
};
const clearLogs = async (): Promise<void> => {
const resp = await fetchAction({
action: '?/clearLogs',
method: 'POST',
form: {},
});
if (resp.ok) {
toast.success('Logs cleared!');
logs = '';
} else {
toast.error('Failed to clear logs');
}
};
const restart = async (): Promise<void> => {
const resp = await fetchAction({
action: '?/restart',
method: 'POST',
form: {},
});
if (resp.ok) {
toast.success('Restarting...');
} else {
toast.error('Failed to restart');
}
};
onMount(() => {
const interval = setInterval(() => {
getServiceLogs();
}, 1000);
return () => {
console.log('clearing interval');
clearInterval(interval);
};
});
</script>
<Layout title={data.title}>
<Card>
<CardHeader>
<CardTitle>Logs</CardTitle>
</CardHeader>
<CardContent class="relative">
<textarea class="w-full h-64 p-2 bg-gray-100" readonly bind:value={logs} />
{#if !logs}
<div class="absolute inset-0 flex items-center justify-center">
<i class="text-4xl animate-spin fas fa-circle-notch"></i>
</div>
{/if}
</CardContent>
<CardFooter class="flex justify-end gap-2">
<Button on:click={restart}>Restart</Button>
<Button variant="destructive" on:click={clearLogs} disabled={!logs}>Clear</Button>
</CardFooter>
</Card>
</Layout>

View File

@ -0,0 +1,24 @@
<script lang="ts">
import BasePage from '$lib/components/page/BasePage.svelte';
export let title: string;
</script>
<BasePage showLogout={true}>
<div class="flex items-center gap-2 py-4 px-2 leading-none">
<a
href="/"
title="Home"
class="space-x-1 font-bold text-sm text-primary hover:text-primary/80 tracking-tight"
>
<i class="fa-solid fa-chevron-left"></i>
<span> Back to Home </span>
</a>
<i class="fa-regular fa-slash-forward"></i>
<span class="mb-0.5"> {title} </span>
</div>
<div class="space-y-3.5">
<slot />
</div>
</BasePage>