diff --git a/src/lib/server/utils/arr/base.ts b/src/lib/server/utils/arr/base.ts index fa0bdae..24bb5bb 100644 --- a/src/lib/server/utils/arr/base.ts +++ b/src/lib/server/utils/arr/base.ts @@ -9,7 +9,10 @@ import type { ArrCustomFormat, ArrQualityProfilePayload, RadarrQualityProfile, - ArrCommand + ArrCommand, + ArrLogResponse, + ArrLogFile, + ArrLogParams } from './types.ts'; import { logger } from '$logger/logger.ts'; @@ -306,4 +309,45 @@ export class BaseArrClient extends BaseHttpClient { await new Promise((resolve) => setTimeout(resolve, pollInterval)); } } + + // ========================================================================= + // Log Methods + // ========================================================================= + + /** + * Get paginated logs from the arr instance + * @param params - Query parameters for filtering and pagination + */ + getLogs(params: ArrLogParams = {}): Promise { + const queryParams = new URLSearchParams(); + + if (params.page !== undefined) queryParams.set('page', String(params.page)); + if (params.pageSize !== undefined) queryParams.set('pageSize', String(params.pageSize)); + if (params.sortKey) queryParams.set('sortKey', params.sortKey); + if (params.sortDirection) queryParams.set('sortDirection', params.sortDirection); + if (params.level) queryParams.set('level', params.level); + + const queryString = queryParams.toString(); + const url = `/api/${this.apiVersion}/log${queryString ? `?${queryString}` : ''}`; + + return this.get(url); + } + + /** + * Get list of available log files + */ + getLogFiles(): Promise { + return this.get(`/api/${this.apiVersion}/log/file`); + } + + /** + * Get raw content of a specific log file + * @param filename - The log filename (e.g., "radarr.txt", "sonarr.debug.0.txt") + * @returns Raw log file content as text + */ + getLogFileContent(filename: string): Promise { + return this.get(`/api/${this.apiVersion}/log/file/${filename}`, { + responseType: 'text' + }); + } } diff --git a/src/lib/server/utils/arr/types.ts b/src/lib/server/utils/arr/types.ts index e76b103..728069c 100644 --- a/src/lib/server/utils/arr/types.ts +++ b/src/lib/server/utils/arr/types.ts @@ -648,3 +648,66 @@ export interface ArrSystemStatus { packageUpdateMechanism: 'builtIn' | string; packageUpdateMechanismMessage: string; } + +// ============================================================================= +// Log Types +// ============================================================================= + +/** + * Log level for filtering + * API accepts: Trace, Debug, Info, Warn, Error, Fatal + */ +export type ArrLogLevel = 'Trace' | 'Debug' | 'Info' | 'Warn' | 'Error' | 'Fatal'; + +/** + * Sort direction for log queries + */ +export type ArrSortDirection = 'ascending' | 'descending' | 'default'; + +/** + * Log entry from /api/v3/log + */ +export interface ArrLogEntry { + id: number; + time: string; // ISO 8601 UTC + level: string; // lowercase: "info", "warn", "error", "debug", "trace", "fatal" + logger: string; // source/component: "RssSyncService", "DiskScanService" + message: string; + exception?: string | null; + exceptionType?: string | null; + method?: string | null; +} + +/** + * Paginated log response from /api/v3/log + */ +export interface ArrLogResponse { + page: number; + pageSize: number; + sortKey: string; + sortDirection: string; + totalRecords: number; + records: ArrLogEntry[]; +} + +/** + * Log file metadata from /api/v3/log/file + */ +export interface ArrLogFile { + id: number; + filename: string; + lastWriteTime: string; // ISO 8601 + contentsUrl: string; + downloadUrl: string; +} + +/** + * Parameters for fetching logs + */ +export interface ArrLogParams { + page?: number; + pageSize?: number; + sortKey?: string; + sortDirection?: ArrSortDirection; + level?: ArrLogLevel; +} diff --git a/src/routes/arr/[id]/logs/+page.server.ts b/src/routes/arr/[id]/logs/+page.server.ts index d66b1d8..21c269a 100644 --- a/src/routes/arr/[id]/logs/+page.server.ts +++ b/src/routes/arr/[id]/logs/+page.server.ts @@ -1,8 +1,10 @@ import { error } from '@sveltejs/kit'; import type { ServerLoad } from '@sveltejs/kit'; import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; +import { createArrClient } from '$arr/factory.ts'; +import type { ArrType } from '$arr/types.ts'; -export const load: ServerLoad = async ({ params }) => { +export const load: ServerLoad = async ({ params, url }) => { const id = parseInt(params.id || '', 10); if (isNaN(id)) { @@ -15,7 +17,34 @@ export const load: ServerLoad = async ({ params }) => { error(404, `Instance not found: ${id}`); } - return { - instance - }; + // Parse query params for pagination/filtering + const page = parseInt(url.searchParams.get('page') || '1', 10); + const pageSize = parseInt(url.searchParams.get('pageSize') || '50', 10); + const level = url.searchParams.get('level') || undefined; + + const client = createArrClient(instance.type as ArrType, instance.url, instance.api_key); + + try { + const logs = await client.getLogs({ + page, + pageSize, + sortKey: 'time', + sortDirection: 'descending', + level: level as 'Trace' | 'Debug' | 'Info' | 'Warn' | 'Error' | 'Fatal' | undefined + }); + + return { + instance, + logs, + filters: { + page, + pageSize, + level + } + }; + } catch (err) { + error(500, `Failed to fetch logs: ${err instanceof Error ? err.message : 'Unknown error'}`); + } finally { + client.close(); + } }; diff --git a/src/routes/arr/[id]/logs/+page.svelte b/src/routes/arr/[id]/logs/+page.svelte index 9ee04fd..9eab1aa 100644 --- a/src/routes/arr/[id]/logs/+page.svelte +++ b/src/routes/arr/[id]/logs/+page.svelte @@ -1,7 +1,148 @@ @@ -9,5 +150,137 @@
- + + + + + + + + + {#each logLevels as level} + + {/each} + + + + + + + + +
+ + +
+
+
+
+ + + + + + +
+ Refresh logs +
+
+
+
+
+ + +
+ + Showing {filteredLogs.length} of {data.logs.totalRecords} logs + {#if selectedLevel !== 'ALL'} + (filtered by {selectedLevel}) + {/if} + + + + {#if totalPages > 1} +
+ + + Page {currentPage} of {totalPages} + + +
+ {/if} +
+ + + + +
+ copyLog(row)} + /> +
+
+
+ + + {#if totalPages > 1} +
+ + + Page {currentPage} of {totalPages} + + +
+ {/if}