"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"; interface AutoSyncSettings { autoSyncEnabled: boolean; syncIntervalType: "predefined" | "custom"; syncIntervalPredefined: string; syncIntervalCron: string; autoDownloadNew: boolean; autoUpdateExisting: boolean; notificationEnabled: boolean; appriseUrls: string[]; lastAutoSync: string; lastAutoSyncError: string | null; lastAutoSyncErrorTime: string | null; } interface GeneralSettingsModalProps { isOpen: boolean; onClose: () => void; } 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 [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); // Auth state 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); const [authLoading, setAuthLoading] = useState(false); const [sessionDurationDays, setSessionDurationDays] = useState(7); // 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 [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< string | null >(null); const [cronValidationError, setCronValidationError] = useState(""); // Repository management state 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 createRepoMutation = api.repositories.create.useMutation(); const updateRepoMutation = api.repositories.update.useMutation(); const deleteRepoMutation = api.repositories.delete.useMutation(); // Load existing settings when modal opens useEffect(() => { if (isOpen) { void loadGithubToken(); void loadSaveFilter(); void loadSavedFilters(); void loadAuthCredentials(); void loadColorCodingSetting(); void loadAutoSyncSettings(); } }, [isOpen]); const loadGithubToken = async () => { setIsLoading(true); try { const response = await fetch("/api/settings/github-token"); if (response.ok) { const data = await response.json(); setGithubToken((data.token as string) ?? ""); } } catch (error) { console.error("Error loading GitHub token:", error); } finally { setIsLoading(false); } }; const loadSaveFilter = async () => { try { 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); } }; const saveSaveFilter = async (enabled: boolean) => { try { const response = await fetch("/api/settings/save-filter", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ enabled }), }); if (response.ok) { setSaveFilter(enabled); 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", }); } } catch { setMessage({ type: "error", text: "Failed to save setting" }); } }; const loadSavedFilters = async () => { try { 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); } }; const clearSavedFilters = async () => { try { const response = await fetch("/api/settings/filters", { method: "DELETE", }); if (response.ok) { setSavedFilters(null); setMessage({ type: "success", text: "Saved filters cleared!" }); } else { const errorData = await response.json(); setMessage({ type: "error", text: errorData.error ?? "Failed to clear filters", }); } } catch { 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", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ token: githubToken }), }); if (response.ok) { setMessage({ type: "success", text: "GitHub token saved successfully!", }); } else { const errorData = await response.json(); setMessage({ type: "error", text: errorData.error ?? "Failed to save token", }); } } catch { setMessage({ type: "error", text: "Failed to save token" }); } finally { setIsSaving(false); } }; const loadColorCodingSetting = async () => { try { 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); } }; const saveColorCodingSetting = async (enabled: boolean) => { try { const response = await fetch("/api/settings/color-coding", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ enabled }), }); if (response.ok) { setColorCodingEnabled(enabled); setMessage({ type: "success", text: "Color coding setting saved successfully", }); setTimeout(() => setMessage(null), 3000); } else { 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", }); setTimeout(() => setMessage(null), 3000); } }; const loadAuthCredentials = async () => { setAuthLoading(true); try { 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 ?? ""); 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); } finally { setAuthLoading(false); } }; // Format expiration time display const formatExpirationTime = (expTime: number | null): string => { if (!expTime) return "No active session"; const now = Date.now(); const timeUntilExpiration = expTime - now; if (timeUntilExpiration <= 0) { 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 parts: string[] = []; if (days > 0) { parts.push(`${days} ${days === 1 ? "day" : "days"}`); } if (hours > 0) { parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`); } if (minutes > 0 && days === 0) { parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`); } if (parts.length === 0) { return "Less than a minute"; } return parts.join(", "); }; // Update expiration display periodically useEffect(() => { const updateExpirationDisplay = () => { if (expirationTime) { setSessionExpirationDisplay(formatExpirationTime(expirationTime)); } else { 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) { void checkAuth(); } }, [activeTab, isOpen, checkAuth]); const saveAuthCredentials = async () => { if (authPassword !== authConfirmPassword) { setMessage({ type: "error", text: "Passwords do not match" }); return; } setAuthLoading(true); setMessage(null); try { const response = await fetch("/api/settings/auth-credentials", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username: authUsername, password: authPassword, enabled: authEnabled, }), }); if (response.ok) { 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", }); } } catch { setMessage({ type: "error", text: "Failed to save credentials" }); } finally { setAuthLoading(false); } }; const saveSessionDuration = async (days: number) => { if (days < 1 || days > 365) { 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", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ sessionDurationDays: days }), }); if (response.ok) { 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", }); setTimeout(() => setMessage(null), 3000); } } catch { setMessage({ type: "error", text: "Failed to update session duration" }); setTimeout(() => setMessage(null), 3000); } finally { setAuthLoading(false); } }; const toggleAuthEnabled = async (enabled: boolean) => { setAuthLoading(true); setMessage(null); try { const response = await fetch("/api/settings/auth-credentials", { method: "PATCH", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ enabled }), }); if (response.ok) { setAuthEnabled(enabled); 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", }); } } catch { setMessage({ type: "error", text: "Failed to update auth status" }); } finally { setAuthLoading(false); } }; // Auto-sync functions const loadAutoSyncSettings = async () => { try { const response = await fetch("/api/settings/auto-sync"); if (response.ok) { const data = (await response.json()) as { settings: AutoSyncSettings | null; }; const settings = data.settings; if (settings) { setAutoSyncEnabled(settings.autoSyncEnabled ?? false); 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 ?? ""); setLastAutoSyncError(settings.lastAutoSyncError ?? null); setLastAutoSyncErrorTime(settings.lastAutoSyncErrorTime ?? null); } } } catch (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" }, body: JSON.stringify({ autoSyncEnabled, syncIntervalType, syncIntervalPredefined, syncIntervalCron, autoDownloadNew, autoUpdateExisting, notificationEnabled, appriseUrls: appriseUrls, }), }); if (response.ok) { 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", }); } } catch (error) { console.error("Error saving auto-sync settings:", error); setMessage({ type: "error", text: "Failed to save auto-sync settings" }); } finally { setIsSaving(false); } }; const handleAppriseUrlsChange = (text: string) => { setAppriseUrlsText(text); const urls = text.split("\n").filter((url) => url.trim() !== ""); setAppriseUrls(urls); }; const validateCronExpression = (cron: string) => { if (!cron.trim()) { 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 isValid = cronRegex.test(cron); if (!isValid) { setCronValidationError("Invalid cron expression format"); return false; } setCronValidationError(""); return true; }; const handleCronChange = (cron: string) => { setSyncIntervalCron(cron); validateCronExpression(cron); }; const testNotification = async () => { try { 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!", }); } else { const errorData = await response.json(); 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" }); } }; const triggerManualSync = async () => { try { 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!", }); // 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", }); } } catch (error) { console.error("Error triggering manual sync:", error); setMessage({ type: "error", text: "Failed to trigger manual sync" }); } }; if (!isOpen) return null; return (
{/* Header */}

