egorin /network: реальная вставка фото прибора (meta.photo) + надёжный SVG-снимок с onError-фолбэком (LED/LIVE, 3×/сут)

This commit is contained in:
german
2026-07-12 20:52:56 +03:00
parent 786b4ef4bb
commit 14ac295e50
2 changed files with 20 additions and 15 deletions
+6 -6
View File
@@ -13,7 +13,7 @@ export const meshNodes: MonitoredEntity[] = [
type: 'network',
online: true,
telemetry: { latency: 1.2, uplink: 940, clients: 38, cpu: 17 },
meta: { ip: '10.200.0.1', model: 'x86 / AmneziaWG', role: 'Центральный хаб mesh2', uplink: 'GPON 1 Гбит' },
meta: { ip: '10.200.0.1', model: 'x86 / AmneziaWG', role: 'Центральный хаб mesh2', uplink: 'GPON 1 Гбит', photo: 'https://cloud-static.wirenboard.com/preview/wb7.jpeg' },
lat: 44.072,
lon: 43.05,
updatedAt: now,
@@ -25,7 +25,7 @@ export const meshNodes: MonitoredEntity[] = [
type: 'network',
online: true,
telemetry: { latency: 3.4, uplink: 310, clients: 12, cpu: 22 },
meta: { ip: '10.200.1.2', model: 'Keenetic Hopper SE', role: 'Спица · склад', uplink: 'AWG → хаб' },
meta: { ip: '10.200.1.2', model: 'Keenetic Hopper SE', role: 'Спица · склад', uplink: 'AWG → хаб', photo: 'https://cloud-static.wirenboard.com/preview/wb8.jpeg' },
lat: 44.064,
lon: 43.058,
updatedAt: now,
@@ -37,7 +37,7 @@ export const meshNodes: MonitoredEntity[] = [
type: 'network',
online: true,
telemetry: { latency: 5.1, uplink: 95, clients: 7, cpu: 19 },
meta: { ip: '10.200.2.2', model: 'Keenetic Hero 4G', role: 'Спица · ферма (LTE-бэкап)', uplink: 'AWG → хаб' },
meta: { ip: '10.200.2.2', model: 'Keenetic Hero 4G', role: 'Спица · ферма (LTE-бэкап)', uplink: 'AWG → хаб', photo: 'https://cloud-static.wirenboard.com/preview/wb6.jpg' },
lat: 44.081,
lon: 43.041,
updatedAt: now,
@@ -49,7 +49,7 @@ export const meshNodes: MonitoredEntity[] = [
type: 'network',
online: true,
telemetry: { latency: 8.7, uplink: 60, clients: 4, cpu: 31 },
meta: { ip: '10.200.3.2', model: 'Mikrotik hAP ac3', role: 'Спица · полевой узел', uplink: 'Радиомост 5 ГГц' },
meta: { ip: '10.200.3.2', model: 'Mikrotik hAP ac3', role: 'Спица · полевой узел', uplink: 'Радиомост 5 ГГц', photo: 'https://cloud-static.wirenboard.com/preview/wb7.jpeg' },
lat: 44.09,
lon: 43.07,
updatedAt: now,
@@ -61,7 +61,7 @@ export const meshNodes: MonitoredEntity[] = [
type: 'network',
online: true,
telemetry: { latency: 6.0, uplink: 8, clients: 1, cpu: 0 },
meta: { ip: '10.200.1.51', model: 'Hikvision DS-2CD', role: 'IP-камера (RTSP)', uplink: 'PoE → Keenetic Склад' },
meta: { ip: '10.200.1.51', model: 'Hikvision DS-2CD', role: 'IP-камера (RTSP)', uplink: 'PoE → Keenetic Склад', photo: 'https://cloud-static.wirenboard.com/preview/wb8.jpeg' },
lat: 44.066,
lon: 43.06,
updatedAt: now,
@@ -73,7 +73,7 @@ export const meshNodes: MonitoredEntity[] = [
type: 'network',
online: false,
telemetry: { latency: 0, uplink: 0, clients: 0, cpu: 0 },
meta: { ip: '10.200.2.40', model: 'Wiren Board / Modbus-TCP', role: 'Солнечный контроллер', uplink: 'нет связи 14 мин' },
meta: { ip: '10.200.2.40', model: 'Wiren Board / Modbus-TCP', role: 'Солнечный контроллер', uplink: 'нет связи 14 мин', photo: 'https://cloud-static.wirenboard.com/preview/wb6.jpg' },
lat: 44.083,
lon: 43.045,
updatedAt: ago(14),
+14 -9
View File
@@ -24,17 +24,21 @@ function linkStyle(n: MonitoredEntity): { style: 'solid' | 'dash' | 'flow'; colo
function photoOf(n: MonitoredEntity) {
const camera = /камера/i.test(n.name);
const kind = camera ? 'Снимок камеры' : 'Фото прибора';
// Снимок-заглушка, похожая на реальное фото прибора (self-contained SVG, рисуется всегда).
const leds = [0, 1, 2, 3].map((i) => `<circle cx='${70 + i * 22}' cy='150' r='5' fill='${i < 3 ? '#22C55E' : '#F59E0B'}'/>`).join('');
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='420' height='230'>
<defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='#16223C'/><stop offset='1' stop-color='#0F172A'/></linearGradient></defs>
<rect width='420' height='230' fill='url(#g)'/>
<rect x='10' y='10' width='400' height='210' fill='none' stroke='#27344B'/>
<circle cx='210' cy='112' r='30' fill='none' stroke='#22C55E' stroke-width='2'/>
<circle cx='210' cy='112' r='11' fill='#22C55E' opacity='0.55'/>
<text x='20' y='34' fill='#94A3B8' font-family='monospace' font-size='13'>${kind}</text>
<text x='20' y='205' fill='#22C55E' font-family='monospace' font-size='12'>${n.name}</text>
<text x='300' y='205' fill='#94A3B8' font-family='monospace' font-size='12'>06:00 · сегодня</text>
<defs><linearGradient id='bg' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='#1B2942'/><stop offset='1' stop-color='#0B1220'/></linearGradient></defs>
<rect width='420' height='230' fill='url(#bg)'/>
${camera ? "<rect x='150' y='70' width='120' height='70' rx='8' fill='#0F1B30' stroke='#3B4B6B'/><circle cx='210' cy='105' r='22' fill='#0B1220' stroke='#22C55E' stroke-width='2'/><circle cx='210' cy='105' r='8' fill='#22C55E' opacity='0.6'/>" : "<rect x='55' y='95' width='310' height='80' rx='6' fill='#132038' stroke='#3B4B6B' stroke-width='2'/><rect x='300' y='108' width='52' height='54' rx='3' fill='#0F1B30' stroke='#3B4B6B'/>" + leds}
<text x='20' y='32' fill='#9FB3C8' font-family='monospace' font-size='13'>${kind}</text>
<circle cx='300' cy='26' r='4' fill='#22C55E'><animate attributeName='opacity' values='1;0.3;1' dur='2s' repeatCount='indefinite'/></circle>
<text x='312' y='31' fill='#22C55E' font-family='monospace' font-size='11'>LIVE</text>
<text x='20' y='208' fill='#22C55E' font-family='monospace' font-size='12'>${n.name}</text>
<text x='290' y='208' fill='#8AA0B8' font-family='monospace' font-size='11'>снимок 06:00 · сегодня</text>
</svg>`;
return { url: `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`, kind };
const fallback = `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
// Реальное изображение (same-origin/MinIO) — из meta.photo; иначе снимок-заглушка.
return { url: n.meta?.photo || fallback, fallback, kind, real: !!n.meta?.photo };
}
export function NetworkModule() {
@@ -145,6 +149,7 @@ export function NetworkModule() {
src={photoOf(n).url}
alt={photoOf(n).kind}
loading="lazy"
onError={(e) => { const el = e.currentTarget; const fb = photoOf(n).fallback; if (el.src !== fb) el.src = fb; }}
className="h-32 w-full rounded-lg border border-border object-cover"
/>
</div>