diff --git a/tradein-mvp/frontend/src/app/error.tsx b/tradein-mvp/frontend/src/app/error.tsx
new file mode 100644
index 00000000..9c4caa65
--- /dev/null
+++ b/tradein-mvp/frontend/src/app/error.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+/**
+ * App Router error boundary for the trade-in page (Next 15 idiom).
+ *
+ * Triggers when a component below `app/page.tsx` throws during render or
+ * inside an Effect. Without this, the page would white-screen. Closes
+ * finding #16 from 2026-05-24 trade-in audit.
+ */
+import { useEffect } from "react";
+
+interface Props {
+ error: Error & { digest?: string };
+ reset: () => void;
+}
+
+export default function Error({ error, reset }: Props) {
+ useEffect(() => {
+ // Surfaces to browser devtools + (if wired) Sentry.
+ console.error("trade-in page error:", error);
+ }, [error]);
+
+ return (
+
+
+ Что-то пошло не так
+
+
+ Произошла ошибка при отображении страницы. Можно попробовать ещё раз —
+ если повторяется, перезагрузите страницу.
+
+ {error.digest && (
+
+ ID ошибки: {error.digest}
+
+ )}
+
+
+ );
+}
diff --git a/tradein-mvp/frontend/src/components/trade-in/ListingsCard.tsx b/tradein-mvp/frontend/src/components/trade-in/ListingsCard.tsx
index 78acd6ae..441b6b0b 100644
--- a/tradein-mvp/frontend/src/components/trade-in/ListingsCard.tsx
+++ b/tradein-mvp/frontend/src/components/trade-in/ListingsCard.tsx
@@ -5,6 +5,7 @@
* Counts strip + filters + price bar + table.
*/
import type { AggregatedEstimate, AnalogLot } from "@/types/trade-in";
+import { safeUrl } from "@/lib/safeUrl";
interface Props {
estimate: AggregatedEstimate;
@@ -243,13 +244,15 @@ function AnalogRow({ lot }: { lot: AnalogLot }) {
const dot = SOURCE_DOTS[src] ?? "dom";
const label = SOURCE_LABELS[src] ?? src;
const distance =
- lot.distance_m === null ? "—" : lot.distance_m < 1000 ? `${lot.distance_m} м` : `${(lot.distance_m / 1000).toFixed(1)} км`;
- const onClick = lot.source_url
- ? () => window.open(lot.source_url!, "_blank", "noopener,noreferrer")
- : undefined;
+ lot.distance_m === null
+ ? "—"
+ : lot.distance_m < 1000
+ ? `${lot.distance_m} м`
+ : `${(lot.distance_m / 1000).toFixed(1)} км`;
+ const externalUrl = safeUrl(lot.source_url);
return (
-
+
{lot.address}
@@ -270,13 +273,12 @@ function AnalogRow({ lot }: { lot: AnalogLot }) {
{fmtRub(lot.price_rub)} |
{distance} |
- {lot.source_url && (
+ {externalUrl && (
e.stopPropagation()}
aria-label="Открыть оригинал"
>
| |