Железный Егорин — форк Орбиты: тёплый стиль (amber), логин с odyssey WebGL-молнией справа, энергомониторинг+панель

Ребренд Мониторинг Орбита -> Железный Егорин; accent green->amber #F59E0B; LoginScreen правая половина = Lightning (WebGL, hue 35 тёплый) под стеклянной формой; левая панель — тёплый градиент.
Доступ агентам: репо публичный, push german-токеном.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
German
2026-07-07 23:57:16 +03:00
co-authored by Claude Opus 4.8
commit b1c52ab748
114 changed files with 14340 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
import { useEffect, useState } from 'react';
import { Smartphone, Plus, Trash2, Info } from 'lucide-react';
import { PageHeader } from '@/components/PageHeader';
import { Card, CardHeader, Badge } from '@/components/ui/primitives';
import { apiGet, apiPost, apiDelete } from '@/api/client';
import { useAuth } from '@/core/store';
interface Device {
imei: string;
name: string;
}
export function DevicesModule() {
const user = useAuth((s) => s.user);
const [devices, setDevices] = useState<Device[]>([]);
const [imei, setImei] = useState('');
const [name, setName] = useState('');
const [err, setErr] = useState('');
const [busy, setBusy] = useState(false);
const refresh = () => apiGet<Device[]>('/devices').then(setDevices).catch(() => setDevices([]));
useEffect(() => {
refresh();
}, []);
const bind = async () => {
setErr('');
if (!/^\d{6,20}$/.test(imei.trim())) {
setErr('IMEI — только цифры (6–20 знаков)');
return;
}
setBusy(true);
try {
await apiPost('/devices/bind', { imei: imei.trim(), name: name.trim() || undefined });
setImei('');
setName('');
await refresh();
} catch {
setErr('Не удалось привязать устройство');
} finally {
setBusy(false);
}
};
const remove = async (i: string) => {
await apiDelete(`/devices/${i}`);
refresh();
};
return (
<div>
<PageHeader
icon={Smartphone}
title="Мои устройства"
desc="Привязка трекеров Navtelecom по IMEI к вашему кабинету"
actions={<Badge severity="info">кабинет: {user.clientId}</Badge>}
/>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<Card className="lg:col-span-2">
<CardHeader title="Привязанные трекеры" desc={`${devices.length} устройств(а)`} />
<div className="px-4 pb-4">
<div className="flex flex-col gap-2 sm:flex-row">
<input
value={imei}
onChange={(e) => setImei(e.target.value)}
placeholder="IMEI трекера"
inputMode="numeric"
className="tnum flex-1 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none placeholder:text-fg-muted focus:border-accent"
/>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Название (опционально)"
className="flex-1 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none placeholder:text-fg-muted focus:border-accent"
/>
<button
onClick={bind}
disabled={busy}
className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-accent px-4 py-2 text-sm font-semibold text-[#06210f] hover:opacity-90 disabled:opacity-50"
>
<Plus className="h-4 w-4" /> Привязать
</button>
</div>
{err && <p className="mt-1.5 text-xs text-crit">{err}</p>}
<div className="mt-3 divide-y divide-border">
{devices.length === 0 ? (
<p className="py-7 text-center text-sm text-fg-muted">
Устройств пока нет. Привяжите трекер по IMEI — данные появятся после первого сеанса связи.
</p>
) : (
devices.map((d) => (
<div key={d.imei} className="flex items-center gap-3 py-3">
<div className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-accent/15 text-accent">
<Smartphone className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-fg">{d.name}</div>
<div className="font-mono text-[11px] text-fg-muted">IMEI {d.imei}</div>
</div>
<button onClick={() => remove(d.imei)} className="grid h-8 w-8 shrink-0 place-items-center rounded-lg text-fg-muted hover:bg-muted hover:text-crit" title="Отвязать">
<Trash2 className="h-4 w-4" />
</button>
</div>
))
)}
</div>
</div>
</Card>
<Card className="lg:col-span-1">
<CardHeader title="Как подключить трекер" />
<div className="space-y-2.5 px-4 pb-4 text-sm text-fg-muted">
<p className="flex items-start gap-2">
<Info className="mt-0.5 h-4 w-4 shrink-0 text-info" /> В конфигураторе Navtelecom укажите телематический сервер:
</p>
<div className="rounded-lg bg-bg/40 p-3 font-mono text-xs leading-relaxed text-fg">
домен: gps.servaki.online
<br />
порт: 2000
<br />
протокол: FLEX
</div>
<p>
После привязки IMEI входящие данные попадут в ваш кабинет (<span className="font-mono">{user.clientId}</span>) и появятся в GPS-мониторинге — только вам.
</p>
</div>
</Card>
</div>
</div>
);
}