Add frontend support for user
This commit is contained in:
@@ -2,6 +2,56 @@ import { env } from '$env/dynamic/public';
|
|||||||
|
|
||||||
export const API_URL = env.PUBLIC_API_URL || 'http://localhost:8000';
|
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
|
* Type definitions for Subscriptions and Notifications
|
||||||
*/
|
*/
|
||||||
@@ -67,7 +117,9 @@ export interface Script {
|
|||||||
|
|
||||||
// Fetch all scripts
|
// Fetch all scripts
|
||||||
export async function fetchScripts(): Promise<Script[]> {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch scripts ' + response.statusText);
|
throw new Error('Failed to fetch scripts ' + response.statusText);
|
||||||
}
|
}
|
||||||
@@ -80,9 +132,7 @@ export async function addScript(
|
|||||||
): Promise<Script> {
|
): Promise<Script> {
|
||||||
const response = await fetch(`${API_URL}/script`, {
|
const response = await fetch(`${API_URL}/script`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(script)
|
body: JSON.stringify(script)
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -92,8 +142,10 @@ export async function addScript(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all settings
|
// Fetch all settings
|
||||||
export async function fetchSettings(): Promise<Settings[]> {
|
export async function fetchUserSettings(): Promise<Settings> {
|
||||||
const response = await fetch(`${API_URL}/settings`);
|
const response = await fetch(`${API_URL}/settings`, {
|
||||||
|
headers: authHeaders()
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch settings ' + response.statusText);
|
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
|
// Fetch a single setting by ID
|
||||||
export async function fetchSettingById(id: number): Promise<Settings> {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch setting');
|
throw new Error('Failed to fetch setting');
|
||||||
}
|
}
|
||||||
@@ -116,9 +170,7 @@ export async function updateSetting(
|
|||||||
): Promise<Settings> {
|
): Promise<Settings> {
|
||||||
const response = await fetch(`${API_URL}/settings/${id}`, {
|
const response = await fetch(`${API_URL}/settings/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ ...updatedSetting, ntfy_url: updatedSetting.ntfy_url })
|
body: JSON.stringify({ ...updatedSetting, ntfy_url: updatedSetting.ntfy_url })
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -129,7 +181,9 @@ export async function updateSetting(
|
|||||||
|
|
||||||
// Fetch all subscriptions
|
// Fetch all subscriptions
|
||||||
export async function fetchSubscriptions(): Promise<Subscription[]> {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch subscriptions');
|
throw new Error('Failed to fetch subscriptions');
|
||||||
}
|
}
|
||||||
@@ -138,7 +192,9 @@ export async function fetchSubscriptions(): Promise<Subscription[]> {
|
|||||||
|
|
||||||
// Fetch subscriptions by topic
|
// Fetch subscriptions by topic
|
||||||
export async function getSubscription(topic_id: string): Promise<Subscription> {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch subscriptions');
|
throw new Error('Failed to fetch subscriptions');
|
||||||
}
|
}
|
||||||
@@ -154,9 +210,7 @@ export async function addNotification(
|
|||||||
): Promise<Notification> {
|
): Promise<Notification> {
|
||||||
const response = await fetch(`${API_URL}/notifications`, {
|
const response = await fetch(`${API_URL}/notifications`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ subscription_id: subscriptionId, title, message, priority })
|
body: JSON.stringify({ subscription_id: subscriptionId, title, message, priority })
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -169,9 +223,7 @@ export async function addNotification(
|
|||||||
export async function setViewed(notificationId: number): Promise<Notification> {
|
export async function setViewed(notificationId: number): Promise<Notification> {
|
||||||
const response = await fetch(`${API_URL}/notifications/${notificationId}`, {
|
const response = await fetch(`${API_URL}/notifications/${notificationId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ viewed: true })
|
body: JSON.stringify({ viewed: true })
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -184,9 +236,7 @@ export async function setViewed(notificationId: number): Promise<Notification> {
|
|||||||
export async function addSubscription(topic: string): Promise<Subscription> {
|
export async function addSubscription(topic: string): Promise<Subscription> {
|
||||||
const response = await fetch(`${API_URL}/subscriptions`, {
|
const response = await fetch(`${API_URL}/subscriptions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ topic })
|
body: JSON.stringify({ topic })
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -198,7 +248,8 @@ export async function addSubscription(topic: string): Promise<Subscription> {
|
|||||||
// Delete a subscription
|
// Delete a subscription
|
||||||
export async function deleteSubscription(subscriptionId: number): Promise<void> {
|
export async function deleteSubscription(subscriptionId: number): Promise<void> {
|
||||||
const response = await fetch(`${API_URL}/subscriptions/${subscriptionId}`, {
|
const response = await fetch(`${API_URL}/subscriptions/${subscriptionId}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE',
|
||||||
|
headers: authHeaders()
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to delete subscription');
|
throw new Error('Failed to delete subscription');
|
||||||
@@ -212,7 +263,10 @@ export async function fetchSubscriptionNotifications(
|
|||||||
offset: number = 0
|
offset: number = 0
|
||||||
): Promise<Notification[]> {
|
): Promise<Notification[]> {
|
||||||
const response = await fetch(
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch subscription notifications');
|
throw new Error('Failed to fetch subscription notifications');
|
||||||
@@ -223,7 +277,9 @@ export async function fetchSubscriptionNotifications(
|
|||||||
// Fetch all notifications or filter by topic
|
// Fetch all notifications or filter by topic
|
||||||
export async function fetchAllNotifications(): Promise<Notification[]> {
|
export async function fetchAllNotifications(): Promise<Notification[]> {
|
||||||
const url = `${API_URL}/notifications`;
|
const url = `${API_URL}/notifications`;
|
||||||
const response = await fetch(url);
|
const response = await fetch(url, {
|
||||||
|
headers: authHeaders()
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch notifications');
|
throw new Error('Failed to fetch notifications');
|
||||||
}
|
}
|
||||||
@@ -233,7 +289,8 @@ export async function fetchAllNotifications(): Promise<Notification[]> {
|
|||||||
// Delete a notification
|
// Delete a notification
|
||||||
export async function deleteNotification(notificationId: number): Promise<void> {
|
export async function deleteNotification(notificationId: number): Promise<void> {
|
||||||
const response = await fetch(`${API_URL}/notifications/${notificationId}`, {
|
const response = await fetch(`${API_URL}/notifications/${notificationId}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE',
|
||||||
|
headers: authHeaders()
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to delete notification');
|
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
|
// Fetch a single script by ID
|
||||||
export async function fetchScriptById(id: number): Promise<Script> {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch script');
|
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> {
|
export async function updateScript(id: number, updatedScript: Partial<Script>): Promise<Script> {
|
||||||
const response = await fetch(`${API_URL}/script/${id}`, {
|
const response = await fetch(`${API_URL}/script/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(updatedScript)
|
body: JSON.stringify(updatedScript)
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -266,7 +323,9 @@ export async function updateScript(id: number, updatedScript: Partial<Script>):
|
|||||||
|
|
||||||
// Fetch logs for a specific script
|
// Fetch logs for a specific script
|
||||||
export async function fetchLogs(scriptId: number): Promise<Log[]> {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch logs');
|
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> {
|
export async function addLog(scriptId: number, log: Log): Promise<Log> {
|
||||||
const response = await fetch(`${API_URL}/script/${scriptId}/log`, {
|
const response = await fetch(`${API_URL}/script/${scriptId}/log`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(log)
|
body: JSON.stringify(log)
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -291,7 +348,8 @@ export async function addLog(scriptId: number, log: Log): Promise<Log> {
|
|||||||
// Execute a script by ID
|
// Execute a script by ID
|
||||||
export async function executeScript(scriptId: number): Promise<{ message: string }> {
|
export async function executeScript(scriptId: number): Promise<{ message: string }> {
|
||||||
const response = await fetch(`${API_URL}/script/${scriptId}/execute`, {
|
const response = await fetch(`${API_URL}/script/${scriptId}/execute`, {
|
||||||
method: 'POST'
|
method: 'POST',
|
||||||
|
headers: authHeaders()
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to execute script');
|
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
|
// Delete a log from a specific script
|
||||||
export async function deleteLog(scriptId: number, logId: number): Promise<void> {
|
export async function deleteLog(scriptId: number, logId: number): Promise<void> {
|
||||||
const response = await fetch(`${API_URL}/script/${scriptId}/log/${logId}`, {
|
const response = await fetch(`${API_URL}/script/${scriptId}/log/${logId}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE',
|
||||||
|
headers: authHeaders()
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to delete log');
|
throw new Error('Failed to delete log');
|
||||||
@@ -313,7 +372,8 @@ export async function deleteLog(scriptId: number, logId: number): Promise<void>
|
|||||||
// Delete a script
|
// Delete a script
|
||||||
export async function deleteScript(id: number): Promise<void> {
|
export async function deleteScript(id: number): Promise<void> {
|
||||||
const response = await fetch(`${API_URL}/script/${id}`, {
|
const response = await fetch(`${API_URL}/script/${id}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE',
|
||||||
|
headers: authHeaders()
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to delete script');
|
throw new Error('Failed to delete script');
|
||||||
|
|||||||
@@ -4,6 +4,14 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import { checkHealth } from '$lib/api';
|
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();
|
let { children } = $props();
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
@@ -12,10 +20,12 @@
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let notifications: Notification[] = $state([]);
|
function checkAuth() {
|
||||||
let notificationId = 0;
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
let healthStatus = writable<'healthy' | 'unhealthy'>('unhealthy');
|
isAuthenticated = !!token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateHealthStatus() {
|
async function updateHealthStatus() {
|
||||||
const status = await checkHealth();
|
const status = await checkHealth();
|
||||||
@@ -30,10 +40,23 @@
|
|||||||
}, 4000);
|
}, 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
isAuthenticated = false;
|
||||||
|
goto('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.showNotification = showNotification;
|
window.showNotification = showNotification;
|
||||||
updateHealthStatus();
|
updateHealthStatus();
|
||||||
setInterval(updateHealthStatus, 10000); // Check health every 10 seconds
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -41,29 +64,37 @@
|
|||||||
<div class="container mx-auto flex justify-between items-center p-4">
|
<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>
|
<a href="/" class="text-2xl font-bold hover:text-gray-400">Project Monitor</a>
|
||||||
<div class="flex space-x-6">
|
<div class="flex space-x-6">
|
||||||
<a href="/scripts" class="text-lg hover:text-gray-400">Scripts</a>
|
{#if isAuthenticated}
|
||||||
<a href="/notifications" class="text-lg hover:text-gray-400">Notifications</a>
|
<a href="/scripts" class="text-lg hover:text-gray-400">Scripts</a>
|
||||||
<a href="/settings" class="text-lg hover:text-gray-400">
|
<a href="/notifications" class="text-lg hover:text-gray-400">Notifications</a>
|
||||||
<Icon icon="material-symbols:settings" width="24" height="24" />
|
<button class="text-lg hover:text-gray-400" onclick={logout}>Logout</button>
|
||||||
</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="relative">
|
<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">
|
<div class="fixed bottom-4 right-4 space-y-2">
|
||||||
{#each notifications as notification (notification.id)}
|
{#each notifications as notification (notification.id)}
|
||||||
<div
|
<div
|
||||||
class="p-4 rounded shadow-lg text-white"
|
class="p-4 rounded shadow-lg text-white"
|
||||||
class:bg-green-500={notification.type === 'success'}
|
class:bg-green-500={notification.type === 'success'}
|
||||||
class:bg-red-500={notification.type === 'error'}
|
class:bg-red-500={notification.type === 'error'}
|
||||||
>
|
>
|
||||||
{notification.message}
|
{notification.message}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fixed bottom-4 left-4 group">
|
<div class="fixed bottom-4 left-4 group">
|
||||||
|
|||||||
91
frontend/src/routes/login/+page.svelte
Normal file
91
frontend/src/routes/login/+page.svelte
Normal 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>
|
||||||
@@ -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: [] };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { addSubscription, deleteSubscription, fetchSubscriptions } from '$lib/api';
|
import { addSubscription, deleteSubscription, fetchSubscriptions } from '$lib/api';
|
||||||
import type { Subscription } 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 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() {
|
async function handleAddSubscription() {
|
||||||
if (!newTopic.trim()) {
|
if (!newTopic.trim()) {
|
||||||
@@ -35,19 +50,48 @@
|
|||||||
<main class="p-4">
|
<main class="p-4">
|
||||||
<h1 class="text-2xl font-bold mb-4">Subscriptions</h1>
|
<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">
|
{#if loading}
|
||||||
{#each subscriptions as subscription (subscription.id)}
|
<p>Loading...</p>
|
||||||
<a
|
{:else if errorMsg}
|
||||||
href={`/notifications/${subscription.id}`}
|
<p class="text-red-500">{errorMsg}</p>
|
||||||
class="relative block p-4 border rounded bg-white hover:bg-gray-100 shadow-lg shadow-blue-500/50"
|
{:else}
|
||||||
>
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{#if subscription.has_unread}
|
{#each subscriptions as subscription (subscription.id)}
|
||||||
<span class="absolute top-2 right-2 w-3 h-3 bg-green-500 rounded-full"></span>
|
<div
|
||||||
{/if}
|
class="relative block p-4 border rounded bg-white hover:bg-gray-100 shadow-lg shadow-blue-500/50"
|
||||||
<h2 class="text-lg font-semibold text-gray-800">{subscription.topic}</h2>
|
>
|
||||||
</a>
|
<!-- Red cross button for delete -->
|
||||||
{/each}
|
<button
|
||||||
</div>
|
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">
|
<div class="mt-8">
|
||||||
<h2 class="text-xl font-semibold mb-2">Add New Subscription</h2>
|
<h2 class="text-xl font-semibold mb-2">Add New Subscription</h2>
|
||||||
|
|||||||
@@ -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: [] };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { deleteNotification, setViewed, fetchSubscriptionNotifications } from '$lib/api';
|
import { deleteNotification, setViewed, fetchSubscriptionNotifications } from '$lib/api';
|
||||||
import type { Notification, Subscription } from '$lib/api';
|
import type { Notification, Subscription } from '$lib/api';
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
export let data: { notifications: Notification[]; subscription: Subscription };
|
let subscription: Subscription | null = $state(data.subscription);
|
||||||
|
let notifications: Notification[] = $state(data.notifications);
|
||||||
let notifications: Notification[] = data.notifications;
|
let selectedNotification: Notification | null = $state(null);
|
||||||
let selectedNotification: Notification | null = null;
|
|
||||||
|
|
||||||
// Delete all notifications for this subscription
|
// Delete all notifications for this subscription
|
||||||
async function handleDeleteAllNotifications() {
|
async function handleDeleteAllNotifications() {
|
||||||
@@ -25,9 +25,9 @@
|
|||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
let limit = 20;
|
let limit = 20;
|
||||||
let offset = notifications.length;
|
let offset = $derived(notifications.length);
|
||||||
let loadingMore = false;
|
let loadingMore = $state(false);
|
||||||
let allLoaded = notifications.length < limit;
|
let allLoaded = $derived(notifications.length < limit);
|
||||||
|
|
||||||
async function openNotificationPopup(notification: Notification) {
|
async function openNotificationPopup(notification: Notification) {
|
||||||
if (!notification.viewed) {
|
if (!notification.viewed) {
|
||||||
@@ -45,7 +45,9 @@
|
|||||||
selectedNotification = null;
|
selectedNotification = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteNotification(id: number) {
|
async function handleDeleteNotification(e, id: number) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteNotification(id);
|
await deleteNotification(id);
|
||||||
notifications = notifications.filter((notification) => notification.id !== id);
|
notifications = notifications.filter((notification) => notification.id !== id);
|
||||||
@@ -74,11 +76,7 @@
|
|||||||
async function loadMoreNotifications() {
|
async function loadMoreNotifications() {
|
||||||
loadingMore = true;
|
loadingMore = true;
|
||||||
try {
|
try {
|
||||||
const more = await fetchSubscriptionNotifications(
|
const more = await fetchSubscriptionNotifications(subscription.id.toString(), limit, offset);
|
||||||
data.subscription.id.toString(),
|
|
||||||
limit,
|
|
||||||
offset
|
|
||||||
);
|
|
||||||
notifications = [...notifications, ...more];
|
notifications = [...notifications, ...more];
|
||||||
offset += more.length;
|
offset += more.length;
|
||||||
if (more.length < limit) {
|
if (more.length < limit) {
|
||||||
@@ -92,17 +90,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="p-4">
|
<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">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<a href="/notifications" class="text-blue-500 hover:underline">← Return to Subscriptions</a>
|
<a href="/notifications" class="text-blue-500 hover:underline">← Return to Subscriptions</a>
|
||||||
<div class="flex gap-2">
|
<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
|
Mark All Viewed
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="bg-red-500 text-white px-4 py-2 rounded"
|
class="bg-red-500 text-white px-4 py-2 rounded"
|
||||||
on:click={handleDeleteAllNotifications}
|
onclick={handleDeleteAllNotifications}
|
||||||
disabled={notifications.length === 0}
|
disabled={notifications.length === 0}
|
||||||
>
|
>
|
||||||
Delete All Notifications
|
Delete All Notifications
|
||||||
@@ -119,7 +117,7 @@
|
|||||||
class="p-2 rounded flex justify-between items-center border-s-slate-400 border-1"
|
class="p-2 rounded flex justify-between items-center border-s-slate-400 border-1"
|
||||||
class:bg-green-200={notification.viewed}
|
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>
|
<div>
|
||||||
<p class="font-semibold">{notification.title}</p>
|
<p class="font-semibold">{notification.title}</p>
|
||||||
<p class="text-sm text-gray-500">
|
<p class="text-sm text-gray-500">
|
||||||
@@ -129,7 +127,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600 block"
|
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
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -141,7 +139,7 @@
|
|||||||
<div class="flex justify-center mt-6">
|
<div class="flex justify-center mt-6">
|
||||||
<button
|
<button
|
||||||
class="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300"
|
class="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300"
|
||||||
on:click={loadMoreNotifications}
|
onclick={loadMoreNotifications}
|
||||||
disabled={loadingMore}
|
disabled={loadingMore}
|
||||||
>
|
>
|
||||||
{loadingMore ? 'Loading...' : 'Load More'}
|
{loadingMore ? 'Loading...' : 'Load More'}
|
||||||
@@ -175,7 +173,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="mt-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
class="mt-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||||
on:click={closeNotificationPopup}
|
onclick={closeNotificationPopup}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
23
frontend/src/routes/notifications/[subscription_id]/+page.ts
Normal file
23
frontend/src/routes/notifications/[subscription_id]/+page.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
86
frontend/src/routes/register/+page.svelte
Normal file
86
frontend/src/routes/register/+page.svelte
Normal 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>
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { addScript } from '$lib/api';
|
import { addScript, fetchScripts } from '$lib/api';
|
||||||
import type { Script } from '$lib/api';
|
import type { Script } from '$lib/api';
|
||||||
import CodeMirror from 'svelte-codemirror-editor';
|
import CodeMirror from 'svelte-codemirror-editor';
|
||||||
import { python } from '@codemirror/lang-python';
|
import { python } from '@codemirror/lang-python';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
export let data: { scripts: Script[] };
|
let scripts: Script[] = [];
|
||||||
let scripts: Script[] = data.scripts;
|
let newScript: Omit<Script, 'id' | 'created_at'> = {
|
||||||
let newScript: Omit<Script, 'id' | 'created_at'> = { name: '', script_content: '' };
|
name: '',
|
||||||
|
script_content: '',
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
scripts = await fetchScripts();
|
||||||
|
});
|
||||||
|
|
||||||
// Add a new script
|
// Add a new script
|
||||||
async function handleAddScript() {
|
async function handleAddScript() {
|
||||||
try {
|
try {
|
||||||
const addedScript = await addScript(newScript);
|
const addedScript = await addScript(newScript);
|
||||||
scripts = [...scripts, addedScript];
|
scripts = [...scripts, addedScript];
|
||||||
newScript = { name: '', script_content: '' };
|
newScript = { name: '', script_content: '', enabled: false };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.showNotification('Failed to add script. ' + err);
|
window.showNotification('Failed to add script. ' + err);
|
||||||
}
|
}
|
||||||
@@ -38,11 +46,7 @@
|
|||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h2 class="text-xl font-semibold mb-2">Add New Script</h2>
|
<h2 class="text-xl font-semibold mb-2">Add New Script</h2>
|
||||||
<form
|
<form onsubmit={handleAddScript} class="space-y-4 p-4 border rounded shadow">
|
||||||
on:submit|preventDefault={handleAddScript}
|
|
||||||
class="space-y-4 p-4 border rounded shadow"
|
|
||||||
on:submit={() => (newScript.script_content = editor.getValue())}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<label for="name" class="block text-sm font-medium">Name</label>
|
<label for="name" class="block text-sm font-medium">Name</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,30 +2,49 @@
|
|||||||
import {
|
import {
|
||||||
updateScript,
|
updateScript,
|
||||||
deleteScript,
|
deleteScript,
|
||||||
addLog,
|
|
||||||
deleteLog,
|
deleteLog,
|
||||||
executeScript,
|
executeScript,
|
||||||
|
fetchScriptById,
|
||||||
fetchLogs
|
fetchLogs
|
||||||
} from '$lib/api';
|
} from '$lib/api';
|
||||||
import type { Script, Log } from '$lib/api';
|
import type { Script, Log } from '$lib/api';
|
||||||
import CodeMirror from 'svelte-codemirror-editor';
|
import CodeMirror from 'svelte-codemirror-editor';
|
||||||
import { python } from '@codemirror/lang-python';
|
import { python } from '@codemirror/lang-python';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
export let data: { script: Script; logs: Log[] };
|
export let params: { id: string };
|
||||||
let script: Script = data.script;
|
let script: Script = null;
|
||||||
let logs: Log[] = data.logs;
|
let logs: Log[] = [];
|
||||||
let updatedTitle: string = script.name || '';
|
let updatedTitle: string = '';
|
||||||
let updatedContent: string = script.script_content || '';
|
let updatedContent: string = '';
|
||||||
let updatedEnabled: boolean = script.enabled || false;
|
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 isEditMode: boolean = false;
|
||||||
|
|
||||||
let newLog: Omit<Log, 'id' | 'script_id'> = {
|
|
||||||
message: '',
|
|
||||||
error_code: 0,
|
|
||||||
error_message: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
let selectedLog: Log | null = null;
|
let selectedLog: Log | null = null;
|
||||||
|
|
||||||
function openLogPopup(log: Log) {
|
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) {
|
async function handleDeleteLog(logId: number) {
|
||||||
try {
|
try {
|
||||||
await deleteLog(script.id, logId);
|
await deleteLog(script.id, logId);
|
||||||
@@ -109,7 +111,11 @@
|
|||||||
<main class="p-4">
|
<main class="p-4">
|
||||||
<!-- Removed local notification container as notifications are now global -->
|
<!-- 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}
|
{#if isEditMode}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -190,46 +196,6 @@
|
|||||||
|
|
||||||
<section class="mt-8">
|
<section class="mt-8">
|
||||||
<h2 class="text-xl font-bold mb-4">Logs</h2>
|
<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">
|
<ul class="space-y-4">
|
||||||
{#each logs as log (log.id)}
|
{#each logs as log (log.id)}
|
||||||
<li
|
<li
|
||||||
|
|||||||
@@ -1,37 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { fetchSettings, updateSetting } from '$lib/api';
|
import { fetchUserSettings, updateSetting } from '$lib/api';
|
||||||
import type { Settings } from '$lib/api';
|
import type { Settings } from '$lib/api';
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
import CodeMirror from 'svelte-codemirror-editor';
|
import CodeMirror from 'svelte-codemirror-editor';
|
||||||
|
|
||||||
let settings = writable<Settings[]>([]);
|
let settings: Settings = $state(null);
|
||||||
let isLoading = writable(false);
|
let isLoading = $state(false);
|
||||||
let error = writable<string | null>(null);
|
let error: string | null = $state(null);
|
||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
isLoading.set(true);
|
isLoading = true;
|
||||||
error.set(null);
|
error = null;
|
||||||
try {
|
try {
|
||||||
const data = await fetchSettings();
|
const data = await fetchUserSettings();
|
||||||
settings.set(data);
|
settings = data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.set('Failed to load settings');
|
error = 'Failed to load settings - ' + err;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.set(false);
|
isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSetting(setting: Settings) {
|
async function saveSetting(setting: Settings) {
|
||||||
isLoading.set(true);
|
isLoading = true;
|
||||||
error.set(null);
|
error = null;
|
||||||
try {
|
try {
|
||||||
await updateSetting(setting.id, setting);
|
await updateSetting(setting.id, setting);
|
||||||
loadSettings(); // Refresh settings after update
|
loadSettings(); // Refresh settings after update
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.set('Failed to save setting');
|
error = 'Failed to save settings - ' + err;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.set(false);
|
isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,45 +42,43 @@
|
|||||||
<div class="container mx-auto p-4">
|
<div class="container mx-auto p-4">
|
||||||
<h1 class="text-2xl font-bold mb-4">Settings</h1>
|
<h1 class="text-2xl font-bold mb-4">Settings</h1>
|
||||||
|
|
||||||
{#if $isLoading}
|
{#if isLoading}
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
{:else if $error}
|
{:else if error}
|
||||||
<p class="text-red-500">{$error}</p>
|
<p class="text-red-500">{$error}</p>
|
||||||
{:else}
|
{:else if settings !== null}
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{#each $settings as setting (setting.id)}
|
<div class="p-4 border rounded shadow">
|
||||||
<div class="p-4 border rounded shadow">
|
<label class="block mb-2 font-bold">
|
||||||
<label class="block mb-2 font-bold">
|
Requirements
|
||||||
Requirements
|
<div class="w-full border rounded">
|
||||||
<div class="w-full border rounded">
|
<CodeMirror bind:value={settings.requirements} />
|
||||||
<CodeMirror bind:value={setting.requirements} />
|
</div>
|
||||||
</div>
|
</label>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="block mt-4 mb-2 font-bold">
|
<label class="block mt-4 mb-2 font-bold">
|
||||||
Environment
|
Environment
|
||||||
<div class="w-full border rounded">
|
<div class="w-full border rounded">
|
||||||
<CodeMirror bind:value={setting.environment} />
|
<CodeMirror bind:value={settings.environment} />
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="block mt-4 mb-2 font-bold">
|
<label class="block mt-4 mb-2 font-bold">
|
||||||
Ntfy URL
|
Ntfy URL
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full p-2 border rounde font-normal"
|
class="w-full p-2 border rounde font-normal"
|
||||||
bind:value={setting.ntfy_url}
|
bind:value={settings.ntfy_url}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||||
on:click={() => saveSetting(setting)}
|
onclick={() => saveSetting(settings)}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user