diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json
index 5cdc587dc..e91f2d29c 100644
--- a/web/public/locales/en/views/settings.json
+++ b/web/public/locales/en/views/settings.json
@@ -204,12 +204,21 @@
"detectionMethodDescription": "Probe the camera with ONVIF (if supported) to find camera stream URLs, or manually select the camera brand to use pre-defined URLs. To enter a custom RTSP URL, choose the manual method and select \"Other\".",
"onvifPortDescription": "For cameras that support ONVIF, this is usually 80 or 8080.",
"probingDevice": "Probing device...",
+ "probeSuccessful": "Probe successful",
"probeError": "Probe Error",
"probeNoSuccess": "Probe unsuccessful",
+ "manufacturer": "Manufacturer",
+ "model": "Model",
+ "firmware": "Firmware",
+ "profiles": "Profiles",
+ "ptzSupport": "PTZ Support",
+ "autotrackingSupport": "Autotracking Support",
+ "presets": "Presets",
"deviceInfo": "Device Information",
"rtspCandidates": "RTSP Candidates",
+ "candidateStreamTitle": "Candidate {{number}}",
"useCandidate": "Use",
- "uriCopied": "URI copied",
+ "uriCopy": "Copy",
"probeFailed": "Failed to probe camera: {{error}}",
"probeButton": "Probe camera",
"warnings": {
diff --git a/web/src/components/settings/wizard/OnvifProbeResults.tsx b/web/src/components/settings/wizard/OnvifProbeResults.tsx
index 9ad3809fb..cf17b7204 100644
--- a/web/src/components/settings/wizard/OnvifProbeResults.tsx
+++ b/web/src/components/settings/wizard/OnvifProbeResults.tsx
@@ -1,8 +1,12 @@
import { useTranslation } from "react-i18next";
import { Card, CardContent, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
+// Input removed: URL shown as text
import ActivityIndicator from "@/components/indicators/activity-indicator";
-import { FaExclamationCircle, FaCopy, FaCheck } from "react-icons/fa";
+import { FaCopy, FaCheck } from "react-icons/fa";
+import { LuX } from "react-icons/lu";
+import { CiCircleAlert } from "react-icons/ci";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { useState } from "react";
import { toast } from "sonner";
import type {
@@ -11,6 +15,8 @@ import type {
TestResult,
CandidateTestMap,
} from "@/types/cameraWizard";
+import { FaCircleCheck } from "react-icons/fa6";
+import { cn } from "@/lib/utils";
type OnvifProbeResultsProps = {
isLoading: boolean;
@@ -61,15 +67,11 @@ export default function OnvifProbeResults({
if (isError) {
return (
-
-
-
-
- {t("cameraWizard.step1.probeError")}
-
- {error &&
{error}
}
-
-
+
+
+ {t("cameraWizard.step1.probeError")}
+ {error && {error}}
+
@@ -80,19 +82,13 @@ export default function OnvifProbeResults({
if (!probeResult?.success) {
return (
-
-
-
-
- {t("cameraWizard.step1.probeNoSuccess")}
-
- {probeResult?.message && (
-
- {probeResult.message}
-
- )}
-
-
+
+
+ {t("cameraWizard.step1.probeNoSuccess")}
+ {probeResult?.message && (
+ {probeResult.message}
+ )}
+
@@ -104,6 +100,13 @@ export default function OnvifProbeResults({
return (
+ {/* Probe success header (green check + text) */}
+ {probeResult?.success && (
+
+
+ {t("cameraWizard.step1.probeSuccessful")}
+
+ )}
{t("cameraWizard.step1.deviceInfo")}
@@ -111,49 +114,73 @@ export default function OnvifProbeResults({
{probeResult.manufacturer && (
- Manufacturer:{" "}
- {probeResult.manufacturer}
+
+ {t("cameraWizard.step1.manufacturer")}:
+ {" "}
+
+ {probeResult.manufacturer}
+
)}
{probeResult.model && (
- Model:{" "}
- {probeResult.model}
+
+ {t("cameraWizard.step1.model")}:
+ {" "}
+ {probeResult.model}
)}
{probeResult.firmware_version && (
- Firmware:{" "}
-
+
+ {t("cameraWizard.step1.firmware")}:
+ {" "}
+
{probeResult.firmware_version}
)}
{probeResult.profiles_count !== undefined && (
- Profiles:{" "}
- {probeResult.profiles_count}
+
+ {t("cameraWizard.step1.profiles")}:
+ {" "}
+
+ {probeResult.profiles_count}
+
)}
{probeResult.ptz_supported !== undefined && (
- PTZ Support:{" "}
-
- {probeResult.ptz_supported ? "Yes" : "No"}
+
+ {t("cameraWizard.step1.ptzSupport")}:
+ {" "}
+
+ {probeResult.ptz_supported
+ ? t("yes", { ns: "common" })
+ : t("no", { ns: "common" })}
)}
{probeResult.ptz_supported && probeResult.autotrack_supported && (
- Autotrack Support:{" "}
- Yes
+
+ {t("cameraWizard.step1.autotrackingSupport")}:
+ {" "}
+
+ {t("yes", { ns: "common" })}
+
)}
{probeResult.ptz_supported &&
probeResult.presets_count !== undefined && (
- Presets:{" "}
- {probeResult.presets_count}
+
+ {t("cameraWizard.step1.presets")}:
+ {" "}
+
+ {probeResult.presets_count}
+
)}
@@ -161,9 +188,7 @@ export default function OnvifProbeResults({
{rtspCandidates.length > 0 && (
-
- {t("cameraWizard.step1.rtspCandidates")}
-
+
{t("cameraWizard.step1.rtspCandidates")}
{rtspCandidates.map((candidate, idx) => {
@@ -174,12 +199,13 @@ export default function OnvifProbeResults({
return (
handleCopyUri(candidate.uri)}
onUse={() => onSelectCandidate(candidate.uri)}
isSelected={isSelected}
- onTest={() => testCandidate && testCandidate(candidate.uri)}
+ testCandidate={testCandidate}
candidateTest={candidateTest}
isTesting={isTesting}
/>
@@ -194,129 +220,142 @@ export default function OnvifProbeResults({
type CandidateItemProps = {
candidate: OnvifRtspCandidate;
- isTested?: boolean;
+ index?: number;
+ // isTested?: boolean; (unused)
copiedUri: string | null;
onCopy: () => void;
onUse: () => void;
isSelected?: boolean;
- onTest?: () => void;
+ // onTest?: () => void; (unused)
+ testCandidate?: (uri: string) => void;
candidateTest?: TestResult | { success: false; error: string };
isTesting?: boolean;
};
function CandidateItem({
+ index,
candidate,
- isTested,
copiedUri,
onCopy,
onUse,
isSelected,
- onTest,
+ testCandidate,
candidateTest,
isTesting,
}: CandidateItemProps) {
const { t } = useTranslation(["views/settings"]);
const [showFull, setShowFull] = useState(false);
- // Mask credentials for display
const maskUri = (uri: string) => {
const match = uri.match(/rtsp:\/\/([^:]+):([^@]+)@(.+)/);
- if (match) {
- return `rtsp://${match[1]}:••••@${match[3]}`;
- }
+ if (match) return `rtsp://${match[1]}:••••@${match[3]}`;
return uri;
};
return (
-
-
-
-
- {isTested !== undefined && (
-
- {isTested ? "✓ OK" : "✗ Failed"}
-
- )}
-
-
-
setShowFull(!showFull)}
- title="Click to toggle masked/full view"
- >
- {showFull ? candidate.uri : maskUri(candidate.uri)}
-
-
-
+
+
+
+
+ {t("cameraWizard.step1.candidateStreamTitle", {
+ number: (index ?? 0) + 1,
+ })}
+
+ {candidateTest?.success && (
+
+ {[
+ candidateTest.resolution,
+ candidateTest.fps
+ ? `${candidateTest.fps} ${t(
+ "cameraWizard.testResultLabels.fps",
+ )}`
+ : null,
+ candidateTest.videoCodec,
+ candidateTest.audioCodec,
+ ]
+ .filter(Boolean)
+ .join(" · ")}
+
+ )}
+
+ {candidateTest?.success && (
+
+
+
+ {t("cameraWizard.step2.connected")}
+
+
+ )}
+
+ {candidateTest && !candidateTest.success && (
+
+
+
+ {t("cameraWizard.step2.notConnected")}
+
+
+ )}
+
+
+
+
+
setShowFull((s) => !s)}
+ title={t("cameraWizard.step1.toggleUriView")}
+ >
+ {showFull ? candidate.uri : maskUri(candidate.uri)}
+
+
+
+
+
-
- {candidateTest && candidateTest.success && (
-
- {candidateTest.resolution && (
-
- {`${t("cameraWizard.testResultLabels.resolution")} ${candidateTest.resolution}`}
-
- )}
- {candidateTest.fps && (
-
- {t("cameraWizard.testResultLabels.fps")} {candidateTest.fps}
-
- )}
- {candidateTest.videoCodec && (
-
- {`${t("cameraWizard.testResultLabels.video")} ${candidateTest.videoCodec}`}
-
- )}
- {candidateTest.audioCodec && (
-
- {`${t("cameraWizard.testResultLabels.audio")} ${candidateTest.audioCodec}`}
-
- )}
-
- )}
+
+
+
+
);
diff --git a/web/src/components/settings/wizard/Step1NameCamera.tsx b/web/src/components/settings/wizard/Step1NameCamera.tsx
index 9d76cf3c3..b4b4928f0 100644
--- a/web/src/components/settings/wizard/Step1NameCamera.tsx
+++ b/web/src/components/settings/wizard/Step1NameCamera.tsx
@@ -458,7 +458,13 @@ export default function Step1NameCamera({
if (streamConfigs.length > 0) {
// Add all successful streams and navigate to Step 2
- onNext({ streams: streamConfigs, customUrl: firstSuccessfulUri });
+ const values = form.getValues();
+ const cameraName = values.cameraName;
+ onNext({
+ cameraName,
+ streams: streamConfigs,
+ customUrl: firstSuccessfulUri,
+ });
toast.success(t("cameraWizard.step1.testSuccess"));
setProbeDialogOpen(false);
setProbeResult(null);
@@ -490,7 +496,7 @@ export default function Step1NameCamera({
setIsTesting(false);
setTestStatus("");
}
- }, [selectedCandidateUris, t, onNext, probeUri]);
+ }, [selectedCandidateUris, t, onNext, probeUri, form]);
const testCandidate = useCallback(
async (uri: string) => {