Add frontend

This commit is contained in:
Sami Abuzakuk
2025-10-11 00:09:31 +02:00
parent aa67ffa704
commit ed33bee7ce
25 changed files with 5061 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
<script lang="ts">
import '../app.css';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { checkHealth } from '$lib/api';
let { children } = $props();
interface Notification {
id: number;
type: 'success' | 'error';
message: string;
}
let notifications: Notification[] = $state([]);
let notificationId = 0;
let healthStatus = writable<'healthy' | 'unhealthy'>('unhealthy');
async function updateHealthStatus() {
const status = await checkHealth();
healthStatus.set(status);
}
function showNotification(type: 'success' | 'error', message: string): void {
const id = notificationId++;
notifications = [...notifications, { id, type, message }];
setTimeout(() => {
notifications = notifications.filter((n) => n.id !== id);
}, 4000);
}
onMount(() => {
window.showNotification = showNotification;
updateHealthStatus();
setInterval(updateHealthStatus, 10000); // Check health every 10 seconds
});
</script>
<nav class="bg-gray-800 text-white shadow-md">
<div class="container mx-auto flex justify-between items-center p-4">
<a href="/" class="text-2xl font-bold hover:text-gray-400">Project Monitor</a>
<div class="flex space-x-6">
<a href="/" class="text-lg hover:text-gray-400">Home</a>
<a href="/scripts" class="text-lg hover:text-gray-400">Scripts</a>
</div>
</div>
</nav>
<div class="relative">
{@render children()}
<div class="fixed bottom-4 right-4 space-y-2">
{#each notifications as notification (notification.id)}
<div
class="p-4 rounded shadow-lg text-white"
class:bg-green-500={notification.type === 'success'}
class:bg-red-500={notification.type === 'error'}
>
{notification.message}
</div>
{/each}
</div>
</div>
<div class="fixed bottom-4 left-4 group">
{#if $healthStatus === 'healthy'}
<span class="text-green-500"></span>
<div
class="absolute bottom-full left-1/2 hidden group-hover:flex group-hover:w-max bg-green-500 text-white px-2 py-1 rounded shadow-lg"
>
Connected to backend
</div>
{:else}
<span class="text-red-500"></span>
<div
class="absolute bottom-full left-1/2 hidden group-hover:flex group-hover:w-max bg-red-500 text-white px-2 py-1 rounded shadow-lg"
>
Not connected
</div>
{/if}
</div>

View File

@@ -0,0 +1,12 @@
<h1 class="text-4xl font-bold text-center mt-10 text-gray-800">Welcome to Project Monitor</h1>
<p class="text-center mt-4 text-lg text-gray-600">
Manage your scripts efficiently with our simple and intuitive interface.
</p>
<div class="flex justify-center mt-8">
<a
href="/scripts"
class="px-6 py-3 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600"
>
Go to Scripts
</a>
</div>

View File

@@ -0,0 +1,12 @@
import { error } from '@sveltejs/kit';
import { fetchScripts } from '$lib/api';
/** @type {import('./$types').PageServerLoad} */
export async function load() {
try {
const scripts = await fetchScripts();
return { scripts };
} catch (err) {
throw error(500, 'Failed to fetch scripts - ' + err);
}
}

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import { addScript } from '$lib/api';
import type { Script } from '$lib/api';
import CodeMirror from 'svelte-codemirror-editor';
import { python } from '@codemirror/lang-python';
export let data: { scripts: Script[] };
let scripts: Script[] = data.scripts;
let newScript: Omit<Script, 'id' | 'created_at'> = { name: '', script_content: '' };
// Add a new script
async function handleAddScript() {
try {
const addedScript = await addScript(newScript);
scripts = [...scripts, addedScript];
newScript = { name: '', script_content: '' };
} catch (err) {
window.showNotification('Failed to add script. ' + err);
}
}
</script>
<main class="p-4">
<h1 class="text-2xl font-bold mb-4">Scripts</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{#each scripts as script (script.id)}
<a
href={`/scripts/${script.id}`}
class="block p-4 border rounded shadow bg-white hover:bg-gray-100"
>
<h2 class="text-lg font-semibold text-gray-800">{script.name}</h2>
</a>
{/each}
</div>
<div class="mt-8">
<h2 class="text-xl font-semibold mb-2">Add New Script</h2>
<form
on:submit|preventDefault={handleAddScript}
class="space-y-4 p-4 border rounded shadow"
on:submit={() => (newScript.script_content = editor.getValue())}
>
<div>
<label for="name" class="block text-sm font-medium">Name</label>
<input
id="name"
type="text"
bind:value={newScript.name}
required
class="mt-1 block w-full p-2 border rounded"
/>
</div>
<div>
<label for="script_content" class="block text-sm font-medium">Content</label>
<CodeMirror bind:value={newScript.script_content} lang={python()} />
</div>
<button type="submit" class="px-4 py-2 bg-green-500 text-white rounded"> Add Script </button>
</form>
</div>
</main>
<style>
main {
max-width: 800px;
margin: 0 auto;
}
</style>

View File

@@ -0,0 +1,22 @@
import { error } from '@sveltejs/kit';
import { fetchScriptById, fetchLogs } from '$lib/api';
import type { Log } from '$lib/api';
export async function load({ params }) {
const { id } = params;
try {
const script = await fetchScriptById(parseInt(id));
const logs: Log[] = (await fetchLogs(script.id)).sort(
(a, b) => new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
);
if (!script) {
throw error(404, 'Script not found');
}
return { script, logs };
} catch (err) {
throw error(500, 'Failed to fetch script data - ' + err);
}
}

View File

@@ -0,0 +1,154 @@
<script lang="ts">
import { updateScript, deleteScript, addLog, deleteLog } from '$lib/api';
import type { Script, Log } from '$lib/api';
import CodeMirror from 'svelte-codemirror-editor';
import { python } from '@codemirror/lang-python';
export let data: { script: Script; logs: Log[] };
let script: Script = data.script;
let logs: Log[] = data.logs;
let updatedTitle: string = script.name || '';
let updatedContent: string = script.script_content || '';
let isEditMode: boolean = false;
let newLogMessage: string = '';
// Notifications are now handled globally via the layout
async function handleUpdateScript() {
if (script) {
try {
const updatedScript = await updateScript(script.id, {
name: updatedTitle,
script_content: updatedContent
});
script = updatedScript;
window.showNotification('success', 'Script updated successfully!');
isEditMode = false;
} catch (err) {
window.showNotification('error', 'Failed to update script. ' + err);
}
}
}
async function handleAddLog() {
if (newLogMessage.trim()) {
try {
const newLog = await addLog(script.id, newLogMessage);
logs = [newLog, ...logs];
newLogMessage = '';
window.showNotification('success', 'Log added successfully!');
} catch (err) {
window.showNotification('error', 'Failed to add log. ' + err);
}
}
}
async function handleDeleteLog(logId: number) {
try {
await deleteLog(script.id, logId);
logs = logs.filter((log) => log.id !== logId);
window.showNotification('success', 'Log deleted successfully!');
} catch (err) {
window.showNotification('error', 'Failed to delete log. ' + err);
}
}
// Delete the script
async function handleDeleteScript() {
if (script) {
try {
await deleteScript(script.id);
window.location.href = '/scripts'; // Redirect to scripts list after deletion
} catch (err) {
window.showNotification('error', 'Failed to delete script. ' + err);
}
}
}
</script>
<main class="p-4">
<!-- Removed local notification container as notifications are now global -->
{#if script}
{#if isEditMode}
<input
type="text"
bind:value={updatedTitle}
required
class="text-2xl font-bold mb-4 w-full p-2 border rounded"
/>
<CodeMirror bind:value={updatedContent} lang={python()} />
<button
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
on:click={handleUpdateScript}
aria-label="Update the script title and content"
>
Update Script
</button>
<button
class="mt-4 px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
on:click={() => (isEditMode = false)}
>
Cancel
</button>
{:else}
<a href="/scripts" class="text-blue-500 hover:underline mb-4 inline-block"
>← Return to Scripts</a
>
<h1 class="text-2xl font-bold mb-4">{script.name}</h1>
<pre
class="w-full p-2 border rounded font-mono text-sm bg-gray-100">{script.script_content}</pre>
<button
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
on:click={() => (isEditMode = true)}
>
Edit Script
</button>
<button
class="mt-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
on:click={handleDeleteScript}
>
Delete Script
</button>
{/if}
{/if}
<section class="mt-8">
<h2 class="text-xl font-bold mb-4">Logs</h2>
<form on:submit|preventDefault={handleAddLog} class="mb-4">
<input
type="text"
bind:value={newLogMessage}
placeholder="Enter new log message"
class="w-full p-2 border rounded mb-2"
required
/>
<button type="submit" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">
Add Log
</button>
</form>
<ul class="space-y-4">
{#each logs as log (log.id)}
<li class="p-4 border rounded bg-gray-50 flex justify-between items-center">
<div>
<p class="text-sm text-gray-700">{log.message}</p>
<p class="text-xs text-gray-500">{log.created_at}</p>
</div>
<button
class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600"
on:click={() => handleDeleteLog(log.id)}
>
Delete
</button>
</li>
{/each}
</ul>
</section>
</main>
<style>
main {
max-width: 800px;
margin: 0 auto;
}
</style>