From 9c759ba99bfac3e2971b041db8a2c7b586dc3eb7 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:53:04 +0100 Subject: [PATCH] fix: ESLint/TypeScript fixes - nullish coalescing, regexp-exec, optional-chain, unescaped-entities, unused-vars, type-safety --- src/app/_components/BackupWarningModal.tsx | 47 +- src/app/_components/BackupsTab.tsx | 244 +- src/app/_components/DownloadedScriptsTab.tsx | 377 +-- src/app/_components/FilterBar.tsx | 649 ++--- src/app/_components/GeneralSettingsModal.tsx | 1390 +++++++---- src/app/_components/HelpModal.tsx | 2220 ++++++++++++----- src/app/_components/InstalledScriptsTab.tsx | 2046 +++++++++------ src/app/_components/LoadingModal.tsx | 71 +- src/app/_components/PBSCredentialsModal.tsx | 217 +- src/app/_components/ScriptCard.tsx | 108 +- src/app/_components/ScriptCardList.tsx | 209 +- src/app/_components/ScriptDetailModal.tsx | 565 +++-- src/app/_components/ScriptVersionModal.tsx | 163 +- src/app/_components/ServerForm.tsx | 737 +++--- src/app/_components/ServerStoragesModal.tsx | 190 +- src/app/_components/TextViewer.tsx | 294 ++- .../_components/UpdateConfirmationModal.tsx | 174 +- src/app/page.tsx | 278 ++- 18 files changed, 6229 insertions(+), 3750 deletions(-) diff --git a/src/app/_components/BackupWarningModal.tsx b/src/app/_components/BackupWarningModal.tsx index d93f5c9..d5e3f5f 100644 --- a/src/app/_components/BackupWarningModal.tsx +++ b/src/app/_components/BackupWarningModal.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import { Button } from './ui/button'; -import { AlertTriangle } from 'lucide-react'; -import { useRegisterModal } from './modal/ModalStackProvider'; +import { Button } from "./ui/button"; +import { AlertTriangle } from "lucide-react"; +import { useRegisterModal } from "./modal/ModalStackProvider"; interface BackupWarningModalProps { isOpen: boolean; @@ -13,33 +13,43 @@ interface BackupWarningModalProps { export function BackupWarningModal({ isOpen, onClose, - onProceed + onProceed, }: BackupWarningModalProps) { - useRegisterModal(isOpen, { id: 'backup-warning-modal', allowEscape: true, onClose }); + useRegisterModal(isOpen, { + id: "backup-warning-modal", + allowEscape: true, + onClose, + }); if (!isOpen) return null; return ( -
-
+
+
{/* Header */} -
+
- -

Backup Failed

+ +

+ Backup Failed +

{/* Content */}
-

- The backup failed, but you can still proceed with the update if you wish. -

- Warning: Proceeding without a backup means you won't be able to restore the container if something goes wrong during the update. +

+ The backup failed, but you can still proceed with the update if you + wish. +
+
+ Warning: Proceeding + without a backup means you won't be able to restore the + container if something goes wrong during the update.

{/* Action Buttons */} -
+
@@ -62,6 +72,3 @@ export function BackupWarningModal({
); } - - - diff --git a/src/app/_components/BackupsTab.tsx b/src/app/_components/BackupsTab.tsx index 2e24b7b..4f579b1 100644 --- a/src/app/_components/BackupsTab.tsx +++ b/src/app/_components/BackupsTab.tsx @@ -1,18 +1,27 @@ -'use client'; +"use client"; -import { useState, useEffect } from 'react'; -import { api } from '~/trpc/react'; -import { Button } from './ui/button'; -import { Badge } from './ui/badge'; -import { RefreshCw, ChevronDown, ChevronRight, HardDrive, Database, Server, CheckCircle, AlertCircle } from 'lucide-react'; +import { useState, useEffect } from "react"; +import { api } from "~/trpc/react"; +import { Button } from "./ui/button"; +import { Badge } from "./ui/badge"; +import { + RefreshCw, + ChevronDown, + ChevronRight, + HardDrive, + Database, + Server, + CheckCircle, + AlertCircle, +} from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from './ui/dropdown-menu'; -import { ConfirmationModal } from './ConfirmationModal'; -import { LoadingModal } from './LoadingModal'; +} from "./ui/dropdown-menu"; +import { ConfirmationModal } from "./ConfirmationModal"; +import { LoadingModal } from "./LoadingModal"; interface Backup { id: number; @@ -35,16 +44,25 @@ interface ContainerBackups { } export function BackupsTab() { - const [expandedContainers, setExpandedContainers] = useState>(new Set()); + const [expandedContainers, setExpandedContainers] = useState>( + new Set(), + ); const [hasAutoDiscovered, setHasAutoDiscovered] = useState(false); const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false); - const [selectedBackup, setSelectedBackup] = useState<{ backup: Backup; containerId: string } | null>(null); + const [selectedBackup, setSelectedBackup] = useState<{ + backup: Backup; + containerId: string; + } | null>(null); const [restoreProgress, setRestoreProgress] = useState([]); const [restoreSuccess, setRestoreSuccess] = useState(false); const [restoreError, setRestoreError] = useState(null); const [shouldPollRestore, setShouldPollRestore] = useState(false); - const { data: backupsData, refetch: refetchBackups, isLoading } = api.backups.getAllBackupsGrouped.useQuery(); + const { + data: backupsData, + refetch: refetchBackups, + isLoading, + } = api.backups.getAllBackupsGrouped.useQuery(); const discoverMutation = api.backups.discoverBackups.useMutation({ onSuccess: () => { void refetchBackups(); @@ -52,26 +70,30 @@ export function BackupsTab() { }); // Poll for restore progress - const { data: restoreLogsData } = api.backups.getRestoreProgress.useQuery(undefined, { - enabled: shouldPollRestore, - refetchInterval: 1000, // Poll every second - refetchIntervalInBackground: true, - }); + const { data: restoreLogsData } = api.backups.getRestoreProgress.useQuery( + undefined, + { + enabled: shouldPollRestore, + refetchInterval: 1000, // Poll every second + refetchIntervalInBackground: true, + }, + ); // Update restore progress when log data changes useEffect(() => { if (restoreLogsData?.success && restoreLogsData.logs) { setRestoreProgress(restoreLogsData.logs); - + // Stop polling when restore is complete if (restoreLogsData.isComplete) { setShouldPollRestore(false); // Check if restore was successful or failed - const lastLog = restoreLogsData.logs[restoreLogsData.logs.length - 1] || ''; - if (lastLog.includes('Restore completed successfully')) { + const lastLog = + restoreLogsData.logs[restoreLogsData.logs.length - 1] || ""; + if (lastLog.includes("Restore completed successfully")) { setRestoreSuccess(true); setRestoreError(null); - } else if (lastLog.includes('Error:') || lastLog.includes('failed')) { + } else if (lastLog.includes("Error:") || lastLog.includes("failed")) { setRestoreError(lastLog); setRestoreSuccess(false); } @@ -83,17 +105,22 @@ export function BackupsTab() { onMutate: () => { // Start polling for progress setShouldPollRestore(true); - setRestoreProgress(['Starting restore...']); + setRestoreProgress(["Starting restore..."]); setRestoreError(null); setRestoreSuccess(false); }, onSuccess: (result) => { // Stop polling - progress will be updated from logs setShouldPollRestore(false); - + if (result.success) { // Update progress with all messages from backend (fallback if polling didn't work) - const progressMessages = restoreProgress.length > 0 ? restoreProgress : (result.progress?.map(p => p.message) || ['Restore completed successfully']); + const progressMessages = + restoreProgress.length > 0 + ? restoreProgress + : result.progress?.map((p) => p.message) || [ + "Restore completed successfully", + ]; setRestoreProgress(progressMessages); setRestoreSuccess(true); setRestoreError(null); @@ -101,8 +128,10 @@ export function BackupsTab() { setSelectedBackup(null); // Keep success message visible - user can dismiss manually } else { - setRestoreError(result.error || 'Restore failed'); - setRestoreProgress(result.progress?.map(p => p.message) || restoreProgress); + setRestoreError(result.error || "Restore failed"); + setRestoreProgress( + result.progress?.map((p) => p.message) || restoreProgress, + ); setRestoreSuccess(false); setRestoreConfirmOpen(false); setSelectedBackup(null); @@ -112,17 +141,18 @@ export function BackupsTab() { onError: (error) => { // Stop polling on error setShouldPollRestore(false); - setRestoreError(error.message || 'Restore failed'); + setRestoreError(error.message || "Restore failed"); setRestoreConfirmOpen(false); setSelectedBackup(null); setRestoreProgress([]); }, }); - + // Update progress text in modal based on current progress - const currentProgressText = restoreProgress.length > 0 - ? restoreProgress[restoreProgress.length - 1] - : 'Restoring backup...'; + const currentProgressText = + restoreProgress.length > 0 + ? restoreProgress[restoreProgress.length - 1] + : "Restoring backup..."; // Auto-discover backups when tab is first opened useEffect(() => { @@ -149,11 +179,11 @@ export function BackupsTab() { const handleRestoreConfirm = () => { if (!selectedBackup) return; - + setRestoreConfirmOpen(false); setRestoreError(null); setRestoreSuccess(false); - + restoreMutation.mutate({ backupId: selectedBackup.backup.id, containerId: selectedBackup.containerId, @@ -172,39 +202,41 @@ export function BackupsTab() { }; const formatFileSize = (bytes: bigint | null): string => { - if (!bytes) return 'Unknown size'; + if (!bytes) return "Unknown size"; const b = Number(bytes); - if (b === 0) return '0 B'; + if (b === 0) return "0 B"; const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(b) / Math.log(k)); return `${(b / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; }; const formatDate = (date: Date | null): string => { - if (!date) return 'Unknown date'; + if (!date) return "Unknown date"; return new Date(date).toLocaleString(); }; const getStorageTypeIcon = (type: string) => { switch (type) { - case 'pbs': + case "pbs": return ; - case 'local': + case "local": return ; default: return ; } }; - const getStorageTypeBadgeVariant = (type: string): 'default' | 'secondary' | 'outline' => { + const getStorageTypeBadgeVariant = ( + type: string, + ): "default" | "secondary" | "outline" => { switch (type) { - case 'pbs': - return 'default'; - case 'local': - return 'secondary'; + case "pbs": + return "default"; + case "local": + return "secondary"; default: - return 'outline'; + return "outline"; } }; @@ -216,8 +248,8 @@ export function BackupsTab() { {/* Header with refresh button */}
-

Backups

-

+

Backups

+

Discovered backups grouped by container ID

@@ -226,31 +258,38 @@ export function BackupsTab() { disabled={isDiscovering} className="flex items-center gap-2" > - - {isDiscovering ? 'Discovering...' : 'Discover Backups'} + + {isDiscovering ? "Discovering..." : "Discover Backups"}
{/* Loading state */} {(isLoading || isDiscovering) && backups.length === 0 && ( -
- +
+

- {isDiscovering ? 'Discovering backups...' : 'Loading backups...'} + {isDiscovering ? "Discovering backups..." : "Loading backups..."}

)} {/* Empty state */} {!isLoading && !isDiscovering && backups.length === 0 && ( -
- -

No backups found

+
+ +

+ No backups found +

- Click "Discover Backups" to scan for backups on your servers. + Click "Discover Backups" to scan for backups on your + servers.

@@ -266,33 +305,35 @@ export function BackupsTab() { return (
{/* Container header - collapsible */} - + handleRestoreClick(backup, container.container_id)} + onClick={() => + handleRestoreClick( + backup, + container.container_id, + ) + } disabled={restoreMutation.isPending} className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" > @@ -386,9 +434,9 @@ export function BackupsTab() { {/* Error state */} {backupsData && !backupsData.success && ( -
+

- Error loading backups: {backupsData.error || 'Unknown error'} + Error loading backups: {backupsData.error || "Unknown error"}

)} @@ -412,7 +460,8 @@ export function BackupsTab() { )} {/* Restore Progress Modal */} - {(restoreMutation.isPending || (restoreSuccess && restoreProgress.length > 0)) && ( + {(restoreMutation.isPending || + (restoreSuccess && restoreProgress.length > 0)) && ( -
+
+
- - Restore Completed Successfully + + + Restore Completed Successfully +
-

+

The container has been restored from backup.

@@ -454,11 +505,11 @@ export function BackupsTab() { {/* Restore Error */} {restoreError && ( -
-
+
+
- - Restore Failed + + Restore Failed
-

- {restoreError} -

+

{restoreError}

{restoreProgress.length > 0 && ( -
+
{restoreProgress.map((message, index) => ( -

+

{message}

))} @@ -500,4 +549,3 @@ export function BackupsTab() {
); } - diff --git a/src/app/_components/DownloadedScriptsTab.tsx b/src/app/_components/DownloadedScriptsTab.tsx index 279906c..4d4c1e3 100644 --- a/src/app/_components/DownloadedScriptsTab.tsx +++ b/src/app/_components/DownloadedScriptsTab.tsx @@ -1,41 +1,53 @@ -'use client'; +"use client"; -import React, { useState, useRef, useEffect } from 'react'; -import { api } from '~/trpc/react'; -import { ScriptCard } from './ScriptCard'; -import { ScriptCardList } from './ScriptCardList'; -import { ScriptDetailModal } from './ScriptDetailModal'; -import { CategorySidebar } from './CategorySidebar'; -import { FilterBar, type FilterState } from './FilterBar'; -import { ViewToggle } from './ViewToggle'; -import { Button } from './ui/button'; -import type { ScriptCard as ScriptCardType } from '~/types/script'; -import { getDefaultFilters, mergeFiltersWithDefaults } from './filterUtils'; +import React, { useState, useRef, useEffect } from "react"; +import { api } from "~/trpc/react"; +import { ScriptCard } from "./ScriptCard"; +import { ScriptCardList } from "./ScriptCardList"; +import { ScriptDetailModal } from "./ScriptDetailModal"; +import { CategorySidebar } from "./CategorySidebar"; +import { FilterBar, type FilterState } from "./FilterBar"; +import { ViewToggle } from "./ViewToggle"; +import { Button } from "./ui/button"; +import type { ScriptCard as ScriptCardType } from "~/types/script"; +import type { Server } from "~/types/server"; +import { getDefaultFilters, mergeFiltersWithDefaults } from "./filterUtils"; interface DownloadedScriptsTabProps { onInstallScript?: ( scriptPath: string, scriptName: string, mode?: "local" | "ssh", - server?: any, + server?: Server, ) => void; } -export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabProps) { +export function DownloadedScriptsTab({ + onInstallScript, +}: DownloadedScriptsTabProps) { const [selectedSlug, setSelectedSlug] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedCategory, setSelectedCategory] = useState(null); - const [viewMode, setViewMode] = useState<'card' | 'list'>('card'); + const [viewMode, setViewMode] = useState<"card" | "list">("card"); const [filters, setFilters] = useState(getDefaultFilters()); const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false); const [isLoadingFilters, setIsLoadingFilters] = useState(true); const gridRef = useRef(null); - const { data: scriptCardsData, isLoading: githubLoading, error: githubError, refetch } = api.scripts.getScriptCardsWithCategories.useQuery(); - const { data: localScriptsData, isLoading: localLoading, error: localError } = api.scripts.getAllDownloadedScripts.useQuery(); + const { + data: scriptCardsData, + isLoading: githubLoading, + error: githubError, + refetch, + } = api.scripts.getScriptCardsWithCategories.useQuery(); + const { + data: localScriptsData, + isLoading: localLoading, + error: localError, + } = api.scripts.getAllDownloadedScripts.useQuery(); const { data: scriptData } = api.scripts.getScriptBySlug.useQuery( - { slug: selectedSlug ?? '' }, - { enabled: !!selectedSlug } + { slug: selectedSlug ?? "" }, + { enabled: !!selectedSlug }, ); // Load SAVE_FILTER setting, saved filters, and view mode on component mount @@ -43,7 +55,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr const loadSettings = async () => { try { // Load SAVE_FILTER setting - const saveFilterResponse = await fetch('/api/settings/save-filter'); + const saveFilterResponse = await fetch("/api/settings/save-filter"); let saveFilterEnabled = false; if (saveFilterResponse.ok) { const saveFilterData = await saveFilterResponse.json(); @@ -53,7 +65,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Load saved filters if SAVE_FILTER is enabled if (saveFilterEnabled) { - const filtersResponse = await fetch('/api/settings/filters'); + const filtersResponse = await fetch("/api/settings/filters"); if (filtersResponse.ok) { const filtersData = await filtersResponse.json(); if (filtersData.filters) { @@ -63,16 +75,20 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr } // Load view mode - const viewModeResponse = await fetch('/api/settings/view-mode'); + const viewModeResponse = await fetch("/api/settings/view-mode"); if (viewModeResponse.ok) { const viewModeData = await viewModeResponse.json(); const viewMode = viewModeData.viewMode; - if (viewMode && typeof viewMode === 'string' && (viewMode === 'card' || viewMode === 'list')) { + if ( + viewMode && + typeof viewMode === "string" && + (viewMode === "card" || viewMode === "list") + ) { setViewMode(viewMode); } } } catch (error) { - console.error('Error loading settings:', error); + console.error("Error loading settings:", error); } finally { setIsLoadingFilters(false); } @@ -87,15 +103,15 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr const saveFilters = async () => { try { - await fetch('/api/settings/filters', { - method: 'POST', + await fetch("/api/settings/filters", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ filters }), }); } catch (error) { - console.error('Error saving filters:', error); + console.error("Error saving filters:", error); } }; @@ -110,15 +126,15 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr const saveViewMode = async () => { try { - await fetch('/api/settings/view-mode', { - method: 'POST', + await fetch("/api/settings/view-mode", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ viewMode }), }); } catch (error) { - console.error('Error saving view mode:', error); + console.error("Error saving view mode:", error); } }; @@ -129,31 +145,32 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Extract categories from metadata const categories = React.useMemo((): string[] => { - if (!scriptCardsData?.success || !scriptCardsData.metadata?.categories) return []; - + if (!scriptCardsData?.success || !scriptCardsData.metadata?.categories) + return []; + return (scriptCardsData.metadata.categories as any[]) .filter((cat) => cat.id !== 0) // Exclude Miscellaneous for main list .sort((a, b) => a.sort_order - b.sort_order) .map((cat) => cat.name as string) - .filter((name): name is string => typeof name === 'string'); + .filter((name): name is string => typeof name === "string"); }, [scriptCardsData]); // Get GitHub scripts with download status (deduplicated) const combinedScripts = React.useMemo((): ScriptCardType[] => { if (!scriptCardsData?.success) return []; - + // Use Map to deduplicate by slug/name const scriptMap = new Map(); - - scriptCardsData.cards?.forEach(script => { + + scriptCardsData.cards?.forEach((script) => { if (script?.name && script?.slug) { // Use slug as unique identifier, only keep first occurrence if (!scriptMap.has(script.slug)) { scriptMap.set(script.slug, { ...script, - source: 'github' as const, + source: "github" as const, isDownloaded: false, // Will be updated by status check - isUpToDate: false, // Will be updated by status check + isUpToDate: false, // Will be updated by status check }); } } @@ -165,68 +182,77 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Update scripts with download status and filter to only downloaded scripts const downloadedScripts = React.useMemo((): ScriptCardType[] => { // Helper to normalize identifiers so underscores vs hyphens don't break matches - const normalizeId = (s?: string): string => (s ?? '') - .toLowerCase() - .replace(/\.(sh|bash|py|js|ts)$/g, '') - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); + const normalizeId = (s?: string): string => + (s ?? "") + .toLowerCase() + .replace(/\.(sh|bash|py|js|ts)$/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); return combinedScripts - .map(script => { + .map((script) => { if (!script?.name) { return script; // Return as-is if invalid } - + // Check if there's a corresponding local script - const hasLocalVersion = localScriptsData?.scripts?.some(local => { - if (!local?.name) return false; - - // Primary: Exact slug-to-slug matching (most reliable, prevents false positives) - if (local.slug && script.slug) { - if (local.slug.toLowerCase() === script.slug.toLowerCase()) { - return true; + const hasLocalVersion = + localScriptsData?.scripts?.some((local) => { + if (!local?.name) return false; + + // Primary: Exact slug-to-slug matching (most reliable, prevents false positives) + if (local.slug && script.slug) { + if (local.slug.toLowerCase() === script.slug.toLowerCase()) { + return true; + } } - } - - // Secondary: Check install basenames (for edge cases where install script names differ from slugs) - // Only use normalized matching for install basenames, not for slug/name matching - const normalizedLocal = normalizeId(local.name); - const matchesInstallBasename = (script as any)?.install_basenames?.some((base: string) => normalizeId(base) === normalizedLocal) ?? false; - return matchesInstallBasename; - }) ?? false; - + + // Secondary: Check install basenames (for edge cases where install script names differ from slugs) + // Only use normalized matching for install basenames, not for slug/name matching + const normalizedLocal = normalizeId(local.name); + const matchesInstallBasename = + (script as any)?.install_basenames?.some( + (base: string) => normalizeId(base) === normalizedLocal, + ) ?? false; + return matchesInstallBasename; + }) ?? false; + return { ...script, isDownloaded: hasLocalVersion, }; }) - .filter(script => script.isDownloaded); // Only show downloaded scripts + .filter((script) => script.isDownloaded); // Only show downloaded scripts }, [combinedScripts, localScriptsData]); // Count scripts per category (using downloaded scripts only) const categoryCounts = React.useMemo((): Record => { if (!scriptCardsData?.success) return {}; - + const counts: Record = {}; - + // Initialize all categories with 0 categories.forEach((categoryName: string) => { counts[categoryName] = 0; }); - + // Count each unique downloaded script only once per category - downloadedScripts.forEach(script => { + downloadedScripts.forEach((script) => { if (script.categoryNames && script.slug) { const countedCategories = new Set(); script.categoryNames.forEach((categoryName: unknown) => { - if (typeof categoryName === 'string' && counts[categoryName] !== undefined && !countedCategories.has(categoryName)) { + if ( + typeof categoryName === "string" && + counts[categoryName] !== undefined && + !countedCategories.has(categoryName) + ) { countedCategories.add(categoryName); counts[categoryName]++; } }); } }); - + return counts; }, [categories, downloadedScripts, scriptCardsData?.success]); @@ -237,15 +263,15 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Filter by search query if (filters.searchQuery?.trim()) { const query = filters.searchQuery.toLowerCase().trim(); - + if (query.length >= 1) { - scripts = scripts.filter(script => { - if (!script || typeof script !== 'object') { + scripts = scripts.filter((script) => { + if (!script || typeof script !== "object") { return false; } - const name = (script.name ?? '').toLowerCase(); - const slug = (script.slug ?? '').toLowerCase(); + const name = (script.name ?? "").toLowerCase(); + const slug = (script.slug ?? "").toLowerCase(); return name.includes(query) ?? slug.includes(query); }); @@ -254,9 +280,9 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Filter by category using real category data from downloaded scripts if (selectedCategory) { - scripts = scripts.filter(script => { + scripts = scripts.filter((script) => { if (!script) return false; - + // Check if the downloaded script has categoryNames that include the selected category return script.categoryNames?.includes(selectedCategory) ?? false; }); @@ -264,7 +290,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Filter by updateable status if (filters.showUpdatable !== null) { - scripts = scripts.filter(script => { + scripts = scripts.filter((script) => { if (!script) return false; const isUpdatable = script.updateable ?? false; return filters.showUpdatable ? isUpdatable : !isUpdatable; @@ -273,28 +299,30 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Filter by script types if (filters.selectedTypes.length > 0) { - scripts = scripts.filter(script => { + scripts = scripts.filter((script) => { if (!script) return false; - const scriptType = (script.type ?? '').toLowerCase(); - + const scriptType = (script.type ?? "").toLowerCase(); + // Map non-standard types to standard categories - const mappedType = scriptType === 'turnkey' ? 'ct' : scriptType; - - return filters.selectedTypes.some(type => type.toLowerCase() === mappedType); + const mappedType = scriptType === "turnkey" ? "ct" : scriptType; + + return filters.selectedTypes.some( + (type) => type.toLowerCase() === mappedType, + ); }); } // Filter by repositories if (filters.selectedRepositories.length > 0) { - scripts = scripts.filter(script => { + scripts = scripts.filter((script) => { if (!script) return false; const repoUrl = script.repository_url; - + // If script has no repository_url, exclude it when filtering by repositories if (!repoUrl) { return false; } - + // Only include scripts from selected repositories return filters.selectedRepositories.includes(repoUrl); }); @@ -303,18 +331,18 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Apply sorting scripts.sort((a, b) => { if (!a || !b) return 0; - + let compareValue = 0; - + switch (filters.sortBy) { - case 'name': - compareValue = (a.name ?? '').localeCompare(b.name ?? ''); + case "name": + compareValue = (a.name ?? "").localeCompare(b.name ?? ""); break; - case 'created': + case "created": // Get creation date from script metadata in JSON format (date_created: "YYYY-MM-DD") - const aCreated = a?.date_created ?? ''; - const bCreated = b?.date_created ?? ''; - + const aCreated = a?.date_created ?? ""; + const bCreated = b?.date_created ?? ""; + // If both have dates, compare them directly if (aCreated && bCreated) { // For dates: asc = oldest first (2020 before 2024), desc = newest first (2024 before 2020) @@ -327,15 +355,15 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr compareValue = 1; } else { // Both have no dates, fallback to name comparison - compareValue = (a.name ?? '').localeCompare(b.name ?? ''); + compareValue = (a.name ?? "").localeCompare(b.name ?? ""); } break; default: - compareValue = (a.name ?? '').localeCompare(b.name ?? ''); + compareValue = (a.name ?? "").localeCompare(b.name ?? ""); } - + // Apply sort order - return filters.sortOrder === 'asc' ? compareValue : -compareValue; + return filters.sortOrder === "asc" ? compareValue : -compareValue; }); return scripts; @@ -343,8 +371,10 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Calculate filter counts for FilterBar const filterCounts = React.useMemo(() => { - const updatableCount = downloadedScripts.filter(script => script?.updateable).length; - + const updatableCount = downloadedScripts.filter( + (script) => script?.updateable, + ).length; + return { installedCount: downloadedScripts.length, updatableCount }; }, [downloadedScripts]); @@ -362,13 +392,13 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr useEffect(() => { if (selectedCategory && gridRef.current) { const timeoutId = setTimeout(() => { - gridRef.current?.scrollIntoView({ - behavior: 'smooth', - block: 'start', - inline: 'nearest' + gridRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest", }); }, 100); - + return () => clearTimeout(timeoutId); } }, [selectedCategory]); @@ -387,22 +417,38 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr if (githubLoading || localLoading) { return (
-
- Loading downloaded scripts... +
+ + Loading downloaded scripts... +
); } if (githubError || localError) { return ( -
+
- - + + -

Failed to load downloaded scripts

-

- {githubError?.message ?? localError?.message ?? 'Unknown error occurred'} +

+ Failed to load downloaded scripts +

+

+ {githubError?.message ?? + localError?.message ?? + "Unknown error occurred"}

- ) : ( - viewMode === 'card' ? ( -
- {filteredScripts.map((script, index) => { + ) : viewMode === "card" ? ( +
+ {filteredScripts.map((script, index) => { // Add validation to ensure script has required properties - if (!script || typeof script !== 'object') { + if (!script || typeof script !== "object") { return null; } - + // Create a unique key by combining slug, name, and index to handle duplicates - const uniqueKey = `${script.slug ?? 'unknown'}-${script.name ?? 'unnamed'}-${index}`; - + const uniqueKey = `${script.slug ?? "unknown"}-${script.name ?? "unnamed"}-${index}`; + return ( ); })} -
- ) : ( -
- {filteredScripts.map((script, index) => { +
+ ) : ( +
+ {filteredScripts.map((script, index) => { // Add validation to ensure script has required properties - if (!script || typeof script !== 'object') { + if (!script || typeof script !== "object") { return null; } - + // Create a unique key by combining slug, name, and index to handle duplicates - const uniqueKey = `${script.slug ?? 'unknown'}-${script.name ?? 'unnamed'}-${index}`; - + const uniqueKey = `${script.slug ?? "unknown"}-${script.name ?? "unnamed"}-${index}`; + return ( ); })} -
- ) +
)} { try { - const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/); + const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(url); if (match) { return `${match[1]}/${match[2]}`; } @@ -98,29 +108,33 @@ export function FilterBar({ }; return ( -
+
{/* Loading State */} {isLoadingFilters && (
-
-
+
+
Loading saved filters...
)} - {/* Filter Header */} {!isLoadingFilters && (
-

Filter Scripts

+

+ Filter Scripts +

- + - - {/* Type Dropdown */} -
- - - {isTypeDropdownOpen && ( -
-
- {SCRIPT_TYPES.map((type) => { - const IconComponent = type.Icon; - return ( - - ); - })} -
-
- -
-
- )} -
- - {/* Repository Filter Buttons - Only show if more than one enabled repo */} - {enabledRepos.length > 1 && enabledRepos.map((repo) => { - const isSelected = filters.selectedRepositories.includes(repo.url); - return ( +
+ {/* Updateable Filter */} - ); - })} - {/* Sort By Dropdown */} -
- + {/* Type Dropdown */} +
+ - {isSortDropdownOpen && ( -
-
- - -
+ {isTypeDropdownOpen && ( +
+
+ {SCRIPT_TYPES.map((type) => { + const IconComponent = type.Icon; + return ( + + ); + })} +
+
+ +
+
+ )}
- )} -
- {/* Sort Order Button */} - -
+ {/* Repository Filter Buttons - Only show if more than one enabled repo */} + {enabledRepos.length > 1 && + enabledRepos.map((repo) => { + const isSelected = filters.selectedRepositories.includes( + repo.url, + ); + return ( + + ); + })} - {/* Filter Summary and Clear All */} -
-
-
- {filteredCount === totalScripts ? ( - Showing all {totalScripts} scripts - ) : ( - - {filteredCount} of {totalScripts} scripts{" "} - {hasActiveFilters && ( - - (filtered) + {/* Sort By Dropdown */} +
+ + + {isSortDropdownOpen && ( +
+
+ + +
+
+ )} +
+ + {/* Sort Order Button */} + +
+ + {/* Filter Summary and Clear All */} +
+
+
+ {filteredCount === totalScripts ? ( + Showing all {totalScripts} scripts + ) : ( + + {filteredCount} of {totalScripts} scripts{" "} + {hasActiveFilters && ( + (filtered) + )} )} - +
+ + {/* Filter Persistence Status */} + {!isLoadingFilters && saveFiltersEnabled && ( +
+ + + + Filters are being saved automatically +
+ )} +
+ + {hasActiveFilters && ( + )}
- - {/* Filter Persistence Status */} - {!isLoadingFilters && saveFiltersEnabled && ( -
- - - - Filters are being saved automatically -
- )} -
- - {hasActiveFilters && ( - - )} -
)} diff --git a/src/app/_components/GeneralSettingsModal.tsx b/src/app/_components/GeneralSettingsModal.tsx index 5e8110c..c592c8b 100644 --- a/src/app/_components/GeneralSettingsModal.tsx +++ b/src/app/_components/GeneralSettingsModal.tsx @@ -1,39 +1,52 @@ -'use client'; +"use client"; -import { useState, useEffect } from 'react'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Toggle } from './ui/toggle'; -import { ContextualHelpIcon } from './ContextualHelpIcon'; -import { useTheme } from './ThemeProvider'; -import { useRegisterModal } from './modal/ModalStackProvider'; -import { api } from '~/trpc/react'; -import { useAuth } from './AuthProvider'; -import { Trash2, ExternalLink } from 'lucide-react'; +import { useState, useEffect } from "react"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Toggle } from "./ui/toggle"; +import { ContextualHelpIcon } from "./ContextualHelpIcon"; +import { useTheme } from "./ThemeProvider"; +import { useRegisterModal } from "./modal/ModalStackProvider"; +import { api } from "~/trpc/react"; +import { useAuth } from "./AuthProvider"; +import { Trash2, ExternalLink } from "lucide-react"; interface GeneralSettingsModalProps { isOpen: boolean; onClose: () => void; } -export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalProps) { - useRegisterModal(isOpen, { id: 'general-settings-modal', allowEscape: true, onClose }); +export function GeneralSettingsModal({ + isOpen, + onClose, +}: GeneralSettingsModalProps) { + useRegisterModal(isOpen, { + id: "general-settings-modal", + allowEscape: true, + onClose, + }); const { theme, setTheme } = useTheme(); const { isAuthenticated, expirationTime, checkAuth } = useAuth(); - const [activeTab, setActiveTab] = useState<'general' | 'github' | 'auth' | 'auto-sync' | 'repositories'>('general'); - const [sessionExpirationDisplay, setSessionExpirationDisplay] = useState(''); - const [githubToken, setGithubToken] = useState(''); + const [activeTab, setActiveTab] = useState< + "general" | "github" | "auth" | "auto-sync" | "repositories" + >("general"); + const [sessionExpirationDisplay, setSessionExpirationDisplay] = + useState(""); + const [githubToken, setGithubToken] = useState(""); const [saveFilter, setSaveFilter] = useState(false); const [savedFilters, setSavedFilters] = useState(null); const [colorCodingEnabled, setColorCodingEnabled] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); - const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); - + const [message, setMessage] = useState<{ + type: "success" | "error"; + text: string; + } | null>(null); + // Auth state - const [authUsername, setAuthUsername] = useState(''); - const [authPassword, setAuthPassword] = useState(''); - const [authConfirmPassword, setAuthConfirmPassword] = useState(''); + const [authUsername, setAuthUsername] = useState(""); + const [authPassword, setAuthPassword] = useState(""); + const [authConfirmPassword, setAuthConfirmPassword] = useState(""); const [authEnabled, setAuthEnabled] = useState(false); const [authHasCredentials, setAuthHasCredentials] = useState(false); const [authSetupCompleted, setAuthSetupCompleted] = useState(false); @@ -42,29 +55,36 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr // Auto-sync state const [autoSyncEnabled, setAutoSyncEnabled] = useState(false); - const [syncIntervalType, setSyncIntervalType] = useState<'predefined' | 'custom'>('predefined'); - const [syncIntervalPredefined, setSyncIntervalPredefined] = useState('1hour'); - const [syncIntervalCron, setSyncIntervalCron] = useState(''); + const [syncIntervalType, setSyncIntervalType] = useState< + "predefined" | "custom" + >("predefined"); + const [syncIntervalPredefined, setSyncIntervalPredefined] = useState("1hour"); + const [syncIntervalCron, setSyncIntervalCron] = useState(""); const [autoDownloadNew, setAutoDownloadNew] = useState(false); const [autoUpdateExisting, setAutoUpdateExisting] = useState(false); const [notificationEnabled, setNotificationEnabled] = useState(false); const [appriseUrls, setAppriseUrls] = useState([]); - const [appriseUrlsText, setAppriseUrlsText] = useState(''); - const [lastAutoSync, setLastAutoSync] = useState(''); - const [lastAutoSyncError, setLastAutoSyncError] = useState(null); - const [lastAutoSyncErrorTime, setLastAutoSyncErrorTime] = useState(null); - const [cronValidationError, setCronValidationError] = useState(''); + const [appriseUrlsText, setAppriseUrlsText] = useState(""); + const [lastAutoSync, setLastAutoSync] = useState(""); + const [lastAutoSyncError, setLastAutoSyncError] = useState( + null, + ); + const [lastAutoSyncErrorTime, setLastAutoSyncErrorTime] = useState< + string | null + >(null); + const [cronValidationError, setCronValidationError] = useState(""); // Repository management state - const [newRepoUrl, setNewRepoUrl] = useState(''); + const [newRepoUrl, setNewRepoUrl] = useState(""); const [newRepoEnabled, setNewRepoEnabled] = useState(true); const [isAddingRepo, setIsAddingRepo] = useState(false); const [deletingRepoId, setDeletingRepoId] = useState(null); // Repository queries and mutations - const { data: repositoriesData, refetch: refetchRepositories } = api.repositories.getAll.useQuery(undefined, { - enabled: isOpen && activeTab === 'repositories' - }); + const { data: repositoriesData, refetch: refetchRepositories } = + api.repositories.getAll.useQuery(undefined, { + enabled: isOpen && activeTab === "repositories", + }); const createRepoMutation = api.repositories.create.useMutation(); const updateRepoMutation = api.repositories.update.useMutation(); const deleteRepoMutation = api.repositories.delete.useMutation(); @@ -84,13 +104,13 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const loadGithubToken = async () => { setIsLoading(true); try { - const response = await fetch('/api/settings/github-token'); + const response = await fetch("/api/settings/github-token"); if (response.ok) { const data = await response.json(); - setGithubToken((data.token as string) ?? ''); + setGithubToken((data.token as string) ?? ""); } } catch (error) { - console.error('Error loading GitHub token:', error); + console.error("Error loading GitHub token:", error); } finally { setIsLoading(false); } @@ -98,94 +118,106 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const loadSaveFilter = async () => { try { - const response = await fetch('/api/settings/save-filter'); + const response = await fetch("/api/settings/save-filter"); if (response.ok) { const data = await response.json(); setSaveFilter((data.enabled as boolean) ?? false); } } catch (error) { - console.error('Error loading save filter setting:', error); + console.error("Error loading save filter setting:", error); } }; const saveSaveFilter = async (enabled: boolean) => { try { - const response = await fetch('/api/settings/save-filter', { - method: 'POST', + const response = await fetch("/api/settings/save-filter", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ enabled }), }); if (response.ok) { setSaveFilter(enabled); - setMessage({ type: 'success', text: 'Save filter setting updated!' }); - + setMessage({ type: "success", text: "Save filter setting updated!" }); + // If disabling save filters, clear saved filters if (!enabled) { await clearSavedFilters(); } } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to save setting' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to save setting", + }); } } catch { - setMessage({ type: 'error', text: 'Failed to save setting' }); + setMessage({ type: "error", text: "Failed to save setting" }); } }; const loadSavedFilters = async () => { try { - const response = await fetch('/api/settings/filters'); + const response = await fetch("/api/settings/filters"); if (response.ok) { const data = await response.json(); setSavedFilters(data.filters); } } catch (error) { - console.error('Error loading saved filters:', error); + console.error("Error loading saved filters:", error); } }; const clearSavedFilters = async () => { try { - const response = await fetch('/api/settings/filters', { - method: 'DELETE', + const response = await fetch("/api/settings/filters", { + method: "DELETE", }); if (response.ok) { setSavedFilters(null); - setMessage({ type: 'success', text: 'Saved filters cleared!' }); + setMessage({ type: "success", text: "Saved filters cleared!" }); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to clear filters' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to clear filters", + }); } } catch { - setMessage({ type: 'error', text: 'Failed to clear filters' }); + setMessage({ type: "error", text: "Failed to clear filters" }); } }; const saveGithubToken = async () => { setIsSaving(true); setMessage(null); - + try { - const response = await fetch('/api/settings/github-token', { - method: 'POST', + const response = await fetch("/api/settings/github-token", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ token: githubToken }), }); if (response.ok) { - setMessage({ type: 'success', text: 'GitHub token saved successfully!' }); + setMessage({ + type: "success", + text: "GitHub token saved successfully!", + }); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to save token' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to save token", + }); } } catch { - setMessage({ type: 'error', text: 'Failed to save token' }); + setMessage({ type: "error", text: "Failed to save token" }); } finally { setIsSaving(false); } @@ -193,37 +225,46 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const loadColorCodingSetting = async () => { try { - const response = await fetch('/api/settings/color-coding'); + const response = await fetch("/api/settings/color-coding"); if (response.ok) { const data = await response.json(); setColorCodingEnabled(Boolean(data.enabled)); } } catch (error) { - console.error('Error loading color coding setting:', error); + console.error("Error loading color coding setting:", error); } }; const saveColorCodingSetting = async (enabled: boolean) => { try { - const response = await fetch('/api/settings/color-coding', { - method: 'POST', + const response = await fetch("/api/settings/color-coding", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ enabled }), }); if (response.ok) { setColorCodingEnabled(enabled); - setMessage({ type: 'success', text: 'Color coding setting saved successfully' }); + setMessage({ + type: "success", + text: "Color coding setting saved successfully", + }); setTimeout(() => setMessage(null), 3000); } else { - setMessage({ type: 'error', text: 'Failed to save color coding setting' }); + setMessage({ + type: "error", + text: "Failed to save color coding setting", + }); setTimeout(() => setMessage(null), 3000); } } catch (error) { - console.error('Error saving color coding setting:', error); - setMessage({ type: 'error', text: 'Failed to save color coding setting' }); + console.error("Error saving color coding setting:", error); + setMessage({ + type: "error", + text: "Failed to save color coding setting", + }); setTimeout(() => setMessage(null), 3000); } }; @@ -231,17 +272,23 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const loadAuthCredentials = async () => { setAuthLoading(true); try { - const response = await fetch('/api/settings/auth-credentials'); + const response = await fetch("/api/settings/auth-credentials"); if (response.ok) { - const data = await response.json() as { username: string; enabled: boolean; hasCredentials: boolean; setupCompleted: boolean; sessionDurationDays?: number }; - setAuthUsername(data.username ?? ''); + const data = (await response.json()) as { + username: string; + enabled: boolean; + hasCredentials: boolean; + setupCompleted: boolean; + sessionDurationDays?: number; + }; + setAuthUsername(data.username ?? ""); setAuthEnabled(data.enabled ?? false); setAuthHasCredentials(data.hasCredentials ?? false); setAuthSetupCompleted(data.setupCompleted ?? false); setSessionDurationDays(data.sessionDurationDays ?? 7); } } catch (error) { - console.error('Error loading auth credentials:', error); + console.error("Error loading auth credentials:", error); } finally { setAuthLoading(false); } @@ -249,35 +296,39 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr // Format expiration time display const formatExpirationTime = (expTime: number | null): string => { - if (!expTime) return 'No active session'; - + if (!expTime) return "No active session"; + const now = Date.now(); const timeUntilExpiration = expTime - now; - + if (timeUntilExpiration <= 0) { - return 'Session expired'; + return "Session expired"; } - + const days = Math.floor(timeUntilExpiration / (1000 * 60 * 60 * 24)); - const hours = Math.floor((timeUntilExpiration % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((timeUntilExpiration % (1000 * 60 * 60)) / (1000 * 60)); - + const hours = Math.floor( + (timeUntilExpiration % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60), + ); + const minutes = Math.floor( + (timeUntilExpiration % (1000 * 60 * 60)) / (1000 * 60), + ); + const parts: string[] = []; if (days > 0) { - parts.push(`${days} ${days === 1 ? 'day' : 'days'}`); + parts.push(`${days} ${days === 1 ? "day" : "days"}`); } if (hours > 0) { - parts.push(`${hours} ${hours === 1 ? 'hour' : 'hours'}`); + parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`); } if (minutes > 0 && days === 0) { - parts.push(`${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`); + parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`); } - + if (parts.length === 0) { - return 'Less than a minute'; + return "Less than a minute"; } - - return parts.join(', '); + + return parts.join(", "); }; // Update expiration display periodically @@ -286,58 +337,64 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr if (expirationTime) { setSessionExpirationDisplay(formatExpirationTime(expirationTime)); } else { - setSessionExpirationDisplay(''); + setSessionExpirationDisplay(""); } }; updateExpirationDisplay(); - + // Update every minute const interval = setInterval(updateExpirationDisplay, 60000); - + return () => clearInterval(interval); }, [expirationTime]); // Refresh auth when tab changes to auth tab useEffect(() => { - if (activeTab === 'auth' && isOpen) { + if (activeTab === "auth" && isOpen) { void checkAuth(); } }, [activeTab, isOpen, checkAuth]); const saveAuthCredentials = async () => { if (authPassword !== authConfirmPassword) { - setMessage({ type: 'error', text: 'Passwords do not match' }); + setMessage({ type: "error", text: "Passwords do not match" }); return; } setAuthLoading(true); setMessage(null); - + try { - const response = await fetch('/api/settings/auth-credentials', { - method: 'POST', + const response = await fetch("/api/settings/auth-credentials", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - body: JSON.stringify({ - username: authUsername, + body: JSON.stringify({ + username: authUsername, password: authPassword, - enabled: authEnabled + enabled: authEnabled, }), }); if (response.ok) { - setMessage({ type: 'success', text: 'Authentication credentials updated successfully!' }); - setAuthPassword(''); - setAuthConfirmPassword(''); + setMessage({ + type: "success", + text: "Authentication credentials updated successfully!", + }); + setAuthPassword(""); + setAuthConfirmPassword(""); void loadAuthCredentials(); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to save credentials' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to save credentials", + }); } } catch { - setMessage({ type: 'error', text: 'Failed to save credentials' }); + setMessage({ type: "error", text: "Failed to save credentials" }); } finally { setAuthLoading(false); } @@ -345,33 +402,42 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const saveSessionDuration = async (days: number) => { if (days < 1 || days > 365) { - setMessage({ type: 'error', text: 'Session duration must be between 1 and 365 days' }); + setMessage({ + type: "error", + text: "Session duration must be between 1 and 365 days", + }); return; } setAuthLoading(true); setMessage(null); - + try { - const response = await fetch('/api/settings/auth-credentials', { - method: 'PATCH', + const response = await fetch("/api/settings/auth-credentials", { + method: "PATCH", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ sessionDurationDays: days }), }); if (response.ok) { - setMessage({ type: 'success', text: `Session duration updated to ${days} days` }); + setMessage({ + type: "success", + text: `Session duration updated to ${days} days`, + }); setSessionDurationDays(days); setTimeout(() => setMessage(null), 3000); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to update session duration' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to update session duration", + }); setTimeout(() => setMessage(null), 3000); } } catch { - setMessage({ type: 'error', text: 'Failed to update session duration' }); + setMessage({ type: "error", text: "Failed to update session duration" }); setTimeout(() => setMessage(null), 3000); } finally { setAuthLoading(false); @@ -381,28 +447,31 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const toggleAuthEnabled = async (enabled: boolean) => { setAuthLoading(true); setMessage(null); - + try { - const response = await fetch('/api/settings/auth-credentials', { - method: 'PATCH', + const response = await fetch("/api/settings/auth-credentials", { + method: "PATCH", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ enabled }), }); if (response.ok) { setAuthEnabled(enabled); - setMessage({ - type: 'success', - text: `Authentication ${enabled ? 'enabled' : 'disabled'} successfully!` + setMessage({ + type: "success", + text: `Authentication ${enabled ? "enabled" : "disabled"} successfully!`, }); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to update auth status' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to update auth status", + }); } } catch { - setMessage({ type: 'error', text: 'Failed to update auth status' }); + setMessage({ type: "error", text: "Failed to update auth status" }); } finally { setAuthLoading(false); } @@ -411,38 +480,38 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr // Auto-sync functions const loadAutoSyncSettings = async () => { try { - const response = await fetch('/api/settings/auto-sync'); + const response = await fetch("/api/settings/auto-sync"); if (response.ok) { - const data = await response.json() as { settings: any }; + const data = (await response.json()) as { settings: any }; const settings = data.settings; if (settings) { setAutoSyncEnabled(settings.autoSyncEnabled ?? false); - setSyncIntervalType(settings.syncIntervalType ?? 'predefined'); - setSyncIntervalPredefined(settings.syncIntervalPredefined ?? '1hour'); - setSyncIntervalCron(settings.syncIntervalCron ?? ''); + setSyncIntervalType(settings.syncIntervalType ?? "predefined"); + setSyncIntervalPredefined(settings.syncIntervalPredefined ?? "1hour"); + setSyncIntervalCron(settings.syncIntervalCron ?? ""); setAutoDownloadNew(settings.autoDownloadNew ?? false); setAutoUpdateExisting(settings.autoUpdateExisting ?? false); setNotificationEnabled(settings.notificationEnabled ?? false); setAppriseUrls(settings.appriseUrls ?? []); - setAppriseUrlsText((settings.appriseUrls ?? []).join('\n')); - setLastAutoSync(settings.lastAutoSync ?? ''); + setAppriseUrlsText((settings.appriseUrls ?? []).join("\n")); + setLastAutoSync(settings.lastAutoSync ?? ""); setLastAutoSyncError(settings.lastAutoSyncError ?? null); setLastAutoSyncErrorTime(settings.lastAutoSyncErrorTime ?? null); } } } catch (error) { - console.error('Error loading auto-sync settings:', error); + console.error("Error loading auto-sync settings:", error); } }; const saveAutoSyncSettings = async () => { setIsSaving(true); setMessage(null); - + try { - const response = await fetch('/api/settings/auto-sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const response = await fetch("/api/settings/auto-sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ autoSyncEnabled, syncIntervalType, @@ -451,20 +520,26 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr autoDownloadNew, autoUpdateExisting, notificationEnabled, - appriseUrls: appriseUrls - }) + appriseUrls: appriseUrls, + }), }); - + if (response.ok) { - setMessage({ type: 'success', text: 'Auto-sync settings saved successfully!' }); + setMessage({ + type: "success", + text: "Auto-sync settings saved successfully!", + }); setTimeout(() => setMessage(null), 3000); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to save auto-sync settings' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to save auto-sync settings", + }); } } catch (error) { - console.error('Error saving auto-sync settings:', error); - setMessage({ type: 'error', text: 'Failed to save auto-sync settings' }); + console.error("Error saving auto-sync settings:", error); + setMessage({ type: "error", text: "Failed to save auto-sync settings" }); } finally { setIsSaving(false); } @@ -472,26 +547,27 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const handleAppriseUrlsChange = (text: string) => { setAppriseUrlsText(text); - const urls = text.split('\n').filter(url => url.trim() !== ''); + const urls = text.split("\n").filter((url) => url.trim() !== ""); setAppriseUrls(urls); }; const validateCronExpression = (cron: string) => { if (!cron.trim()) { - setCronValidationError(''); + setCronValidationError(""); return true; } - + // Basic cron validation - you might want to use a library like cron-validator - const cronRegex = /^(\*|([0-5]?\d)) (\*|([01]?\d|2[0-3])) (\*|([012]?\d|3[01])) (\*|([0]?\d|1[0-2])) (\*|([0-6]))$/; + const cronRegex = + /^(\*|([0-5]?\d)) (\*|([01]?\d|2[0-3])) (\*|([012]?\d|3[01])) (\*|([0]?\d|1[0-2])) (\*|([0-6]))$/; const isValid = cronRegex.test(cron); - + if (!isValid) { - setCronValidationError('Invalid cron expression format'); + setCronValidationError("Invalid cron expression format"); return false; } - - setCronValidationError(''); + + setCronValidationError(""); return true; }; @@ -502,56 +578,73 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const testNotification = async () => { try { - const response = await fetch('/api/settings/auto-sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ testNotification: true }) + const response = await fetch("/api/settings/auto-sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ testNotification: true }), }); - + if (response.ok) { - setMessage({ type: 'success', text: 'Test notification sent successfully!' }); + setMessage({ + type: "success", + text: "Test notification sent successfully!", + }); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to send test notification' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to send test notification", + }); } } catch (error) { - console.error('Error sending test notification:', error); - setMessage({ type: 'error', text: 'Failed to send test notification' }); + console.error("Error sending test notification:", error); + setMessage({ type: "error", text: "Failed to send test notification" }); } }; const triggerManualSync = async () => { try { - const response = await fetch('/api/settings/auto-sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ triggerManualSync: true }) + const response = await fetch("/api/settings/auto-sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ triggerManualSync: true }), }); - + if (response.ok) { - setMessage({ type: 'success', text: 'Manual sync triggered successfully!' }); + setMessage({ + type: "success", + text: "Manual sync triggered successfully!", + }); // Reload settings to get updated last sync time await loadAutoSyncSettings(); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to trigger manual sync' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to trigger manual sync", + }); } } catch (error) { - console.error('Error triggering manual sync:', error); - setMessage({ type: 'error', text: 'Failed to trigger manual sync' }); + console.error("Error triggering manual sync:", error); + setMessage({ type: "error", text: "Failed to trigger manual sync" }); } }; if (!isOpen) return null; return ( -
-
+
+
{/* Header */} -
+
-

Settings

- +

+ Settings +

+
{/* Tabs */} -
-