From 070ab80d4a95a4b23fff7d2fdd5ae88415ca4818 Mon Sep 17 00:00:00 2001 From: mo Date: Sun, 12 Jul 2026 21:54:28 +0000 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20Mek-Tech=20Live?= =?UTF-8?q?=20Labs=20Showcase=20v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + Dockerfile | 19 + frontend/index.html | 15 + frontend/src/App.jsx | 23 + frontend/src/components/GlobalOpsLayout.jsx | 43 + frontend/src/components/LiveCameraFeed.jsx | 236 ++ frontend/src/components/SmartCityMap.jsx | 123 + frontend/src/components/dashboard.jsx | 66 + frontend/src/components/shared.jsx | 58 + frontend/src/layouts/Shell.jsx | 49 + frontend/src/main.jsx | 13 + frontend/src/pages/Fraud.jsx | 110 + frontend/src/pages/Hub.jsx | 85 + frontend/src/pages/Retail.jsx | 100 + frontend/src/pages/SmartCity.jsx | 179 + frontend/src/pages/SupplyChain.jsx | 94 + frontend/src/pages/Trading.jsx | 107 + frontend/src/styles.css | 350 ++ frontend/vite.config.js | 20 + package-lock.json | 3540 +++++++++++++++++++ package.json | 30 + server/index.js | 120 + server/lib/cache.js | 48 + server/lib/camera-proxy.js | 47 + server/lib/sources/fraud.js | 116 + server/lib/sources/retail.js | 69 + server/lib/sources/smart-city-infra.js | 118 + server/lib/sources/smart-city.js | 209 ++ server/lib/sources/supply-chain.js | 68 + server/lib/sources/trading.js | 127 + 30 files changed, 6187 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 frontend/index.html create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/components/GlobalOpsLayout.jsx create mode 100644 frontend/src/components/LiveCameraFeed.jsx create mode 100644 frontend/src/components/SmartCityMap.jsx create mode 100644 frontend/src/components/dashboard.jsx create mode 100644 frontend/src/components/shared.jsx create mode 100644 frontend/src/layouts/Shell.jsx create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/pages/Fraud.jsx create mode 100644 frontend/src/pages/Hub.jsx create mode 100644 frontend/src/pages/Retail.jsx create mode 100644 frontend/src/pages/SmartCity.jsx create mode 100644 frontend/src/pages/SupplyChain.jsx create mode 100644 frontend/src/pages/Trading.jsx create mode 100644 frontend/src/styles.css create mode 100644 frontend/vite.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/index.js create mode 100644 server/lib/cache.js create mode 100644 server/lib/camera-proxy.js create mode 100644 server/lib/sources/fraud.js create mode 100644 server/lib/sources/retail.js create mode 100644 server/lib/sources/smart-city-infra.js create mode 100644 server/lib/sources/smart-city.js create mode 100644 server/lib/sources/supply-chain.js create mode 100644 server/lib/sources/trading.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1a1bf4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +cache/ +frontend/dist/ +.env +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..825a182 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY frontend ./frontend +COPY server ./server +RUN npm run build + +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install --omit=dev +COPY server ./server +COPY --from=builder /app/frontend/dist ./frontend/dist +ENV PORT=3010 +ENV CACHE_DIR=/app/cache +RUN mkdir -p /app/cache +EXPOSE 3010 +CMD ["node", "server/index.js"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..7b36b9e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + + Mek-Tech Live Labs + + + + +
+ + + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..6c13b6d --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,23 @@ +import { Routes, Route } from 'react-router-dom'; +import Shell from './layouts/Shell'; +import Hub from './pages/Hub'; +import Trading from './pages/Trading'; +import Retail from './pages/Retail'; +import SupplyChain from './pages/SupplyChain'; +import Fraud from './pages/Fraud'; +import SmartCity from './pages/SmartCity'; + +export default function App() { + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/frontend/src/components/GlobalOpsLayout.jsx b/frontend/src/components/GlobalOpsLayout.jsx new file mode 100644 index 0000000..0e3648d --- /dev/null +++ b/frontend/src/components/GlobalOpsLayout.jsx @@ -0,0 +1,43 @@ +import { LiveBadge } from './shared'; +import { InsightStrip } from './dashboard'; + +export function GlobalOpsLayout({ + badge = 'GLOBAL OPERATIONS CENTER', + title, + subtitle, + lastUpdate, + refreshMs, + contextBlocks, + insights, + children +}) { + return ( +
+
+
+ {badge} +

{title}

+ {subtitle &&

{subtitle}

} +
+
+ + {refreshMs && ⚡ {refreshMs}ms SSE} +
+
+ + {contextBlocks?.length > 0 && ( +
+ {contextBlocks.map((block, i) => ( +
+

{block.title}

+

{block.body}

+
+ ))} +
+ )} + + + {children} +
+ ); +} diff --git a/frontend/src/components/LiveCameraFeed.jsx b/frontend/src/components/LiveCameraFeed.jsx new file mode 100644 index 0000000..fd5c05c --- /dev/null +++ b/frontend/src/components/LiveCameraFeed.jsx @@ -0,0 +1,236 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; + +const REFRESH_MS = 1200; +const PRIMARY_REFRESH_MS = 800; + +function CctvOverlay({ camera }) { + return ( +
+
{camera.name}
+
{camera.city} · {camera.zone}
+
{new Date().toLocaleTimeString('nl-NL')}
+
+ ); +} + +function SnapshotFeed({ camera, compact, fast }) { + const [tick, setTick] = useState(0); + const [err, setErr] = useState(false); + const interval = fast ? PRIMARY_REFRESH_MS : REFRESH_MS; + + useEffect(() => { + setErr(false); + const iv = setInterval(() => setTick(t => t + 1), interval); + return () => clearInterval(iv); + }, [camera?.id, interval]); + + const src = camera?.frameUrl + ? `${camera.frameUrl}?t=${tick}` + : camera?.youtubeId + ? `https://i.ytimg.com/vi/${camera.youtubeId}/hqdefault_live.jpg?t=${tick}` + : null; + + if (!src || err) { + return ( +
+
+ +
+ ); + } + + return ( + <> + {camera.name} setErr(true)} + loading="eager" + decoding="async" + referrerPolicy="strict-origin-when-cross-origin" + /> +
+ {!compact && } + + ); +} + +function YoutubeFeed({ camera, onFail }) { + const failed = useRef(false); + const params = new URLSearchParams({ + autoplay: '1', + mute: '1', + controls: '1', + playsinline: '1', + rel: '0', + modestbranding: '1', + iv_load_policy: '3', + fs: '0' + }); + + useEffect(() => { + failed.current = false; + const timer = setTimeout(() => { + if (!failed.current) onFail?.(); + }, 12000); + return () => clearTimeout(timer); + }, [camera?.id, onFail]); + + const embedUrl = `https://www.youtube.com/embed/${camera.youtubeId}?${params}`; + + return ( +