diff --git a/src/lib/client/ui/navigation/pageNav/pageNav.svelte b/src/lib/client/ui/navigation/pageNav/pageNav.svelte index c61ea14..4e3347b 100644 --- a/src/lib/client/ui/navigation/pageNav/pageNav.svelte +++ b/src/lib/client/ui/navigation/pageNav/pageNav.svelte @@ -10,9 +10,7 @@
- - - + diff --git a/src/lib/client/ui/table/Table.svelte b/src/lib/client/ui/table/Table.svelte index 5e2cf72..62a7a1e 100644 --- a/src/lib/client/ui/table/Table.svelte +++ b/src/lib/client/ui/table/Table.svelte @@ -108,7 +108,7 @@ $: sortedData = sortData(data); -
+
{ + const instances = arrInstancesQueries.getAll(); + + return { + instances + }; +}; + +export const actions = { + delete: async ({ request }) => { + const formData = await request.formData(); + const id = parseInt(formData.get('id')?.toString() || '0', 10); + + if (!id) { + await logger.warn('Attempted to delete arr instance without ID', { + source: 'arr' + }); + + return fail(400, { + error: 'Instance ID is required' + }); + } + + try { + const deleted = arrInstancesQueries.delete(id); + + if (!deleted) { + return fail(404, { + error: 'Instance not found' + }); + } + + await logger.info(`Deleted arr instance: ${id}`, { + source: 'arr', + meta: { id } + }); + + redirect(303, '/arr'); + } catch (error) { + // Re-throw redirect errors (they're not actual errors) + if (error && typeof error === 'object' && 'status' in error && 'location' in error) { + throw error; + } + + await logger.error('Failed to delete arr instance', { + source: 'arr', + meta: { error: error instanceof Error ? error.message : String(error) } + }); + + return fail(500, { + error: error instanceof Error ? error.message : 'Failed to delete instance' + }); + } + } +} satisfies Actions; diff --git a/src/routes/arr/+page.svelte b/src/routes/arr/+page.svelte new file mode 100644 index 0000000..d2060d3 --- /dev/null +++ b/src/routes/arr/+page.svelte @@ -0,0 +1,188 @@ + + + + Arr Instances - Profilarr + + +{#if data.instances.length === 0} + +{:else} +
+ +
+
+

Arr Instances

+

+ Manage your Radarr, Sonarr, Lidarr, and Chaptarr instances +

+
+ + + Add Instance + +
+ + +
+ + {#if column.key === 'name'} +
+ {row.name} +
+ {:else if column.key === 'type'} + + {formatType(row.type)} + + {:else if column.key === 'url'} + + {row.url} + + {:else if column.key === 'enabled'} +
+ {#if row.enabled} + + + + {:else} + + + + {/if} +
+ {/if} +
+ + +
+ + e.stopPropagation()} + class="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-lg border border-neutral-300 bg-white text-neutral-600 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-700" + title="Edit instance" + > + + + + + +
+
+
+
+{/if} + + + { + showDeleteModal = false; + if (selectedInstance) { + deleteFormElement?.requestSubmit(); + } + }} + on:cancel={() => { + showDeleteModal = false; + selectedInstance = null; + }} +/> + + + diff --git a/src/routes/arr/[id]/+layout.svelte b/src/routes/arr/[id]/+layout.svelte new file mode 100644 index 0000000..210b255 --- /dev/null +++ b/src/routes/arr/[id]/+layout.svelte @@ -0,0 +1,39 @@ + + +
+ + +
diff --git a/src/routes/arr/[id]/+page.server.ts b/src/routes/arr/[id]/+page.server.ts new file mode 100644 index 0000000..b58e859 --- /dev/null +++ b/src/routes/arr/[id]/+page.server.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; +import type { ServerLoad } from '@sveltejs/kit'; + +export const load: ServerLoad = ({ params }) => { + // Redirect to the library tab by default + redirect(302, `/arr/${params.id}/library`); +}; diff --git a/src/routes/arr/[id]/+page.svelte b/src/routes/arr/[id]/+page.svelte new file mode 100644 index 0000000..35c4f6f --- /dev/null +++ b/src/routes/arr/[id]/+page.svelte @@ -0,0 +1,4 @@ + diff --git a/src/routes/arr/[id]/edit/+layout@.svelte b/src/routes/arr/[id]/edit/+layout@.svelte new file mode 100644 index 0000000..af1293c --- /dev/null +++ b/src/routes/arr/[id]/edit/+layout@.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/arr/[id]/edit/+page.server.ts b/src/routes/arr/[id]/edit/+page.server.ts new file mode 100644 index 0000000..de54385 --- /dev/null +++ b/src/routes/arr/[id]/edit/+page.server.ts @@ -0,0 +1,149 @@ +import { error, redirect, fail } from '@sveltejs/kit'; +import type { ServerLoad, Actions } from '@sveltejs/kit'; +import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; +import { logger } from '$logger/logger.ts'; + +export const load: ServerLoad = ({ params }) => { + const id = parseInt(params.id || '', 10); + + // Validate ID + if (isNaN(id)) { + error(404, `Invalid instance ID: ${params.id}`); + } + + // Fetch the specific instance + const instance = arrInstancesQueries.getById(id); + + if (!instance) { + error(404, `Instance not found: ${id}`); + } + + return { + instance + }; +}; + +export const actions: Actions = { + default: async ({ params, request }) => { + const id = parseInt(params.id || '', 10); + + // Validate ID + if (isNaN(id)) { + return fail(400, { error: 'Invalid instance ID' }); + } + + // Fetch the instance to verify it exists + const instance = arrInstancesQueries.getById(id); + + if (!instance) { + return fail(404, { error: 'Instance not found' }); + } + + const formData = await request.formData(); + const name = formData.get('name')?.toString().trim(); + const url = formData.get('url')?.toString().trim(); + const apiKey = formData.get('api_key')?.toString().trim(); + const tagsJson = formData.get('tags')?.toString() || ''; + const enabled = formData.get('enabled')?.toString() === '1'; + + // Validate required fields + if (!name) { + return fail(400, { error: 'Name is required' }); + } + + if (!url) { + return fail(400, { error: 'URL is required' }); + } + + if (!apiKey) { + return fail(400, { error: 'API Key is required' }); + } + + // Check for duplicate name + if (arrInstancesQueries.nameExists(name, id)) { + return fail(400, { error: 'An instance with this name already exists' }); + } + + // Parse tags + let tags: string[] = []; + if (tagsJson) { + try { + tags = JSON.parse(tagsJson); + } catch { + // Ignore parse errors, use empty array + } + } + + try { + arrInstancesQueries.update(id, { + name, + url, + apiKey, + tags, + enabled + }); + + await logger.info(`Updated arr instance: ${name}`, { + source: 'arr/[id]/edit', + meta: { id, name, type: instance.type, url } + }); + + redirect(303, `/arr/${id}`); + } catch (err) { + // Re-throw redirect errors + if (err && typeof err === 'object' && 'status' in err && 'location' in err) { + throw err; + } + + await logger.error('Failed to update arr instance', { + source: 'arr/[id]/edit', + meta: { error: err instanceof Error ? err.message : String(err) } + }); + + return fail(500, { error: 'Failed to update instance' }); + } + }, + + delete: async ({ params }) => { + const id = parseInt(params.id || '', 10); + + // Validate ID + if (isNaN(id)) { + await logger.warn('Delete failed: Invalid instance ID', { + source: 'arr/[id]/edit', + meta: { id: params.id } + }); + return fail(400, { error: 'Invalid instance ID' }); + } + + // Fetch the instance to verify it exists + const instance = arrInstancesQueries.getById(id); + + if (!instance) { + await logger.warn('Delete failed: Instance not found', { + source: 'arr/[id]/edit', + meta: { id } + }); + return fail(404, { error: 'Instance not found' }); + } + + // Delete the instance + const deleted = arrInstancesQueries.delete(id); + + if (!deleted) { + await logger.error('Failed to delete instance', { + source: 'arr/[id]/edit', + meta: { id, name: instance.name, type: instance.type } + }); + return fail(500, { error: 'Failed to delete instance' }); + } + + await logger.info(`Deleted ${instance.type} instance: ${instance.name}`, { + source: 'arr/[id]/edit', + meta: { id, name: instance.name, type: instance.type, url: instance.url } + }); + + // Redirect to the arr landing page + redirect(303, '/arr'); + } +}; diff --git a/src/routes/arr/[type]/[id]/edit/+page.svelte b/src/routes/arr/[id]/edit/+page.svelte similarity index 74% rename from src/routes/arr/[type]/[id]/edit/+page.svelte rename to src/routes/arr/[id]/edit/+page.svelte index a8052b8..230fb97 100644 --- a/src/routes/arr/[type]/[id]/edit/+page.svelte +++ b/src/routes/arr/[id]/edit/+page.svelte @@ -1,5 +1,5 @@ + + + {data.instance.name} - Library - Profilarr + + +
+
+

Library

+

+ View and manage the library for this {data.instance.type} instance. +

+
+ Library content coming soon... +
+
+
diff --git a/src/routes/arr/[id]/logs/+page.server.ts b/src/routes/arr/[id]/logs/+page.server.ts new file mode 100644 index 0000000..8c8ae41 --- /dev/null +++ b/src/routes/arr/[id]/logs/+page.server.ts @@ -0,0 +1,21 @@ +import { error } from '@sveltejs/kit'; +import type { ServerLoad } from '@sveltejs/kit'; +import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; + +export const load: ServerLoad = ({ params }) => { + const id = parseInt(params.id || '', 10); + + if (isNaN(id)) { + error(404, `Invalid instance ID: ${params.id}`); + } + + const instance = arrInstancesQueries.getById(id); + + if (!instance) { + error(404, `Instance not found: ${id}`); + } + + return { + instance + }; +}; diff --git a/src/routes/arr/[id]/logs/+page.svelte b/src/routes/arr/[id]/logs/+page.svelte new file mode 100644 index 0000000..5fe599c --- /dev/null +++ b/src/routes/arr/[id]/logs/+page.svelte @@ -0,0 +1,21 @@ + + + + {data.instance.name} - Logs - Profilarr + + +
+
+

Logs

+

+ View sync and activity logs for this {data.instance.type} instance. +

+
+ Logs viewer coming soon... +
+
+
diff --git a/src/routes/arr/[id]/search-priority/+page.server.ts b/src/routes/arr/[id]/search-priority/+page.server.ts new file mode 100644 index 0000000..8c8ae41 --- /dev/null +++ b/src/routes/arr/[id]/search-priority/+page.server.ts @@ -0,0 +1,21 @@ +import { error } from '@sveltejs/kit'; +import type { ServerLoad } from '@sveltejs/kit'; +import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; + +export const load: ServerLoad = ({ params }) => { + const id = parseInt(params.id || '', 10); + + if (isNaN(id)) { + error(404, `Invalid instance ID: ${params.id}`); + } + + const instance = arrInstancesQueries.getById(id); + + if (!instance) { + error(404, `Instance not found: ${id}`); + } + + return { + instance + }; +}; diff --git a/src/routes/arr/[id]/search-priority/+page.svelte b/src/routes/arr/[id]/search-priority/+page.svelte new file mode 100644 index 0000000..78b0ee4 --- /dev/null +++ b/src/routes/arr/[id]/search-priority/+page.svelte @@ -0,0 +1,21 @@ + + + + {data.instance.name} - Search Priority - Profilarr + + +
+
+

Search Priority

+

+ Configure search priority settings for this {data.instance.type} instance. +

+
+ Search priority configuration coming soon... +
+
+
diff --git a/src/routes/arr/[type]/+page.server.ts b/src/routes/arr/[type]/+page.server.ts deleted file mode 100644 index 27b024d..0000000 --- a/src/routes/arr/[type]/+page.server.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { error, redirect } from '@sveltejs/kit'; -import type { ServerLoad } from '@sveltejs/kit'; -import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; - -// Valid arr types -const VALID_TYPES = ['radarr', 'sonarr', 'lidarr', 'chaptarr']; - -export const load: ServerLoad = ({ params }) => { - const type = params.type; - - // Validate type - if (!type || !VALID_TYPES.includes(type)) { - error(404, `Invalid arr type: ${type}`); - } - - // Fetch instances for this type - const instances = arrInstancesQueries.getByType(type); - - // If instances exist, redirect to the first one - if (instances.length > 0) { - redirect(302, `/arr/${type}/${instances[0].id}`); - } - - // If no instances, continue to show the page - return { - type, - instances - }; -}; diff --git a/src/routes/arr/[type]/+page.svelte b/src/routes/arr/[type]/+page.svelte deleted file mode 100644 index 510f139..0000000 --- a/src/routes/arr/[type]/+page.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/src/routes/arr/[type]/[id]/+page.server.ts b/src/routes/arr/[type]/[id]/+page.server.ts deleted file mode 100644 index fabc117..0000000 --- a/src/routes/arr/[type]/[id]/+page.server.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { error } from '@sveltejs/kit'; -import type { ServerLoad } from '@sveltejs/kit'; -import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; - -// Valid arr types -const VALID_TYPES = ['radarr', 'sonarr', 'lidarr', 'chaptarr']; - -export const load: ServerLoad = ({ params }) => { - const type = params.type; - const id = parseInt(params.id || '', 10); - - // Validate type - if (!type || !VALID_TYPES.includes(type)) { - error(404, `Invalid arr type: ${type}`); - } - - // Validate ID - if (isNaN(id)) { - error(404, `Invalid instance ID: ${params.id}`); - } - - // Fetch the specific instance - const instance = arrInstancesQueries.getById(id); - - if (!instance) { - error(404, `Instance not found: ${id}`); - } - - // Verify instance type matches route type - if (instance.type !== type) { - error(404, `Instance ${id} is not a ${type} instance`); - } - - // Fetch all instances of this type for the tabs - const allInstances = arrInstancesQueries.getByType(type); - - return { - type, - instance, - allInstances - }; -}; diff --git a/src/routes/arr/[type]/[id]/+page.svelte b/src/routes/arr/[type]/[id]/+page.svelte deleted file mode 100644 index 048793b..0000000 --- a/src/routes/arr/[type]/[id]/+page.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - -
- - - - -
-

Instance content for: {data.instance.name} (ID: {data.instance.id})

-

URL: {data.instance.url}

-

Type: {data.instance.type}

-
-
- - - - - diff --git a/src/routes/arr/[type]/[id]/edit/+page.server.ts b/src/routes/arr/[type]/[id]/edit/+page.server.ts deleted file mode 100644 index d6945d6..0000000 --- a/src/routes/arr/[type]/[id]/edit/+page.server.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { error, redirect, fail } from '@sveltejs/kit'; -import type { ServerLoad, Actions } from '@sveltejs/kit'; -import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; -import { logger } from '$logger/logger.ts'; - -// Valid arr types -const VALID_TYPES = ['radarr', 'sonarr', 'lidarr', 'chaptarr']; - -export const load: ServerLoad = ({ params }) => { - const type = params.type; - const id = parseInt(params.id || '', 10); - - // Validate type - if (!type || !VALID_TYPES.includes(type)) { - error(404, `Invalid arr type: ${type}`); - } - - // Validate ID - if (isNaN(id)) { - error(404, `Invalid instance ID: ${params.id}`); - } - - // Fetch the specific instance - const instance = arrInstancesQueries.getById(id); - - if (!instance) { - error(404, `Instance not found: ${id}`); - } - - // Verify instance type matches route type - if (instance.type !== type) { - error(404, `Instance ${id} is not a ${type} instance`); - } - - return { - type, - instance - }; -}; - -export const actions: Actions = { - delete: async ({ params }) => { - const type = params.type; - const id = parseInt(params.id || '', 10); - - // Validate type - if (!type || !VALID_TYPES.includes(type)) { - await logger.warn('Delete failed: Invalid arr type', { - source: 'arr/[type]/[id]/edit', - meta: { type } - }); - return fail(400, { error: 'Invalid arr type' }); - } - - // Validate ID - if (isNaN(id)) { - await logger.warn('Delete failed: Invalid instance ID', { - source: 'arr/[type]/[id]/edit', - meta: { id: params.id } - }); - return fail(400, { error: 'Invalid instance ID' }); - } - - // Fetch the instance to verify it exists - const instance = arrInstancesQueries.getById(id); - - if (!instance) { - await logger.warn('Delete failed: Instance not found', { - source: 'arr/[type]/[id]/edit', - meta: { id, type } - }); - return fail(404, { error: 'Instance not found' }); - } - - // Verify instance type matches route type - if (instance.type !== type) { - await logger.warn('Delete failed: Instance type mismatch', { - source: 'arr/[type]/[id]/edit', - meta: { id, expectedType: type, actualType: instance.type } - }); - return fail(400, { error: 'Instance type mismatch' }); - } - - // Delete the instance - const deleted = arrInstancesQueries.delete(id); - - if (!deleted) { - await logger.error('Failed to delete instance', { - source: 'arr/[type]/[id]/edit', - meta: { id, name: instance.name, type: instance.type } - }); - return fail(500, { error: 'Failed to delete instance' }); - } - - await logger.info(`Deleted ${type} instance: ${instance.name}`, { - source: 'arr/[type]/[id]/edit', - meta: { id, name: instance.name, type: instance.type, url: instance.url } - }); - - // Redirect to the arr type page - redirect(303, `/arr/${type}`); - } -}; diff --git a/src/routes/arr/components/InstanceForm.svelte b/src/routes/arr/components/InstanceForm.svelte index e5fbea8..00c6983 100644 --- a/src/routes/arr/components/InstanceForm.svelte +++ b/src/routes/arr/components/InstanceForm.svelte @@ -32,6 +32,7 @@ let url = (form as any)?.values?.url ?? (mode === 'edit' ? instance?.url : '') ?? ''; let apiKey = ''; // Never pre-populate API key for security let tags: string[] = mode === 'edit' && instance ? parseTags(instance.tags) : []; + let enabled: boolean = mode === 'edit' ? Boolean(instance?.enabled) : true; // Connection test state type ConnectionStatus = 'idle' | 'testing' | 'success' | 'error'; @@ -185,6 +186,38 @@ {/if}
+ + +
+
+ +

+ Disable to exclude this instance from sync operations +

+
+ + +
@@ -312,8 +345,7 @@
{#if mode === 'edit'} Cancel diff --git a/src/routes/arr/new/+page.server.ts b/src/routes/arr/new/+page.server.ts index c850ca9..48b8c1a 100644 --- a/src/routes/arr/new/+page.server.ts +++ b/src/routes/arr/new/+page.server.ts @@ -14,6 +14,7 @@ export const actions = { const url = formData.get('url')?.toString().trim(); const apiKey = formData.get('api_key')?.toString().trim(); const tagsJson = formData.get('tags')?.toString().trim(); + const enabled = formData.get('enabled')?.toString() === '1'; // Validation if (!name || !type || !url || !apiKey) { @@ -69,14 +70,16 @@ export const actions = { } } + let id: number; try { // Create the instance - const id = arrInstancesQueries.create({ + id = arrInstancesQueries.create({ name, type, url, apiKey, - tags + tags, + enabled }); await logger.info(`Created new ${type} instance: ${name}`, { @@ -95,7 +98,7 @@ export const actions = { }); } - // Redirect to the type page (outside try-catch since redirect throws) - redirect(303, `/arr/${type}`); + // Redirect to the new instance page (outside try-catch since redirect throws) + redirect(303, `/arr/${id}`); } } satisfies Actions;