mirror of
https://github.com/wireadmin/wireadmin
synced 2025-03-09 13:20:39 +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">
|
||||
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>
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import '$lib/assets/fontawesome/index.css';
|
||||
import { Toaster } from 'svelte-french-toast';
|
||||
</script>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
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">
|
||||
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'}>
|
||||
|
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