diff --git a/src/components/arr/ArrInstanceForm.svelte b/src/components/arr/ArrInstanceForm.svelte new file mode 100644 index 0000000..393c0d8 --- /dev/null +++ b/src/components/arr/ArrInstanceForm.svelte @@ -0,0 +1,357 @@ + + +
+
+

{title}

+

+ {description} +

+
+ +
+
{ + return async ({ result, update }) => { + if (result.type === 'failure' && result.data) { + // Show error toast + toastStore.add('error', (result.data as { error?: string }).error || errorMessage); + } else if (result.type === 'redirect') { + // Show success toast before redirect + toastStore.add('success', successMessage); + } + await update(); + }; + }} + > + +
+ + +
+ + +
+ + {#if mode === 'edit'} + + +

+ Type cannot be changed after creation +

+ + {:else} + + + {/if} +
+ + +
+ + +
+ + +
+ + + {#if mode === 'edit'} +

+ Re-enter API key to update or test connection +

+ {/if} +
+ + +
+ +
+ +
+

+ Optional. Press Enter to add a tag, Backspace to remove. +

+ + 0 ? JSON.stringify(tags) : ''} /> +
+ + +
+
+
+ + + {#if mode === 'edit'} + + Cancel + + {/if} +
+ + +
+ + + {#if connectionStatus === 'success'} +

+ Connection test passed! You can now save this instance. +

+ {/if} + {#if connectionStatus === 'error'} +

+ {connectionError} +

+ {/if} + {#if !canSubmit && connectionStatus !== 'success'} +

+ Please test the connection before saving +

+ {/if} +
+
+ + + {#if mode === 'edit'} +
+

Danger Zone

+

+ Once you delete this instance, there is no going back. Please be certain. +

+ + + + +
+ {/if} +
+
+ + +{#if mode === 'edit'} + { + showDeleteModal = false; + deleteFormElement?.requestSubmit(); + }} + on:cancel={() => (showDeleteModal = false)} + /> +{/if} diff --git a/src/components/modal/Modal.svelte b/src/components/modal/Modal.svelte new file mode 100644 index 0000000..96c1bf2 --- /dev/null +++ b/src/components/modal/Modal.svelte @@ -0,0 +1,93 @@ + + +{#if open} + + + + +{/if} diff --git a/src/components/toast/Toast.svelte b/src/components/toast/Toast.svelte index acacd41..d58c331 100644 --- a/src/components/toast/Toast.svelte +++ b/src/components/toast/Toast.svelte @@ -18,9 +18,12 @@ // Style mapping const styles = { - success: 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200', - error: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200', - warning: 'bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200', + success: + 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200', + error: + 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200', + warning: + 'bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200', info: 'bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-200' }; diff --git a/src/components/toast/ToastContainer.svelte b/src/components/toast/ToastContainer.svelte index 3a44ee7..fccd9be 100644 --- a/src/components/toast/ToastContainer.svelte +++ b/src/components/toast/ToastContainer.svelte @@ -3,7 +3,7 @@ import Toast from './Toast.svelte'; -
+
{#each $toastStore as toast (toast.id)}
diff --git a/src/db/migrations.ts b/src/db/migrations.ts index 610b5eb..e7c9f28 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -1,233 +1,212 @@ -import { db } from "./db.ts"; -import { logger } from "$logger"; +import { db } from './db.ts'; +import { logger } from '$logger'; export interface Migration { - version: number; - name: string; - up: string; - down?: string; + version: number; + name: string; + up: string; + down?: string; } /** * Migration runner for database schema management */ class MigrationRunner { - private migrationsTable = "migrations"; + private migrationsTable = 'migrations'; - /** - * Initialize the migrations table - */ - initialize(): void { - const sql = ` + /** + * Initialize the migrations table + */ + initialize(): void { + const sql = ` CREATE TABLE IF NOT EXISTS ${this.migrationsTable} ( version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `; - db.exec(sql); - } + db.exec(sql); + } - /** - * Get the current migration version - */ - getCurrentVersion(): number { - const result = db.queryFirst<{ version: number }>( - `SELECT MAX(version) as version FROM ${this.migrationsTable}`, - ); - return result?.version ?? 0; - } + /** + * Get the current migration version + */ + getCurrentVersion(): number { + const result = db.queryFirst<{ version: number }>( + `SELECT MAX(version) as version FROM ${this.migrationsTable}` + ); + return result?.version ?? 0; + } - /** - * Check if a migration has been applied - */ - isApplied(version: number): boolean { - const result = db.queryFirst<{ count: number }>( - `SELECT COUNT(*) as count FROM ${this.migrationsTable} WHERE version = ?`, - version, - ); - return (result?.count ?? 0) > 0; - } + /** + * Check if a migration has been applied + */ + isApplied(version: number): boolean { + const result = db.queryFirst<{ count: number }>( + `SELECT COUNT(*) as count FROM ${this.migrationsTable} WHERE version = ?`, + version + ); + return (result?.count ?? 0) > 0; + } - /** - * Apply a single migration - */ - private async applyMigration(migration: Migration): Promise { - try { - await db.transaction(async () => { - // Execute the migration - db.exec(migration.up); + /** + * Apply a single migration + */ + private async applyMigration(migration: Migration): Promise { + try { + await db.transaction(async () => { + // Execute the migration + db.exec(migration.up); - // Record the migration - db.execute( - `INSERT INTO ${this.migrationsTable} (version, name) VALUES (?, ?)`, - migration.version, - migration.name, - ); + // Record the migration + db.execute( + `INSERT INTO ${this.migrationsTable} (version, name) VALUES (?, ?)`, + migration.version, + migration.name + ); - await logger.info( - `✓ Applied migration ${migration.version}: ${migration.name}`, - { - source: "MigrationRunner", - }, - ); - }); - } catch (error) { - await logger.error( - `✗ Failed to apply migration ${migration.version}: ${migration.name}`, - { - source: "MigrationRunner", - meta: error, - }, - ); - throw error; - } - } + await logger.info(`✓ Applied migration ${migration.version}: ${migration.name}`, { + source: 'MigrationRunner' + }); + }); + } catch (error) { + await logger.error(`✗ Failed to apply migration ${migration.version}: ${migration.name}`, { + source: 'MigrationRunner', + meta: error + }); + throw error; + } + } - /** - * Rollback a single migration - */ - private async rollbackMigration(migration: Migration): Promise { - if (!migration.down) { - throw new Error( - `Migration ${migration.version} does not support rollback`, - ); - } + /** + * Rollback a single migration + */ + private async rollbackMigration(migration: Migration): Promise { + if (!migration.down) { + throw new Error(`Migration ${migration.version} does not support rollback`); + } - try { - await db.transaction(async () => { - // Execute the rollback - db.exec(migration.down!); + try { + await db.transaction(async () => { + // Execute the rollback + db.exec(migration.down!); - // Remove the migration record - db.execute( - `DELETE FROM ${this.migrationsTable} WHERE version = ?`, - migration.version, - ); + // Remove the migration record + db.execute(`DELETE FROM ${this.migrationsTable} WHERE version = ?`, migration.version); - await logger.info( - `✓ Rolled back migration ${migration.version}: ${migration.name}`, - { - source: "MigrationRunner", - }, - ); - }); - } catch (error) { - await logger.error( - `✗ Failed to rollback migration ${migration.version}: ${migration.name}`, - { - source: "MigrationRunner", - meta: error, - }, - ); - throw error; - } - } + await logger.info(`✓ Rolled back migration ${migration.version}: ${migration.name}`, { + source: 'MigrationRunner' + }); + }); + } catch (error) { + await logger.error(`✗ Failed to rollback migration ${migration.version}: ${migration.name}`, { + source: 'MigrationRunner', + meta: error + }); + throw error; + } + } - /** - * Run all pending migrations - */ - async up(migrations: Migration[]): Promise { - this.initialize(); + /** + * Run all pending migrations + */ + async up(migrations: Migration[]): Promise { + this.initialize(); - // Sort migrations by version - const sortedMigrations = [...migrations].sort((a, b) => - a.version - b.version - ); + // Sort migrations by version + const sortedMigrations = [...migrations].sort((a, b) => a.version - b.version); - let applied = 0; - for (const migration of sortedMigrations) { - if (this.isApplied(migration.version)) { - continue; - } + let applied = 0; + for (const migration of sortedMigrations) { + if (this.isApplied(migration.version)) { + continue; + } - await this.applyMigration(migration); - applied++; - } + await this.applyMigration(migration); + applied++; + } - if (applied === 0) { - await logger.info("✓ Database is up to date", { - source: "MigrationRunner", - }); - } - } + if (applied === 0) { + await logger.info('✓ Database is up to date', { + source: 'MigrationRunner' + }); + } + } - /** - * Rollback to a specific version - */ - async down(migrations: Migration[], targetVersion = 0): Promise { - this.initialize(); + /** + * Rollback to a specific version + */ + async down(migrations: Migration[], targetVersion = 0): Promise { + this.initialize(); - const currentVersion = this.getCurrentVersion(); - if (currentVersion <= targetVersion) { - await logger.info("✓ Already at target version or below", { - source: "MigrationRunner", - }); - return; - } + const currentVersion = this.getCurrentVersion(); + if (currentVersion <= targetVersion) { + await logger.info('✓ Already at target version or below', { + source: 'MigrationRunner' + }); + return; + } - // Sort migrations by version in descending order - const sortedMigrations = [...migrations] - .filter((m) => m.version > targetVersion && m.version <= currentVersion) - .sort((a, b) => b.version - a.version); + // Sort migrations by version in descending order + const sortedMigrations = [...migrations] + .filter((m) => m.version > targetVersion && m.version <= currentVersion) + .sort((a, b) => b.version - a.version); - let rolledBack = 0; - for (const migration of sortedMigrations) { - if (!this.isApplied(migration.version)) { - continue; - } + let rolledBack = 0; + for (const migration of sortedMigrations) { + if (!this.isApplied(migration.version)) { + continue; + } - await this.rollbackMigration(migration); - rolledBack++; - } + await this.rollbackMigration(migration); + rolledBack++; + } - await logger.info(`✓ Rolled back ${rolledBack} migration(s)`, { - source: "MigrationRunner", - }); - } + await logger.info(`✓ Rolled back ${rolledBack} migration(s)`, { + source: 'MigrationRunner' + }); + } - /** - * Get list of applied migrations - */ - getAppliedMigrations(): Array< - { version: number; name: string; applied_at: string } - > { - return db.query( - `SELECT version, name, applied_at FROM ${this.migrationsTable} ORDER BY version`, - ); - } + /** + * Get list of applied migrations + */ + getAppliedMigrations(): Array<{ version: number; name: string; applied_at: string }> { + return db.query( + `SELECT version, name, applied_at FROM ${this.migrationsTable} ORDER BY version` + ); + } - /** - * Get list of pending migrations - */ - getPendingMigrations(migrations: Migration[]): Migration[] { - const pending: Migration[] = []; - for (const migration of migrations) { - if (!this.isApplied(migration.version)) { - pending.push(migration); - } - } - return pending.sort((a, b) => a.version - b.version); - } + /** + * Get list of pending migrations + */ + getPendingMigrations(migrations: Migration[]): Migration[] { + const pending: Migration[] = []; + for (const migration of migrations) { + if (!this.isApplied(migration.version)) { + pending.push(migration); + } + } + return pending.sort((a, b) => a.version - b.version); + } - /** - * Reset the database (rollback all migrations) - */ - async reset(migrations: Migration[]): Promise { - await this.down(migrations, 0); - } + /** + * Reset the database (rollback all migrations) + */ + async reset(migrations: Migration[]): Promise { + await this.down(migrations, 0); + } - /** - * Fresh migration (reset and reapply all) - */ - async fresh(migrations: Migration[]): Promise { - await logger.warn("⚠ Resetting database...", { source: "MigrationRunner" }); - await this.reset(migrations); - await logger.info("↻ Reapplying all migrations...", { - source: "MigrationRunner", - }); - await this.up(migrations); - } + /** + * Fresh migration (reset and reapply all) + */ + async fresh(migrations: Migration[]): Promise { + await logger.warn('⚠ Resetting database...', { source: 'MigrationRunner' }); + await this.reset(migrations); + await logger.info('↻ Reapplying all migrations...', { + source: 'MigrationRunner' + }); + await this.up(migrations); + } } // Export singleton instance @@ -238,64 +217,57 @@ export const migrationRunner = new MigrationRunner(); * Loads all migration files from src/db/migrations/ directory */ export async function loadMigrations(): Promise { - const migrations: Migration[] = []; - const migrationsDir = new URL("./migrations/", import.meta.url).pathname; + const migrations: Migration[] = []; + const migrationsDir = new URL('./migrations/', import.meta.url).pathname; - try { - // Read all files in migrations directory - for await (const entry of Deno.readDir(migrationsDir)) { - // Skip template files (starting with _) - if (entry.name.startsWith("_")) { - continue; - } + try { + // Read all files in migrations directory + for await (const entry of Deno.readDir(migrationsDir)) { + // Skip template files (starting with _) + if (entry.name.startsWith('_')) { + continue; + } - // Only process TypeScript/JavaScript files - if ( - entry.isFile && - (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) - ) { - try { - // Dynamically import the migration file - const migrationModule = await import(/* @vite-ignore */ `./migrations/${entry.name}`); + // Only process TypeScript/JavaScript files + if (entry.isFile && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) { + try { + // Dynamically import the migration file + const migrationModule = await import(/* @vite-ignore */ `./migrations/${entry.name}`); - // Get the migration object (could be default export or named export) - const migration = migrationModule.default || - migrationModule.migration; + // Get the migration object (could be default export or named export) + const migration = migrationModule.default || migrationModule.migration; - if (migration && typeof migration.version === "number") { - migrations.push(migration); - } else { - await logger.warn( - `Migration file ${entry.name} does not export a valid migration`, - { - source: "MigrationRunner", - }, - ); - } - } catch (error) { - await logger.error(`Failed to load migration file ${entry.name}`, { - source: "MigrationRunner", - meta: error, - }); - } - } - } - } catch (_error) { - // If directory doesn't exist or can't be read, return empty array - await logger.info("No migrations directory found or empty", { - source: "MigrationRunner", - }); - return []; - } + if (migration && typeof migration.version === 'number') { + migrations.push(migration); + } else { + await logger.warn(`Migration file ${entry.name} does not export a valid migration`, { + source: 'MigrationRunner' + }); + } + } catch (error) { + await logger.error(`Failed to load migration file ${entry.name}`, { + source: 'MigrationRunner', + meta: error + }); + } + } + } + } catch (_error) { + // If directory doesn't exist or can't be read, return empty array + await logger.info('No migrations directory found or empty', { + source: 'MigrationRunner' + }); + return []; + } - // Sort by version number - return migrations.sort((a, b) => a.version - b.version); + // Sort by version number + return migrations.sort((a, b) => a.version - b.version); } /** * Run migrations */ export async function runMigrations(migrations?: Migration[]): Promise { - const migrationsToRun = migrations ?? await loadMigrations(); - await migrationRunner.up(migrationsToRun); + const migrationsToRun = migrations ?? (await loadMigrations()); + await migrationRunner.up(migrationsToRun); } diff --git a/src/db/queries/arrInstances.ts b/src/db/queries/arrInstances.ts index b55e041..9bc320e 100644 --- a/src/db/queries/arrInstances.ts +++ b/src/db/queries/arrInstances.ts @@ -78,10 +78,7 @@ export const arrInstancesQueries = { * Get arr instances by type */ getByType(type: string): ArrInstance[] { - return db.query( - 'SELECT * FROM arr_instances WHERE type = ? ORDER BY name', - type - ); + return db.query('SELECT * FROM arr_instances WHERE type = ? ORDER BY name', type); }, /** diff --git a/src/routes/arr/[type]/+page.server.ts b/src/routes/arr/[type]/+page.server.ts index 9957cfb..27b024d 100644 --- a/src/routes/arr/[type]/+page.server.ts +++ b/src/routes/arr/[type]/+page.server.ts @@ -1,5 +1,6 @@ -import { error } from '@sveltejs/kit'; +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']; @@ -12,7 +13,17 @@ export const load: ServerLoad = ({ params }) => { 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 + type, + instances }; }; diff --git a/src/routes/arr/[type]/+page.svelte b/src/routes/arr/[type]/+page.svelte index 8da5051..7d7577f 100644 --- a/src/routes/arr/[type]/+page.svelte +++ b/src/routes/arr/[type]/+page.svelte @@ -1,32 +1,39 @@ -
-
-
-

{displayName}

-

- Manage your {displayName} instances -

+
+
+ +
+
+ +
+ + +

+ No {displayName} instances yet +

+ + +

+ Get started by adding your first {displayName} instance to begin managing your media library. +

+ + - - Add Instance + + Add {displayName} Instance
- - -
-

No instances configured yet. Click "Add Instance" to get started.

-
diff --git a/src/routes/arr/[type]/[id]/+page.server.ts b/src/routes/arr/[type]/[id]/+page.server.ts new file mode 100644 index 0000000..fabc117 --- /dev/null +++ b/src/routes/arr/[type]/[id]/+page.server.ts @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000..7f2e9d8 --- /dev/null +++ b/src/routes/arr/[type]/[id]/+page.svelte @@ -0,0 +1,53 @@ + + +
+ +
+
+ +
+
+ + +
+

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 new file mode 100644 index 0000000..5b7380b --- /dev/null +++ b/src/routes/arr/[type]/[id]/edit/+page.server.ts @@ -0,0 +1,103 @@ +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'; + +// 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/[type]/[id]/edit/+page.svelte b/src/routes/arr/[type]/[id]/edit/+page.svelte new file mode 100644 index 0000000..5d5b87a --- /dev/null +++ b/src/routes/arr/[type]/[id]/edit/+page.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/arr/new/+page.server.ts b/src/routes/arr/new/+page.server.ts index 32d2d9d..3319f2f 100644 --- a/src/routes/arr/new/+page.server.ts +++ b/src/routes/arr/new/+page.server.ts @@ -83,7 +83,6 @@ export const actions = { source: 'arr/new', meta: { id, name, type, url } }); - } catch (error) { await logger.error('Failed to create arr instance', { source: 'arr/new', diff --git a/src/routes/arr/new/+page.svelte b/src/routes/arr/new/+page.svelte index 9041115..dc4b268 100644 --- a/src/routes/arr/new/+page.svelte +++ b/src/routes/arr/new/+page.svelte @@ -1,229 +1,12 @@ -
-
-

Add Arr Instance

-

- Configure a new Radarr, Sonarr, Lidarr, or Chaptarr instance -

-
- -
-
{ - return async ({ result, update }) => { - if (result.type === 'failure' && result.data) { - // Show error toast - toastStore.add('error', (result.data as { error?: string }).error || 'Failed to save instance'); - } else if (result.type === 'redirect') { - // Show success toast before redirect - toastStore.add('success', 'Instance created successfully!'); - } - await update(); - }; - }} - > - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- -
- -
-

- Optional. Press Enter to add a tag, Backspace to remove. -

- - 0 ? JSON.stringify(tags) : ''} /> -
- - -
-
- - - -
- - - {#if connectionStatus === 'success'} -

- Connection test passed! You can now save this instance. -

- {/if} - {#if connectionStatus === 'error'} -

- {connectionError} -

- {/if} - {#if !canSubmit && connectionStatus !== 'success'} -

- Please test the connection before saving -

- {/if} -
-
-
-
+ diff --git a/src/routes/settings/about/+page.svelte b/src/routes/settings/about/+page.svelte index b8abc06..c9ee3eb 100644 --- a/src/routes/settings/about/+page.svelte +++ b/src/routes/settings/about/+page.svelte @@ -121,184 +121,194 @@
{:else}
- - - - - Version - - -
- + + + + Version + + +
+ + v{data.version} + + +
+ + + +
+ + {#each sections as section (section.title)} + + {#each section.rows as row (row.label)} + + {/each} + + {/each} + + + {#if data.migration.applied.length > 0} + + + - v{data.version} -
- -
- - - -
- - {#each sections as section (section.title)} - - {#each section.rows as row (row.label)} - - {/each} - - {/each} - - - {#if data.migration.applied.length > 0} - - - - Migrations - - -
- {#each data.migration.applied as migration (migration.version)} -
-
- - v{migration.version} - - - {migration.name} + Migrations + + +
+ {#each data.migration.applied as migration (migration.version)} +
+
+ + v{migration.version} + + + {migration.name} + + {#if migration.latest} + + Latest + + {/if} +
+ + {new Date(migration.applied_at).toLocaleDateString()} - {#if migration.latest} - - Latest - - {/if}
- - {new Date(migration.applied_at).toLocaleDateString()} - -
- {/each} -
- - - - {/if} + {/each} +
+ + + + {/if} - - {#if data.releases.length > 0} - - - -
- {#each data.releases as release, index (release.tag_name)} -
-
- - {release.tag_name} - - {#if index === 0} - + {#if data.releases.length > 0} + + + +
+ {#each data.releases as release, index (release.tag_name)} +
+
+ - Latest - - {/if} - {#if release.prerelease} - - Pre-release - - {/if} + {release.tag_name} + + {#if index === 0} + + Latest + + {/if} + {#if release.prerelease} + + Pre-release + + {/if} +
+ + {new Date(release.published_at).toLocaleDateString()} +
- - {new Date(release.published_at).toLocaleDateString()} - -
- {/each} -
- - - - {/if} + {/each} +
+ + + + {/if} - -
- -
- -

Dev Team

-
+ +
+ +
+ +

Dev Team

+
- -
-
- - - - - - - - - - {#each devTeam as member (member.name)} - - - - - - {/each} - -
- Name - - Remark - - Tags -
- {member.name} - - {#if member.remark} - {member.remark} - {:else} - Remark pending - someone should probably ask them - {/if} - -
- {#each member.tags as tag} - - {tag} - - {/each} -
-
+ +
+
+ + + + + + + + + + {#each devTeam as member (member.name)} + + + + + + {/each} + +
+ Name + + Remark + + Tags +
+ {member.name} + + {#if member.remark} + {member.remark} + {:else} + Remark pending - someone should probably ask them + {/if} + +
+ {#each member.tags as tag} + + {tag} + + {/each} +
+
+
-
- -
-

- This project is dedicated to Faiza, for helping me find my heart. -

-
+ +
+

+ This project is dedicated to Faiza, for helping me find my heart. +

+
{/if}
diff --git a/src/utils/arr/README.md b/src/utils/arr/README.md index e39f782..9c9851a 100644 --- a/src/utils/arr/README.md +++ b/src/utils/arr/README.md @@ -1,6 +1,6 @@ # Arr HTTP Client Utilities -Object-oriented HTTP client architecture for communicating with *arr +Object-oriented HTTP client architecture for communicating with \*arr applications (Radarr, Sonarr, Lidarr, Chaptarr). ## Architecture @@ -64,7 +64,7 @@ new BaseHttpClient(baseUrl: string, options?: HttpClientOptions) ### BaseArrClient (`src/utils/arr/base.ts`) -Base client for all *arr applications. Extends `BaseHttpClient`. +Base client for all \*arr applications. Extends `BaseHttpClient`. **Features:** @@ -86,9 +86,9 @@ new BaseArrClient(url: string, apiKey: string) ### Future Usage (when specific methods are implemented) ```typescript -import { createArrClient } from "$utils/arr/factory.ts"; +import { createArrClient } from '$utils/arr/factory.ts'; -const radarr = createArrClient("radarr", "http://localhost:7878", "api-key"); +const radarr = createArrClient('radarr', 'http://localhost:7878', 'api-key'); // Get movies const movies = await radarr.getMovies(); @@ -98,8 +98,8 @@ const profiles = await radarr.getQualityProfiles(); // Add movie await radarr.addMovie({ - title: "Inception", - tmdbId: 27205, - qualityProfileId: 1, + title: 'Inception', + tmdbId: 27205, + qualityProfileId: 1 }); ``` diff --git a/src/utils/arr/factory.ts b/src/utils/arr/factory.ts index 8348a0d..827538d 100644 --- a/src/utils/arr/factory.ts +++ b/src/utils/arr/factory.ts @@ -12,11 +12,7 @@ import { ChaptarrClient } from './clients/chaptarr.ts'; * @param apiKey - API key for authentication * @returns Arr client instance */ -export function createArrClient( - type: ArrType, - url: string, - apiKey: string -): BaseArrClient { +export function createArrClient(type: ArrType, url: string, apiKey: string): BaseArrClient { switch (type) { case 'radarr': return new RadarrClient(url, apiKey); diff --git a/src/utils/config/config.ts b/src/utils/config/config.ts index 8ddccf0..ab98660 100644 --- a/src/utils/config/config.ts +++ b/src/utils/config/config.ts @@ -52,7 +52,7 @@ class Config { }, get database(): string { return `${config.basePath}/data/profilarr.db`; - }, + } }; } diff --git a/src/utils/http/client.ts b/src/utils/http/client.ts index c5895ce..56ade9e 100644 --- a/src/utils/http/client.ts +++ b/src/utils/http/client.ts @@ -113,10 +113,7 @@ export class BaseHttpClient { } // Wrap other errors - throw new HttpError( - error instanceof Error ? error.message : 'Unknown error', - 0 - ); + throw new HttpError(error instanceof Error ? error.message : 'Unknown error', 0); } } catch (error) { // If it's not an HttpError or not retryable, throw immediately