Add frontend support for user

This commit is contained in:
Sami Abuzakuk
2025-11-01 16:05:52 +01:00
parent 374558d30f
commit e9d94f706c
14 changed files with 521 additions and 285 deletions

View File

@@ -2,6 +2,56 @@ import { env } from '$env/dynamic/public';
export const API_URL = env.PUBLIC_API_URL || 'http://localhost:8000';
// Helper to get token from localStorage
export function getToken(): string | null {
return localStorage.getItem('token');
}
// Helper to add Authorization header if token exists
export function authHeaders(headers: Record<string, string> = {}): Record<string, string> {
const token = getToken();
return token ? { ...headers, Authorization: `Bearer ${token}` } : headers;
}
/**
* Login and Register API
*/
export interface AuthResponse {
access_token: string;
token_type: string;
}
export async function login(username: string, password: string): Promise<AuthResponse> {
const form = new FormData();
form.append('username', username);
form.append('password', password);
const response = await fetch(`${API_URL}/login`, {
method: 'POST',
body: form
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.detail || 'Login failed');
}
return response.json();
}
export async function register(username: string, password: string): Promise<AuthResponse> {
const response = await fetch(`${API_URL}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.detail || 'Registration failed');
}
return response.json();
}
/**
* Type definitions for Subscriptions and Notifications
*/
@@ -67,7 +117,9 @@ export interface Script {
// Fetch all scripts
export async function fetchScripts(): Promise<Script[]> {
const response = await fetch(`${API_URL}/script`);
const response = await fetch(`${API_URL}/script`, {
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch scripts ' + response.statusText);
}
@@ -80,9 +132,7 @@ export async function addScript(
): Promise<Script> {
const response = await fetch(`${API_URL}/script`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(script)
});
if (!response.ok) {
@@ -92,8 +142,10 @@ export async function addScript(
}
// Fetch all settings
export async function fetchSettings(): Promise<Settings[]> {
const response = await fetch(`${API_URL}/settings`);
export async function fetchUserSettings(): Promise<Settings> {
const response = await fetch(`${API_URL}/settings`, {
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch settings ' + response.statusText);
}
@@ -102,7 +154,9 @@ export async function fetchSettings(): Promise<Settings[]> {
// Fetch a single setting by ID
export async function fetchSettingById(id: number): Promise<Settings> {
const response = await fetch(`${API_URL}/settings/${id}`);
const response = await fetch(`${API_URL}/settings/${id}`, {
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch setting');
}
@@ -116,9 +170,7 @@ export async function updateSetting(
): Promise<Settings> {
const response = await fetch(`${API_URL}/settings/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ ...updatedSetting, ntfy_url: updatedSetting.ntfy_url })
});
if (!response.ok) {
@@ -129,7 +181,9 @@ export async function updateSetting(
// Fetch all subscriptions
export async function fetchSubscriptions(): Promise<Subscription[]> {
const response = await fetch(`${API_URL}/subscriptions`);
const response = await fetch(`${API_URL}/subscriptions`, {
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch subscriptions');
}
@@ -138,7 +192,9 @@ export async function fetchSubscriptions(): Promise<Subscription[]> {
// Fetch subscriptions by topic
export async function getSubscription(topic_id: string): Promise<Subscription> {
const response = await fetch(`${API_URL}/subscriptions/${topic_id}`);
const response = await fetch(`${API_URL}/subscriptions/${topic_id}`, {
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch subscriptions');
}
@@ -154,9 +210,7 @@ export async function addNotification(
): Promise<Notification> {
const response = await fetch(`${API_URL}/notifications`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ subscription_id: subscriptionId, title, message, priority })
});
if (!response.ok) {
@@ -169,9 +223,7 @@ export async function addNotification(
export async function setViewed(notificationId: number): Promise<Notification> {
const response = await fetch(`${API_URL}/notifications/${notificationId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ viewed: true })
});
if (!response.ok) {
@@ -184,9 +236,7 @@ export async function setViewed(notificationId: number): Promise<Notification> {
export async function addSubscription(topic: string): Promise<Subscription> {
const response = await fetch(`${API_URL}/subscriptions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ topic })
});
if (!response.ok) {
@@ -198,7 +248,8 @@ export async function addSubscription(topic: string): Promise<Subscription> {
// Delete a subscription
export async function deleteSubscription(subscriptionId: number): Promise<void> {
const response = await fetch(`${API_URL}/subscriptions/${subscriptionId}`, {
method: 'DELETE'
method: 'DELETE',
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to delete subscription');
@@ -212,7 +263,10 @@ export async function fetchSubscriptionNotifications(
offset: number = 0
): Promise<Notification[]> {
const response = await fetch(
`${API_URL}/subscriptions/${subscriptionId}/notifications?limit=${limit}&offset=${offset}`
`${API_URL}/subscriptions/${subscriptionId}/notifications?limit=${limit}&offset=${offset}`,
{
headers: authHeaders()
}
);
if (!response.ok) {
throw new Error('Failed to fetch subscription notifications');
@@ -223,7 +277,9 @@ export async function fetchSubscriptionNotifications(
// Fetch all notifications or filter by topic
export async function fetchAllNotifications(): Promise<Notification[]> {
const url = `${API_URL}/notifications`;
const response = await fetch(url);
const response = await fetch(url, {
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch notifications');
}
@@ -233,7 +289,8 @@ export async function fetchAllNotifications(): Promise<Notification[]> {
// Delete a notification
export async function deleteNotification(notificationId: number): Promise<void> {
const response = await fetch(`${API_URL}/notifications/${notificationId}`, {
method: 'DELETE'
method: 'DELETE',
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to delete notification');
@@ -242,7 +299,9 @@ export async function deleteNotification(notificationId: number): Promise<void>
// Fetch a single script by ID
export async function fetchScriptById(id: number): Promise<Script> {
const response = await fetch(`${API_URL}/script/${id}`);
const response = await fetch(`${API_URL}/script/${id}`, {
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch script');
}
@@ -253,9 +312,7 @@ export async function fetchScriptById(id: number): Promise<Script> {
export async function updateScript(id: number, updatedScript: Partial<Script>): Promise<Script> {
const response = await fetch(`${API_URL}/script/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(updatedScript)
});
if (!response.ok) {
@@ -266,7 +323,9 @@ export async function updateScript(id: number, updatedScript: Partial<Script>):
// Fetch logs for a specific script
export async function fetchLogs(scriptId: number): Promise<Log[]> {
const response = await fetch(`${API_URL}/script/${scriptId}/log`);
const response = await fetch(`${API_URL}/script/${scriptId}/log`, {
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch logs');
}
@@ -277,9 +336,7 @@ export async function fetchLogs(scriptId: number): Promise<Log[]> {
export async function addLog(scriptId: number, log: Log): Promise<Log> {
const response = await fetch(`${API_URL}/script/${scriptId}/log`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(log)
});
if (!response.ok) {
@@ -291,7 +348,8 @@ export async function addLog(scriptId: number, log: Log): Promise<Log> {
// Execute a script by ID
export async function executeScript(scriptId: number): Promise<{ message: string }> {
const response = await fetch(`${API_URL}/script/${scriptId}/execute`, {
method: 'POST'
method: 'POST',
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to execute script');
@@ -302,7 +360,8 @@ export async function executeScript(scriptId: number): Promise<{ message: string
// Delete a log from a specific script
export async function deleteLog(scriptId: number, logId: number): Promise<void> {
const response = await fetch(`${API_URL}/script/${scriptId}/log/${logId}`, {
method: 'DELETE'
method: 'DELETE',
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to delete log');
@@ -313,7 +372,8 @@ export async function deleteLog(scriptId: number, logId: number): Promise<void>
// Delete a script
export async function deleteScript(id: number): Promise<void> {
const response = await fetch(`${API_URL}/script/${id}`, {
method: 'DELETE'
method: 'DELETE',
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to delete script');

View File

@@ -4,6 +4,14 @@
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { checkHealth } from '$lib/api';
import { goto } from '$app/navigation';
import { page } from '$app/state';
let notifications: Notification[] = $state([]);
let notificationId = 0;
let healthStatus = writable<'healthy' | 'unhealthy'>('unhealthy');
let isAuthenticated = $state(false);
let { children } = $props();
interface Notification {
@@ -12,10 +20,12 @@
message: string;
}
let notifications: Notification[] = $state([]);
let notificationId = 0;
let healthStatus = writable<'healthy' | 'unhealthy'>('unhealthy');
function checkAuth() {
if (typeof localStorage !== 'undefined') {
const token = localStorage.getItem('token');
isAuthenticated = !!token;
}
}
async function updateHealthStatus() {
const status = await checkHealth();
@@ -30,10 +40,23 @@
}, 4000);
}
function logout() {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('token');
isAuthenticated = false;
goto('/login');
}
}
onMount(() => {
window.showNotification = showNotification;
updateHealthStatus();
setInterval(updateHealthStatus, 10000); // Check health every 10 seconds
checkAuth();
// Redirect unauthenticated users to /login unless they're on /register or /login
if (!isAuthenticated && page.url.pathname !== '/register' && page.url.pathname !== '/login') {
goto('/login');
}
});
</script>
@@ -41,29 +64,37 @@
<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="/scripts" class="text-lg hover:text-gray-400">Scripts</a>
<a href="/notifications" class="text-lg hover:text-gray-400">Notifications</a>
<a href="/settings" class="text-lg hover:text-gray-400">
<Icon icon="material-symbols:settings" width="24" height="24" />
</a>
{#if isAuthenticated}
<a href="/scripts" class="text-lg hover:text-gray-400">Scripts</a>
<a href="/notifications" class="text-lg hover:text-gray-400">Notifications</a>
<button class="text-lg hover:text-gray-400" onclick={logout}>Logout</button>
<a href="/settings" class="text-lg hover:text-gray-400">
<Icon icon="material-symbols:settings" width="24" height="24" />
</a>
{:else}
<a href="/login" class="text-lg hover:text-gray-400">Login</a>
<a href="/register" class="text-lg hover:text-gray-400">Register</a>
{/if}
</div>
</div>
</nav>
<div class="relative">
{@render children()}
{#if isAuthenticated || page.url.pathname === '/login' || page.url.pathname === '/register'}
{@render children()}
{/if}
</div>
<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 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 class="fixed bottom-4 left-4 group">

View File

@@ -0,0 +1,91 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { goto } from '$app/navigation';
import { API_URL } from '$lib/api';
// Login form state
let loginUsername = $state('');
let loginPassword = $state('');
let loginError: string | null = $state(null);
let loginLoading = $state(false);
async function handleLogin(e: Event) {
e.preventDefault();
loginError = null;
loginLoading = true;
try {
const form = new FormData();
form.append('username', loginUsername);
form.append('password', loginPassword);
const response = await fetch(`${API_URL}/login`, {
method: 'POST',
body: form
});
if (!response.ok) {
const data = await response.json();
loginError = data.detail || 'Login failed';
loginLoading = false;
return;
}
const data = await response.json();
localStorage.setItem('token', data.access_token);
goto('/').then(() => location.reload());
} catch (err) {
loginError = 'Network error - ' + err;
} finally {
loginLoading = false;
}
}
</script>
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-50">
<form
class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 w-full max-w-sm"
onsubmit={handleLogin}
>
<h2 class="text-2xl font-bold mb-6 text-center">Login</h2>
{#if loginError}
<div class="mb-4 text-red-600 text-sm">{loginError}</div>
{/if}
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username"> Username </label>
<input
id="username"
type="text"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
bind:value={loginUsername}
required
autocomplete="username"
/>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password"> Password </label>
<input
id="password"
type="password"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
bind:value={loginPassword}
required
autocomplete="current-password"
/>
</div>
<div class="flex items-center justify-between">
<button
type="submit"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
disabled={loginLoading}
>
{loginLoading ? 'Logging in...' : 'Login'}
</button>
<a
href="/register"
class="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800 ml-4"
>
Create an account
</a>
</div>
</form>
</div>

View File

@@ -1,12 +0,0 @@
import { fetchSubscriptions } from '$lib/api';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
try {
const subscriptions = await fetchSubscriptions();
return { subscriptions };
} catch (error) {
console.error('Failed to load subscriptions:', error);
return { subscriptions: [] };
}
};

View File

@@ -1,10 +1,25 @@
<script lang="ts">
import { addSubscription, deleteSubscription, fetchSubscriptions } from '$lib/api';
import type { Subscription } from '$lib/api';
export let data: { subscriptions: Subscription[] };
import { onMount } from 'svelte';
let subscriptions: Subscription[] = data.subscriptions;
let subscriptions: Subscription[] = [];
let newTopic = '';
let loading: boolean = true;
let errorMsg: string | null = null;
onMount(async () => {
loading = true;
errorMsg = null;
try {
subscriptions = await fetchSubscriptions();
} catch (error) {
console.error('Failed to load subscriptions:', error);
errorMsg = 'Failed to load subscriptions';
subscriptions = [];
}
loading = false;
});
async function handleAddSubscription() {
if (!newTopic.trim()) {
@@ -35,19 +50,48 @@
<main class="p-4">
<h1 class="text-2xl font-bold mb-4">Subscriptions</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{#each subscriptions as subscription (subscription.id)}
<a
href={`/notifications/${subscription.id}`}
class="relative block p-4 border rounded bg-white hover:bg-gray-100 shadow-lg shadow-blue-500/50"
>
{#if subscription.has_unread}
<span class="absolute top-2 right-2 w-3 h-3 bg-green-500 rounded-full"></span>
{/if}
<h2 class="text-lg font-semibold text-gray-800">{subscription.topic}</h2>
</a>
{/each}
</div>
{#if loading}
<p>Loading...</p>
{:else if errorMsg}
<p class="text-red-500">{errorMsg}</p>
{:else}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{#each subscriptions as subscription (subscription.id)}
<div
class="relative block p-4 border rounded bg-white hover:bg-gray-100 shadow-lg shadow-blue-500/50"
>
<!-- Red cross button for delete -->
<button
class="absolute top-2 right-2 w-6 h-6 flex items-center justify-center bg-transparent text-red-600 hover:text-red-800"
aria-label="Delete subscription"
on:click|stopPropagation={() => handleDeleteSubscription(subscription.id)}
tabindex="0"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2" />
<line x1="6" y1="18" x2="18" y2="6" stroke="currentColor" stroke-width="2" />
</svg>
</button>
<a
href={`/notifications/${subscription.id}`}
class="block"
style="text-decoration: none;"
>
{#if subscription.has_unread}
<span class="absolute top-2 left-2 w-3 h-3 bg-green-500 rounded-full"></span>
{/if}
<h2 class="text-lg font-semibold text-gray-800">{subscription.topic}</h2>
</a>
</div>
{/each}
</div>
{/if}
<div class="mt-8">
<h2 class="text-xl font-semibold mb-2">Add New Subscription</h2>

View File

@@ -1,18 +0,0 @@
import { fetchSubscriptionNotifications, getSubscription } from '$lib/api';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
try {
const subscription_id: string = params.subscription_id;
const subscription = await getSubscription(subscription_id);
const notifications = (await fetchSubscriptionNotifications(subscription_id)).sort(
(a, b) => new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
);
return { subscription, notifications };
} catch (error) {
console.error('Failed to load:', error);
return { subscription: {}, notifications: [] };
}
};

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { deleteNotification, setViewed, fetchSubscriptionNotifications } from '$lib/api';
import type { Notification, Subscription } from '$lib/api';
let { data } = $props();
export let data: { notifications: Notification[]; subscription: Subscription };
let notifications: Notification[] = data.notifications;
let selectedNotification: Notification | null = null;
let subscription: Subscription | null = $state(data.subscription);
let notifications: Notification[] = $state(data.notifications);
let selectedNotification: Notification | null = $state(null);
// Delete all notifications for this subscription
async function handleDeleteAllNotifications() {
@@ -25,9 +25,9 @@
// Pagination state
let limit = 20;
let offset = notifications.length;
let loadingMore = false;
let allLoaded = notifications.length < limit;
let offset = $derived(notifications.length);
let loadingMore = $state(false);
let allLoaded = $derived(notifications.length < limit);
async function openNotificationPopup(notification: Notification) {
if (!notification.viewed) {
@@ -45,7 +45,9 @@
selectedNotification = null;
}
async function handleDeleteNotification(id: number) {
async function handleDeleteNotification(e, id: number) {
e.stopPropagation();
try {
await deleteNotification(id);
notifications = notifications.filter((notification) => notification.id !== id);
@@ -74,11 +76,7 @@
async function loadMoreNotifications() {
loadingMore = true;
try {
const more = await fetchSubscriptionNotifications(
data.subscription.id.toString(),
limit,
offset
);
const more = await fetchSubscriptionNotifications(subscription.id.toString(), limit, offset);
notifications = [...notifications, ...more];
offset += more.length;
if (more.length < limit) {
@@ -92,17 +90,17 @@
</script>
<main class="p-4">
<h1 class="text-2xl font-bold mb-4">Notifications for {data.subscription.topic}:</h1>
<h1 class="text-2xl font-bold mb-4">Notifications for {subscription.topic}:</h1>
<div class="flex justify-between items-center mb-4">
<a href="/notifications" class="text-blue-500 hover:underline">← Return to Subscriptions</a>
<div class="flex gap-2">
<button class="bg-blue-500 text-white px-4 py-2 rounded" on:click={markAllViewed}>
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick={markAllViewed}>
Mark All Viewed
</button>
<button
class="bg-red-500 text-white px-4 py-2 rounded"
on:click={handleDeleteAllNotifications}
onclick={handleDeleteAllNotifications}
disabled={notifications.length === 0}
>
Delete All Notifications
@@ -119,7 +117,7 @@
class="p-2 rounded flex justify-between items-center border-s-slate-400 border-1"
class:bg-green-200={notification.viewed}
>
<button class="p-2 w-full text-left" on:click={() => openNotificationPopup(notification)}>
<button class="p-2 w-full text-left" onclick={() => openNotificationPopup(notification)}>
<div>
<p class="font-semibold">{notification.title}</p>
<p class="text-sm text-gray-500">
@@ -129,7 +127,7 @@
</button>
<button
class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600 block"
on:click|stopPropagation={() => handleDeleteNotification(notification.id)}
onclick={(e) => handleDeleteNotification(e, notification.id)}
>
Delete
</button>
@@ -141,7 +139,7 @@
<div class="flex justify-center mt-6">
<button
class="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300"
on:click={loadMoreNotifications}
onclick={loadMoreNotifications}
disabled={loadingMore}
>
{loadingMore ? 'Loading...' : 'Load More'}
@@ -175,7 +173,7 @@
</div>
<button
class="mt-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
on:click={closeNotificationPopup}
onclick={closeNotificationPopup}
>
Close
</button>

View File

@@ -0,0 +1,23 @@
import type { PageLoad } from './$types';
import { getSubscription, fetchSubscriptionNotifications } from '$lib/api';
export const load: PageLoad = async ({ params }) => {
if (import.meta.env.SSR) {
return {
subscription: null,
notifications: []
};
} else {
const subscription_id = params.subscription_id;
const subscription = await getSubscription(subscription_id);
const notifications = (await fetchSubscriptionNotifications(subscription_id)).sort(
(a, b) => new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
);
return {
subscription: subscription,
notifications: notifications
};
}
};

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { API_URL } from '$lib/api';
let username = '';
let password = '';
let error: string | null = null;
let loading = false;
async function handleRegister(e: Event) {
e.preventDefault();
error = null;
loading = true;
try {
const response = await fetch(`${API_URL}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
if (!response.ok) {
const data = await response.json();
error = data.detail || 'Registration failed';
loading = false;
return;
}
const data = await response.json();
localStorage.setItem('token', data.access_token);
goto('/');
} catch (err) {
error = 'Network error';
} finally {
loading = false;
}
}
</script>
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-50">
<form
class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 w-full max-w-sm"
on:submit|preventDefault={handleRegister}
>
<h2 class="text-2xl font-bold mb-6 text-center">Register</h2>
{#if error}
<div class="mb-4 text-red-600 text-sm">{error}</div>
{/if}
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username"> Username </label>
<input
id="username"
type="text"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
bind:value={username}
required
autocomplete="username"
/>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password"> Password </label>
<input
id="password"
type="password"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
bind:value={password}
required
autocomplete="new-password"
/>
</div>
<div class="flex items-center justify-between">
<button
type="submit"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
disabled={loading}
>
{loading ? 'Registering...' : 'Register'}
</button>
<a
href="/login"
class="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800 ml-4"
>
Already have an account?
</a>
</div>
</form>
</div>

View File

@@ -1,12 +0,0 @@
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

@@ -1,19 +1,27 @@
<script lang="ts">
import { addScript } from '$lib/api';
import { addScript, fetchScripts } from '$lib/api';
import type { Script } from '$lib/api';
import CodeMirror from 'svelte-codemirror-editor';
import { python } from '@codemirror/lang-python';
import { onMount } from 'svelte';
export let data: { scripts: Script[] };
let scripts: Script[] = data.scripts;
let newScript: Omit<Script, 'id' | 'created_at'> = { name: '', script_content: '' };
let scripts: Script[] = [];
let newScript: Omit<Script, 'id' | 'created_at'> = {
name: '',
script_content: '',
enabled: false
};
onMount(async () => {
scripts = await fetchScripts();
});
// Add a new script
async function handleAddScript() {
try {
const addedScript = await addScript(newScript);
scripts = [...scripts, addedScript];
newScript = { name: '', script_content: '' };
newScript = { name: '', script_content: '', enabled: false };
} catch (err) {
window.showNotification('Failed to add script. ' + err);
}
@@ -38,11 +46,7 @@
<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())}
>
<form onsubmit={handleAddScript} class="space-y-4 p-4 border rounded shadow">
<div>
<label for="name" class="block text-sm font-medium">Name</label>
<input

View File

@@ -1,22 +0,0 @@
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

@@ -2,30 +2,49 @@
import {
updateScript,
deleteScript,
addLog,
deleteLog,
executeScript,
fetchScriptById,
fetchLogs
} from '$lib/api';
import type { Script, Log } from '$lib/api';
import CodeMirror from 'svelte-codemirror-editor';
import { python } from '@codemirror/lang-python';
import { onMount } from 'svelte';
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 updatedEnabled: boolean = script.enabled || false;
export let params: { id: string };
let script: Script = null;
let logs: Log[] = [];
let updatedTitle: string = '';
let updatedContent: string = '';
let updatedEnabled: boolean = false;
let loading: boolean = true;
let errorMsg: string | null = null;
onMount(async () => {
try {
const fetchedScript = await fetchScriptById(parseInt(params.id));
if (!fetchedScript) {
errorMsg = 'Script not found';
loading = false;
return;
}
script = fetchedScript;
updatedTitle = script.name || '';
updatedContent = script.script_content || '';
updatedEnabled = script.enabled || false;
const fetchedLogs: Log[] = (await fetchLogs(script.id)).sort(
(a, b) => new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
);
logs = fetchedLogs;
} catch (err) {
errorMsg = 'Failed to fetch script data - ' + err;
}
loading = false;
});
let isEditMode: boolean = false;
let newLog: Omit<Log, 'id' | 'script_id'> = {
message: '',
error_code: 0,
error_message: ''
};
let selectedLog: Log | null = null;
function openLogPopup(log: Log) {
@@ -66,23 +85,6 @@
}
}
async function handleAddLog() {
if (newLog.message.trim()) {
try {
const addedLog = await addLog(script.id, newLog);
logs = [addedLog, ...logs];
newLog = {
message: '',
error_code: 0,
error_message: ''
};
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);
@@ -109,7 +111,11 @@
<main class="p-4">
<!-- Removed local notification container as notifications are now global -->
{#if script}
{#if loading}
<p>Loading...</p>
{:else if errorMsg}
<p class="text-red-500">{errorMsg}</p>
{:else if script}
{#if isEditMode}
<input
type="text"
@@ -190,46 +196,6 @@
<section class="mt-8">
<h2 class="text-xl font-bold mb-4">Logs</h2>
<!--- --
<form on:submit|preventDefault={handleAddLog} class="mb-4 space-y-4">
<div>
<label for="logMessage" class="block text-sm font-medium">Log Message</label>
<input
id="logMessage"
type="text"
bind:value={newLog.message}
placeholder="Enter new log message"
class="w-full p-2 border rounded"
required
/>
</div>
<div>
<label for="errorCode" class="block text-sm font-medium">Error Code</label>
<input
id="errorCode"
type="number"
bind:value={newLog.error_code}
placeholder="Enter error code (0 for no error)"
class="w-full p-2 border rounded"
required
/>
</div>
<div>
<label for="errorMessage" class="block text-sm font-medium">Error Message</label>
<textarea
id="errorMessage"
type="text"
bind:value={newLog.error_message}
placeholder="Enter error message (optional)"
class="w-full p-2 border rounded"
>
</textarea>
</div>
<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

View File

@@ -1,37 +1,36 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fetchSettings, updateSetting } from '$lib/api';
import { fetchUserSettings, updateSetting } from '$lib/api';
import type { Settings } from '$lib/api';
import { writable } from 'svelte/store';
import CodeMirror from 'svelte-codemirror-editor';
let settings = writable<Settings[]>([]);
let isLoading = writable(false);
let error = writable<string | null>(null);
let settings: Settings = $state(null);
let isLoading = $state(false);
let error: string | null = $state(null);
async function loadSettings() {
isLoading.set(true);
error.set(null);
isLoading = true;
error = null;
try {
const data = await fetchSettings();
settings.set(data);
const data = await fetchUserSettings();
settings = data;
} catch (err) {
error.set('Failed to load settings');
error = 'Failed to load settings - ' + err;
} finally {
isLoading.set(false);
isLoading = false;
}
}
async function saveSetting(setting: Settings) {
isLoading.set(true);
error.set(null);
isLoading = true;
error = null;
try {
await updateSetting(setting.id, setting);
loadSettings(); // Refresh settings after update
} catch (err) {
error.set('Failed to save setting');
error = 'Failed to save settings - ' + err;
} finally {
isLoading.set(false);
isLoading = false;
}
}
@@ -43,45 +42,43 @@
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">Settings</h1>
{#if $isLoading}
{#if isLoading}
<p>Loading...</p>
{:else if $error}
{:else if error}
<p class="text-red-500">{$error}</p>
{:else}
{:else if settings !== null}
<div class="space-y-4">
{#each $settings as setting (setting.id)}
<div class="p-4 border rounded shadow">
<label class="block mb-2 font-bold">
Requirements
<div class="w-full border rounded">
<CodeMirror bind:value={setting.requirements} />
</div>
</label>
<div class="p-4 border rounded shadow">
<label class="block mb-2 font-bold">
Requirements
<div class="w-full border rounded">
<CodeMirror bind:value={settings.requirements} />
</div>
</label>
<label class="block mt-4 mb-2 font-bold">
Environment
<div class="w-full border rounded">
<CodeMirror bind:value={setting.environment} />
</div>
</label>
<label class="block mt-4 mb-2 font-bold">
Environment
<div class="w-full border rounded">
<CodeMirror bind:value={settings.environment} />
</div>
</label>
<label class="block mt-4 mb-2 font-bold">
Ntfy URL
<input
type="text"
class="w-full p-2 border rounde font-normal"
bind:value={setting.ntfy_url}
/>
</label>
<label class="block mt-4 mb-2 font-bold">
Ntfy URL
<input
type="text"
class="w-full p-2 border rounde font-normal"
bind:value={settings.ntfy_url}
/>
</label>
<button
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
on:click={() => saveSetting(setting)}
>
Save
</button>
</div>
{/each}
<button
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
onclick={() => saveSetting(settings)}
>
Save
</button>
</div>
</div>
{/if}
</div>