diff --git a/package-lock.json b/package-lock.json
index 75ddf7b..5e088bd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,9 +19,7 @@
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@ethercorps/sveltekit-redis-session": "^1.3.1",
- "@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/adapter-node": "^5.2.12",
- "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/postcss": "^4.0.4",
@@ -1319,19 +1317,6 @@
"win32"
]
},
- "node_modules/@sveltejs/adapter-auto": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-4.0.0.tgz",
- "integrity": "sha512-kmuYSQdD2AwThymQF0haQhM8rE5rhutQXG4LNbnbShwhMO4qQGnKaaTy+88DuNSuoQDi58+thpq8XpHc1+oEKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "import-meta-resolve": "^4.1.0"
- },
- "peerDependencies": {
- "@sveltejs/kit": "^2.0.0"
- }
- },
"node_modules/@sveltejs/adapter-node": {
"version": "5.2.12",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
@@ -1348,16 +1333,6 @@
"@sveltejs/kit": "^2.4.0"
}
},
- "node_modules/@sveltejs/adapter-static": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz",
- "integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@sveltejs/kit": "^2.0.0"
- }
- },
"node_modules/@sveltejs/kit": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.17.1.tgz",
diff --git a/package.json b/package.json
index 99baec2..ebeb1b1 100644
--- a/package.json
+++ b/package.json
@@ -17,9 +17,7 @@
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@ethercorps/sveltekit-redis-session": "^1.3.1",
- "@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/adapter-node": "^5.2.12",
- "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/postcss": "^4.0.4",
diff --git a/src/components/Toast.svelte b/src/components/Toast.svelte
new file mode 100644
index 0000000..447c553
--- /dev/null
+++ b/src/components/Toast.svelte
@@ -0,0 +1,33 @@
+
+
+{#if visible}
+
+{/if}
diff --git a/src/routes/dashboard/+page.server.ts b/src/routes/dashboard/+page.server.ts
index 2aa7a59..7a7eedf 100644
--- a/src/routes/dashboard/+page.server.ts
+++ b/src/routes/dashboard/+page.server.ts
@@ -1,7 +1,6 @@
-import { logout } from '$api/authService';
import { checkAuth } from '$api/authService';
import { getServers, restartService, startService, stopService } from '$api/serverService';
-import { redirect, type Actions, type RequestEvent } from '@sveltejs/kit';
+import { fail, redirect, type Actions, type RequestEvent } from '@sveltejs/kit';
export const load = async (event: RequestEvent) => {
const isAuth = await checkAuth(event);
@@ -10,17 +9,45 @@ export const load = async (event: RequestEvent) => {
return { servers };
};
+// Helper function to create a server action with validation and error handling
+const createServerAction = (
+ action: (event: RequestEvent, id: number) => Promise,
+ { success, failure }: { success: string; failure: string }
+) => {
+ return async (event: RequestEvent) => {
+ const formData = await event.request.formData();
+ const id = formData.get('id');
+
+ if (!id || typeof id !== 'string') {
+ return fail(400, { message: 'Invalid server ID provided.' });
+ }
+
+ const serverId = Number(id);
+ if (isNaN(serverId)) {
+ return fail(400, { message: 'Server ID must be a number.' });
+ }
+
+ try {
+ await action(event, serverId);
+ return { success: true, message: success };
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Unknown error';
+ return fail(500, { message: `${failure}: ${message}` });
+ }
+ };
+};
+
export const actions = {
- start: async (event: RequestEvent) => {
- const id = (await event.request.formData()).get('id') as string;
- await startService(event, +id);
- },
- restart: async (event: RequestEvent) => {
- const id = (await event.request.formData()).get('id') as string;
- await restartService(event, +id);
- },
- stop: async (event: RequestEvent) => {
- const id = (await event.request.formData()).get('id') as string;
- await stopService(event, +id);
- }
+ start: createServerAction(startService, {
+ success: 'Server started successfully.',
+ failure: 'Failed to start server'
+ }),
+ restart: createServerAction(restartService, {
+ success: 'Server restarted successfully.',
+ failure: 'Failed to restart server'
+ }),
+ stop: createServerAction(stopService, {
+ success: 'Server stopped successfully.',
+ failure: 'Failed to stop server'
+ })
} satisfies Actions;
diff --git a/src/routes/dashboard/+page.svelte b/src/routes/dashboard/+page.svelte
index d89e5d4..188709b 100644
--- a/src/routes/dashboard/+page.svelte
+++ b/src/routes/dashboard/+page.svelte
@@ -1,11 +1,16 @@
+{#if form?.message}
+
+{/if}
+
diff --git a/src/routes/dashboard/server/[id]/+page.server.ts b/src/routes/dashboard/server/[id]/+page.server.ts
index 7669d93..7e4afc4 100644
--- a/src/routes/dashboard/server/[id]/+page.server.ts
+++ b/src/routes/dashboard/server/[id]/+page.server.ts
@@ -2,15 +2,14 @@ import {
updateConfig,
getConfigFiles,
getServerById,
- getStateHistory,
getStateHistoryStats
} from '$api/serverService';
import type { Actions } from './$types';
import { checkAuth } from '$api/authService';
import { getTracks } from '$api/lookupService';
-import { redirect } from '@sveltejs/kit';
+import { fail, redirect } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
-import { configFile, type Config, type Session } from '$models/config';
+import { configFile, type Config } from '$models/config';
import { set } from 'lodash-es';
import { subDays, formatISO } from 'date-fns';
import { UTCDate } from '@date-fns/utc';
@@ -38,20 +37,16 @@ export const load = async (event: RequestEvent) => {
};
};
-type SessionField =
- | 'sessionDurationMinutes'
- | 'sessionType'
- | 'timeMultiplier'
- | 'dayOfWeekend'
- | 'hourOfDay';
-
export const actions = {
update: async (event: RequestEvent) => {
- const { id, restart, file, data } = await destructureFormData(event);
-
- const sessions: Array
> = [];
-
- await updateConfig(event, id, file, data, true, restart);
+ try {
+ const { id, restart, file, data } = await destructureFormData(event);
+ await updateConfig(event, id, file, data, true, restart);
+ return { success: true, message: `Configuration file '${file}' updated successfully.` };
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'An unknown error occurred';
+ return fail(500, { message: `Failed to update configuration: ${message}` });
+ }
}
} satisfies Actions;
@@ -59,36 +54,46 @@ async function destructureFormData(
event: RequestEvent
): Promise<{ id: string; restart: boolean; data: Config; file: configFile }> {
const formData = await event.request.formData();
- const id = formData.get('id') as string;
+
+ const id = formData.get('id');
+ if (!id || typeof id !== 'string') {
+ throw new Error('Server ID is missing or invalid.');
+ }
+
+ const file = formData.get('file');
+ if (!file || typeof file !== 'string') {
+ throw new Error('Config file name is missing or invalid.');
+ }
+
const restart = formData.get('restart');
- const file = formData.get('file') as configFile;
- const object: any = {};
+ const object: Record = {};
+
formData.forEach((value, key) => {
- switch (key) {
- case 'id':
- case 'restart':
- case 'file':
- return;
- default:
- set(object, key, parseFormField(value));
+ // Exclude metadata fields from the dynamic object
+ if (key === 'id' || key === 'restart' || key === 'file') {
+ return;
}
+ set(object, key, parseFormField(value));
});
+
return {
id,
- restart: restart == 'on' || restart == 'true',
- data: object,
- file
+ restart: restart === 'on' || restart === 'true',
+ data: object as unknown as Config,
+ file: file as configFile
};
}
+/**
+ * Parses a form field value. If the value can be cleanly converted to a number,
+ * it returns a number; otherwise, it returns the original string.
+ */
function parseFormField(value: FormDataEntryValue): string | number {
- return value !== '' && !Number.isNaN(+value) ? +value : (value as string);
-}
-
-function tryParse(str: string) {
- try {
- return JSON.parse(str);
- } catch {
- return str;
+ // Avoid converting empty strings to 0
+ if (value === '' || typeof value !== 'string') {
+ return value as string;
}
+ // Check if string is a valid number without being too aggressive
+ const num = Number(value);
+ return !Number.isNaN(num) && value.trim() !== '' ? num : value;
}
diff --git a/src/routes/dashboard/server/[id]/+page.svelte b/src/routes/dashboard/server/[id]/+page.svelte
index e143e19..721b326 100644
--- a/src/routes/dashboard/server/[id]/+page.svelte
+++ b/src/routes/dashboard/server/[id]/+page.svelte
@@ -5,10 +5,11 @@
import EditorEventRules from '$components/EditorEventRules.svelte';
import EditorSettings from '$components/EditorSettings.svelte';
import Statistics from '$components/Statistics.svelte';
+ import Toast from '$components/Toast.svelte';
import { getStatusColor, serviceStatusToString } from '$lib/types/serviceStatus.js';
import { serverTab } from '$models/config.js';
- let { data } = $props();
+ let { data, form } = $props();
const configs = data.configs;
const tracks = data.tracks;
const id = data.id;
@@ -17,6 +18,10 @@
let tab = $state(serverTab.statistics);
+{#if form?.message}
+
+{/if}
+
{server.name}
@@ -30,7 +35,7 @@
{server.name}
-
+
{serviceStatusToString(server.status)}