diff --git a/src/interfaces/Job.ts b/src/interfaces/Job.ts index a89c65b89..401e50026 100644 --- a/src/interfaces/Job.ts +++ b/src/interfaces/Job.ts @@ -17,7 +17,14 @@ export interface Workload { // Parameters for bundle job export interface BundleJobParameters { anonymize: boolean; + // Anonymization level applied when anonymize is enabled. "default" keeps + // internal IP ranges readable; "strict" also anonymizes them. Omitted or + // empty resolves to the default on the peer. + anonymize_level?: "default" | "strict"; bundle_for: boolean; bundle_for_time: number; log_file_count: number; + // Upload service URL the peer requests an upload URL from. Empty selects the + // default upload server. + upload_url?: string; } diff --git a/src/modules/jobs/CreateDebugJobModal.tsx b/src/modules/jobs/CreateDebugJobModal.tsx index 683e05734..c45436f58 100644 --- a/src/modules/jobs/CreateDebugJobModal.tsx +++ b/src/modules/jobs/CreateDebugJobModal.tsx @@ -4,6 +4,7 @@ import { FileText, PlusCircle, Shield, + UploadCloud, } from "lucide-react"; import { useMemo, useState } from "react"; import { useSWRConfig } from "swr"; @@ -19,6 +20,13 @@ import { } from "@/components/modal/Modal"; import ModalHeader from "@/components/modal/ModalHeader"; import { notify } from "@/components/Notification"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/Select"; import Separator from "@/components/Separator"; import { Workload } from "@/interfaces/Job"; import { useApiCall } from "@/utils/api"; @@ -36,10 +44,15 @@ export function CreateDebugJobModalContent({ peerID, onSuccess }: Props) { const [bundleForTime, setBundleForTime] = useState(""); const [logFileCount, setLogFileCount] = useState("10"); const [anonymize, setAnonymize] = useState(false); + const [anonymizeLevel, setAnonymizeLevel] = useState<"default" | "strict">( + "default", + ); + const [uploadUrl, setUploadUrl] = useState(""); const isValid = useMemo(() => { let validBundleFor = true; let validLogFileCount = true; + let validUploadUrl = true; const logFileCountNumber = Number(logFileCount); const bundleForTimeNumber = Number(bundleForTime); @@ -50,8 +63,18 @@ export function CreateDebugJobModalContent({ peerID, onSuccess }: Props) { validLogFileCount = logFileCountNumber >= 1 && logFileCountNumber <= 1000; - return validLogFileCount && validBundleFor; - }, [bundleForTime, logFileCount]); + const trimmedUploadUrl = uploadUrl.trim(); + if (trimmedUploadUrl) { + try { + const parsed = new URL(trimmedUploadUrl); + validUploadUrl = parsed.protocol === "https:" && parsed.host !== ""; + } catch { + validUploadUrl = false; + } + } + + return validLogFileCount && validBundleFor && validUploadUrl; + }, [bundleForTime, logFileCount, uploadUrl]); const createDebugJob = async () => { notify({ @@ -64,11 +87,13 @@ export function CreateDebugJobModalContent({ peerID, onSuccess }: Props) { type: "bundle", parameters: { anonymize, + anonymize_level: anonymize ? anonymizeLevel : undefined, bundle_for: bundleForTimeEnabled, bundle_for_time: bundleForTimeEnabled ? Number(bundleForTime) : undefined, log_file_count: logFileCount ? Number(logFileCount) : 10, + upload_url: uploadUrl.trim() ? uploadUrl.trim() : undefined, }, }, }) @@ -172,6 +197,55 @@ export function CreateDebugJobModalContent({ peerID, onSuccess }: Props) { } helpText="Remove sensitive information (IP addresses, domains etc.) before creating the debug bundle." /> + + {/* Anonymization Level */} + {anonymize && ( +
+
+ + + Default keeps internal (private) IP ranges readable; Strict also + anonymizes private, CGNAT and link-local addresses. + +
+ + +
+ )} + + {/* Upload URL */} +
+
+ + + Service the peer requests an upload URL from. Leave empty to use + the default upload server. Must be an https URL. + +
+ + setUploadUrl(e.target.value)} + maxWidthClass="w-[220px]" + customPrefix={} + /> +