Settings

{/* Tabs */}
{/* Content */}
{activeTab === "general" && (

General Settings

Configure general application preferences and behavior.

Theme

Choose your preferred color theme for the application.

Current Theme

{theme === "light" ? "Light mode" : "Dark mode"}

Save Filters

Save your configured script filters.

{saveFilter && (

Saved Filters

{savedFilters ? "Filters are currently saved" : "No filters saved yet"}

{savedFilters && (
Search: {savedFilters.searchQuery ?? "None"}
Types: {savedFilters.selectedTypes?.length ?? 0}{" "} selected
Sort: {savedFilters.sortBy} ( {savedFilters.sortOrder})
)}
{savedFilters && ( )}
)}

Server Color Coding

Enable color coding for servers to visually distinguish them throughout the application.

)} {activeTab === "github" && (

GitHub Integration

Configure GitHub integration for script management and updates.

GitHub Personal Access Token

Save a GitHub Personal Access Token to circumvent GitHub API rate limits.

) => setGithubToken(e.target.value) } disabled={isLoading || isSaving} className="w-full" />
{message && (
{message.text}
)}
)} {activeTab === "auth" && (

Authentication Settings

Configure authentication to secure access to your application.

Authentication Status

{authSetupCompleted ? authHasCredentials ? `Authentication is ${authEnabled ? "enabled" : "disabled"}. Current username: ${authUsername}` : `Authentication is ${authEnabled ? "enabled" : "disabled"}. No credentials configured.` : "Authentication setup has not been completed yet."}

Enable Authentication

{authEnabled ? "Authentication is required on every page load" : "Authentication is optional"}

{isAuthenticated && expirationTime && (

Session Information

Session expires in:

{sessionExpirationDisplay}

Expiration date:

{new Date(expirationTime).toLocaleString()}

)}

Session Duration

Configure how long user sessions should last before requiring re-authentication.

, ) => { const value = parseInt(e.target.value, 10); if (!isNaN(value)) { setSessionDurationDays(value); } }} disabled={authLoading || !authSetupCompleted} className="w-32" /> days (1-365)

