commit 070ab80d4a95a4b23fff7d2fdd5ae88415ca4818 Author: mo Date: Sun Jul 12 21:54:28 2026 +0000 Initial commit — Mek-Tech Live Labs Showcase v1.0 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 ( +