fix(gps): отсев устаревших копий и LBS-телепортов (stale/jump фильтры), настройка maxJumpSpeedKmh, фильтрация трека при выдаче

This commit is contained in:
2026-08-25 17:49:38 +03:00
parent b635d1e16e
commit 3d24a15fb3
8 changed files with 189 additions and 38 deletions

View File

@@ -332,6 +332,12 @@ export function GpsRoutesTab() {
{trackData?.sampled && ` (всего ${trackData.total}, показана каждая N-я)`}
{pointsThinned && `, маркеров на карте: ${displayedPoints.length}`}
</span>
{(trackData?.filteredCount ?? 0) > 0 && (
<span>
Отфильтровано мусорных точек:{' '}
<span className="text-foreground font-medium">{trackData!.filteredCount}</span>
</span>
)}
<span>
Пробег: <span className="text-foreground font-medium">{stats.distance.toFixed(1)} км</span>
</span>

View File

@@ -11,12 +11,13 @@ import { useToast } from '@/hooks/use-toast';
interface GpsSettings {
success: boolean;
jitterMeters: number;
maxJumpSpeedKmh: number;
}
/**
* Таб «Настройки» GPS-модуля.
* Секция «Трекинг»: фильтр дрожания позиции (джиттер) — сервер не записывает
* точки ближе N метров при скорости < 5 км/ч. Сюда же позже добавятся
* Секция «Трекинг»: фильтры входящих точек — дрожание позиции (джиттер)
* и максимальный скачок скорости (LBS-прыжки). Сюда же позже добавятся
* другие настройки модуля — структура секций-Card под это готова.
*/
export function GpsSettingsTab() {
@@ -26,15 +27,22 @@ export function GpsSettingsTab() {
});
const [jitterMeters, setJitterMeters] = useState('0');
const [maxJumpSpeedKmh, setMaxJumpSpeedKmh] = useState('0');
useEffect(() => {
if (data) setJitterMeters(String(data.jitterMeters));
if (data) {
setJitterMeters(String(data.jitterMeters));
setMaxJumpSpeedKmh(String(data.maxJumpSpeedKmh));
}
}, [data]);
const saveMutation = useMutation({
mutationFn: async () => {
const value = Number(jitterMeters);
const res = await apiRequest('PUT', '/api/gps/settings', { jitterMeters: value });
// Оба поля сохраняются одним PUT (непереданные на сервере сохраняются)
const res = await apiRequest('PUT', '/api/gps/settings', {
jitterMeters: Number(jitterMeters),
maxJumpSpeedKmh: Number(maxJumpSpeedKmh),
});
return res.json();
},
onSuccess: () => {
@@ -49,7 +57,9 @@ export function GpsSettingsTab() {
},
});
const jitterValid = /^\d+$/.test(jitterMeters.trim()) && Number(jitterMeters) >= 0 && Number(jitterMeters) <= 1000;
const isIntInRange = (v: string, max: number) => /^\d+$/.test(v.trim()) && Number(v) >= 0 && Number(v) <= max;
const jitterValid = isIntInRange(jitterMeters, 1000);
const jumpValid = isIntInRange(maxJumpSpeedKmh, 5000);
return (
<div className="max-w-2xl space-y-4">
@@ -84,10 +94,29 @@ export function GpsSettingsTab() {
<p className="text-xs text-destructive">Целое число от 0 до 1000</p>
)}
</div>
<div className="space-y-1">
<Label className="text-xs">Максимальный скачок, км/ч</Label>
<Input
type="text"
inputMode="numeric"
className="h-8 w-32"
value={maxJumpSpeedKmh}
onChange={(e) => setMaxJumpSpeedKmh(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
0 = выключен. Точки, требующие перемещения быстрее N км/ч от предыдущей,
отбрасываются — отсекает LBS-прыжки на вышки при потере GPS. Действует на новые точки.
</p>
{!jumpValid && (
<p className="text-xs text-destructive">Целое число от 0 до 5000</p>
)}
</div>
<Button
size="sm"
className="h-8"
disabled={saveMutation.isPending || !jitterValid}
disabled={saveMutation.isPending || !jitterValid || !jumpValid}
onClick={() => saveMutation.mutate()}
>
{saveMutation.isPending ? (

View File

@@ -64,6 +64,8 @@ export interface GpsTrackResponse {
success: boolean;
points: GpsTrackPoint[];
total: number;
/** Число отброшенных сервером устаревших/телепорт-точек */
filteredCount?: number;
sampled: boolean;
}