Note: This setting applies to new logins. Current sessions will not be affected.

Update Credentials

Change your username and password for authentication.

) => setAuthUsername(e.target.value) } disabled={authLoading} className="w-full" minLength={3} />
) => setAuthPassword(e.target.value) } disabled={authLoading} className="w-full" minLength={6} />
) => setAuthConfirmPassword(e.target.value) } disabled={authLoading} className="w-full" minLength={6} />
{message && (
{message.text}
)}
)} {activeTab === "auto-sync" && (

Auto-Sync Settings

Configure automatic synchronization of scripts with configurable intervals and notifications.

{/* Enable Auto-Sync */}

Enable Auto-Sync

Automatically sync JSON files from GitHub at specified intervals

{ setAutoSyncEnabled(checked); // Auto-save when toggle changes try { // If syncIntervalType is custom but no cron expression, fallback to predefined const effectiveSyncIntervalType = syncIntervalType === "custom" && !syncIntervalCron ? "predefined" : syncIntervalType; const response = await fetch( "/api/settings/auto-sync", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ autoSyncEnabled: checked, syncIntervalType: effectiveSyncIntervalType, syncIntervalPredefined: effectiveSyncIntervalType === "predefined" ? syncIntervalPredefined : undefined, syncIntervalCron: effectiveSyncIntervalType === "custom" ? syncIntervalCron : undefined, autoDownloadNew, autoUpdateExisting, notificationEnabled, appriseUrls: appriseUrls, }), }, ); if (response.ok) { // Update local state to reflect the effective sync interval type if ( effectiveSyncIntervalType !== syncIntervalType ) { setSyncIntervalType(effectiveSyncIntervalType); } } } catch (error) { console.error( "Error saving auto-sync toggle:", error, ); } }} disabled={isSaving} />
{/* Sync Interval */} {autoSyncEnabled && (

Sync Interval

{syncIntervalType === "predefined" && (
)} {syncIntervalType === "custom" && (
handleCronChange(e.target.value)} className="w-full" autoFocus onFocus={() => setCronValidationError("")} /> {cronValidationError && (

{cronValidationError}

)}

Format: minute hour day month weekday. See{" "} crontab.guru {" "} for examples

Common examples:

  • * * * * * - Every minute
  • 0 * * * * - Every hour
  • 0 */6 * * * - Every 6 hours
  • 0 0 * * * - Every day at midnight
  • 0 0 * * 0 - Every Sunday at midnight
)}
)} {/* Auto-Download Options */} {autoSyncEnabled && (

Auto-Download Options

Auto-download new scripts

Automatically download scripts that haven't been downloaded yet

Auto-update existing scripts

Automatically update scripts that have newer versions available

)} {/* Notifications */} {autoSyncEnabled && (

Enable Notifications

Send notifications when sync completes

If you want any other notification service, please open an issue on the GitHub repository.

{notificationEnabled && (