mirror of
https://github.com/wireadmin/wireadmin
synced 2025-06-26 18:28:06 +00:00
adds a section for showing the state of bg services
This commit is contained in:
parent
a2f644b163
commit
2629204d76
@ -1,14 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Logo from '$lib/assets/logo.png';
|
|
||||||
import { buttonVariants } from '$lib/components/ui/button';
|
|
||||||
|
|
||||||
export let showLogout: boolean = false;
|
export let showLogout: boolean = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class={'w-full py-3 px-2'}>
|
<header class={'w-full py-3 px-2'}>
|
||||||
<nav class={'w-full flex items-center justify-between'}>
|
<nav class={'w-full flex items-center justify-between'}>
|
||||||
<div class={'flex items-center gap-x-2 text-3xl font-medium'}>
|
<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>
|
<h1 class="max-sm:text-lg">WireAdmin</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import '$lib/assets/fontawesome/index.css';
|
|
||||||
import { Toaster } from 'svelte-french-toast';
|
import { Toaster } from 'svelte-french-toast';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -4,9 +4,10 @@
|
|||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import CreateServerDialog from './CreateServerDialog.svelte';
|
import CreateServerDialog from './CreateServerDialog.svelte';
|
||||||
import Server from './Server.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 fetchAction from '$lib/fetch-action';
|
||||||
import { Empty } from '$lib/components/empty';
|
import { Empty } from '$lib/components/empty';
|
||||||
|
import Service from './Service.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
@ -49,15 +50,33 @@
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Servers</CardTitle>
|
<CardTitle>Servers</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
{#each data.servers as server}
|
{#each data.servers as server}
|
||||||
<Server
|
<Server
|
||||||
{server}
|
{server}
|
||||||
on:rename={({ detail }) => {
|
on:rename={({ detail }) => handleRename(server.id.toString(), detail)}
|
||||||
handleRename(server.id.toString(), detail);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
</CardContent>
|
||||||
{/if}
|
{/if}
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
</BasePage>
|
</BasePage>
|
||||||
|
@ -14,18 +14,21 @@
|
|||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
</script>
|
</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="w-full md:w-2/3 flex items-center gap-x-2">
|
||||||
<div class="flex grow">
|
<div class="flex grow">
|
||||||
<div
|
<div
|
||||||
class={'w-12 aspect-square flex items-center justify-center mr-4 rounded-full bg-gray-200 max-md:hidden'}
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class={cn(
|
class={cn(
|
||||||
server.tor ? 'fa-solid fa-onion text-purple-700' : 'fa-solid fa-server text-gray-400',
|
'relative w-12 aspect-square',
|
||||||
'text-xl',
|
'flex items-center justify-center mr-4 ',
|
||||||
|
'bg-gray-200 max-md:hidden',
|
||||||
|
'rounded-full',
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="h-full flex flex-col justify-between col-span-4 gap-y-1.5">
|
<div class="h-full flex flex-col justify-between col-span-4 gap-y-1.5">
|
||||||
@ -41,7 +44,7 @@
|
|||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<a href={`/${server.id}`} title="Manage the Server" class={cn({ hidden: editMode })}>
|
<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}
|
{server.name}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -52,11 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={'flex col-span-4 justify-end'}>
|
<div class={'flex col-span-4 justify-end'}>
|
||||||
{#if server.status === 'up'}
|
<Badge variant={server.status === 'up' ? 'success' : 'destructive'} />
|
||||||
<Badge variant="success">Online</Badge>
|
|
||||||
{:else}
|
|
||||||
<Badge variant="destructive">Offline</Badge>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
50
web/src/routes/Service.svelte
Normal file
50
web/src/routes/Service.svelte
Normal 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>
|
9
web/src/routes/api/health/[serviceName]/+server.ts
Normal file
9
web/src/routes/api/health/[serviceName]/+server.ts
Normal 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 });
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PageFooter from '$lib/components/page/PageFooter.svelte';
|
import PageFooter from '$lib/components/page/PageFooter.svelte';
|
||||||
import Logo from '$lib/assets/logo.png';
|
import Logo from '../../../static/logo.png';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={'w-full min-h-screen flex justify-center px-2 md:px-6 py-2'}>
|
<div class={'w-full min-h-screen flex justify-center px-2 md:px-6 py-2'}>
|
||||||
|
6
web/src/routes/service/+page.server.ts
Normal file
6
web/src/routes/service/+page.server.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async () => {
|
||||||
|
throw redirect(303, '/');
|
||||||
|
};
|
78
web/src/routes/service/[serviceName]/+page.server.ts
Normal file
78
web/src/routes/service/[serviceName]/+page.server.ts
Normal 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');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
88
web/src/routes/service/[serviceName]/+page.svelte
Normal file
88
web/src/routes/service/[serviceName]/+page.svelte
Normal 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>
|
24
web/src/routes/service/[serviceName]/Layout.svelte
Normal file
24
web/src/routes/service/[serviceName]/Layout.svelte
Normal 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>
|
Loading…
Reference in New Issue
Block a user