feat: replace UI mockups with real netOrk screenshots

The homepage showed seven hand-built JSX imitations of the netOrk UI. They
are gone; every image is now a screenshot of netOrk v0.28.0 itself, taken
from an anonymized copy of a production database (scripts/demo) with
scripts/screenshots/capture.py and published as WebP (~630 KB for all eight).

- Hero: the device inventory. Walkthrough: device detail, VLANs, the Security
  tab, the vulnerability triage queue (replacing the config-diff row), the
  dashboard and service checks (new row 6). NIS2: the audit log, filtered to
  what people did.
- Copy follows the images: row 3 describes the security assessment, row 4 the
  triage queue; row 2 no longer claims corrections are always automatic;
  18 widgets. Alt texts in both languages.
- Also fixed on the homepage: the NIS2 teaser for Art. 21 (2e) and the
  container list of a deployment (three worker pools, plus Flower, registry,
  APT cache and the Signal gateway).
- Demo tooling hardened on the real dump: secrets inside JSON (Wi-Fi keys),
  reverse DNS zones, glued identifiers, tens of thousands of CrowdSec
  addresses, a schema newer than the release (anonymize, then downgrade),
  MFA-enforcing roles, and click steps for view filters.
- DESIGN.md: real screenshots only. PAGES.md: the six rows as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-09-26 08:24:51 +02:00
co-authored by Claude Opus 5.5
parent 38834100d1
commit 011f816fc9
16 changed files with 203 additions and 374 deletions
+18 -300
View File
@@ -15,7 +15,7 @@ const DRIVERS = [
'TP-Link JetStream', 'Yealink', 'Zyxel VMG',
]
function BrowserFrame({ label, children }: { label: string; children: React.ReactNode }) {
function Screenshot({ src, label, alt }: { src: string; label: string; alt: string }) {
return (
<div className="rounded-xl border border-slate-700 overflow-hidden shadow-2xl">
<div className="flex items-center gap-1.5 border-b border-slate-700 bg-slate-800 px-4 py-2.5">
@@ -24,282 +24,7 @@ function BrowserFrame({ label, children }: { label: string; children: React.Reac
<span className="h-2.5 w-2.5 rounded-full bg-green-500/70" />
<span className="ml-4 text-xs text-slate-500 font-mono">{label}</span>
</div>
{children}
</div>
)
}
function StatusBadge({ status }: { status: 'active' | 'warning' | 'offline' }) {
const cls =
status === 'active' ? 'bg-green-500/20 text-green-400' :
status === 'warning' ? 'bg-yellow-500/20 text-yellow-400' :
'bg-red-500/20 text-red-400'
return (
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${cls}`}>
{status}
</span>
)
}
function MockDeviceList() {
const rows = [
{ name: 'fw-001.lan', ip: '10.0.0.1', driver: 'OPNsense', status: 'active' as const },
{ name: 'sw-core.lan', ip: '10.0.0.2', driver: 'HP ProCurve', status: 'warning' as const },
{ name: 'ap-001.lan', ip: '10.0.0.10', driver: 'OpenWRT', status: 'active' as const },
{ name: 'ap-002.lan', ip: '10.0.0.11', driver: 'OpenWRT', status: 'warning' as const },
{ name: 'pve-01.lan', ip: '10.0.0.20', driver: 'Proxmox VE', status: 'active' as const },
{ name: 'nas-01.lan', ip: '10.0.0.30', driver: 'OpenMediaVault', status: 'active' as const },
]
return (
<div className="bg-slate-950 p-4">
<div className="mb-3 flex items-center justify-between">
<span className="text-xs font-semibold text-slate-100">Devices</span>
<span className="text-xs px-2.5 py-1 rounded-md border border-slate-700 text-slate-400">Discovery</span>
</div>
<div className="rounded-lg border border-slate-800 overflow-hidden">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-slate-800 bg-slate-900">
<th className="px-3 py-2 text-left font-medium text-slate-500">Hostname</th>
<th className="px-3 py-2 text-left font-medium text-slate-500">IP</th>
<th className="px-3 py-2 text-left font-medium text-slate-500 hidden sm:table-cell">Driver</th>
<th className="px-3 py-2 text-left font-medium text-slate-500">Status</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.name} className="border-b border-slate-800 last:border-0 hover:bg-slate-800/40">
<td className="px-3 py-2 font-mono text-sky-400">{r.name}</td>
<td className="px-3 py-2 font-mono text-slate-400">{r.ip}</td>
<td className="px-3 py-2 text-slate-400 hidden sm:table-cell">{r.driver}</td>
<td className="px-3 py-2"><StatusBadge status={r.status} /></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
function MockDeviceDetail() {
return (
<div className="bg-slate-950 p-4">
<div className="rounded-lg border border-slate-800 bg-slate-900 p-4 mb-3">
<div className="flex items-start justify-between mb-3">
<div>
<p className="text-xs font-semibold text-slate-100">ap-001.lan</p>
<p className="text-xs text-slate-500 font-mono">10.0.0.10</p>
</div>
<StatusBadge status="active" />
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div><span className="text-slate-500">Vendor</span><p className="text-slate-300">GL.iNet</p></div>
<div><span className="text-slate-500">OS</span><p className="text-slate-300">OpenWRT 23.05</p></div>
<div><span className="text-slate-500">Driver</span><p className="text-slate-300">openwrt</p></div>
<div><span className="text-slate-500">Last poll</span><p className="text-slate-300">2 min ago</p></div>
</div>
</div>
<div className="flex gap-2 text-xs mb-3">
{['Interfaces', 'LLDP', 'ARP', 'VLANs', 'Services'].map((t) => (
<span key={t} className={`px-2 py-1 rounded text-xs ${t === 'Interfaces' ? 'bg-sky-600/20 text-sky-400' : 'text-slate-500 hover:text-slate-300'}`}>{t}</span>
))}
</div>
<div className="rounded-lg border border-slate-800 overflow-hidden">
<table className="w-full text-xs">
<thead><tr className="bg-slate-900 border-b border-slate-800">
<th className="px-3 py-1.5 text-left text-slate-500 font-medium">Interface</th>
<th className="px-3 py-1.5 text-left text-slate-500 font-medium">IP</th>
<th className="px-3 py-1.5 text-left text-slate-500 font-medium">State</th>
</tr></thead>
<tbody>
{[['br-lan', '10.0.0.10/24', 'up'], ['wlan0', '—', 'up'], ['eth0', '—', 'up']].map(([iface, ip, state]) => (
<tr key={iface} className="border-b border-slate-800 last:border-0">
<td className="px-3 py-1.5 font-mono text-slate-300">{iface}</td>
<td className="px-3 py-1.5 font-mono text-slate-400">{ip}</td>
<td className="px-3 py-1.5"><span className="text-green-400">{state}</span></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
function MockVlans() {
return (
<div className="bg-slate-950 p-4">
<div className="mb-3 flex items-center justify-between">
<span className="text-xs font-semibold text-slate-100">VLANs</span>
<span className="text-xs px-2.5 py-1 rounded-md bg-sky-600 text-white">+ Add VLAN</span>
</div>
<div className="space-y-2">
{[
{ id: 10, name: 'management', devices: 6, drift: false },
{ id: 20, name: 'iot', devices: 4, drift: true },
{ id: 30, name: 'servers', devices: 3, drift: false },
{ id: 40, name: 'guest', devices: 2, drift: false },
].map((v) => (
<div key={v.id} className="flex items-center justify-between rounded-lg border border-slate-800 bg-slate-900 px-3 py-2">
<div className="flex items-center gap-3">
<span className="text-xs font-mono text-slate-500 w-8">#{v.id}</span>
<span className="text-xs text-slate-300">{v.name}</span>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-slate-500">{v.devices} devices</span>
{v.drift
? <span className="text-xs font-medium px-2 py-0.5 rounded-full bg-yellow-500/20 text-yellow-400">drift</span>
: <span className="text-xs font-medium px-2 py-0.5 rounded-full bg-green-500/20 text-green-400">clean</span>
}
</div>
</div>
))}
</div>
<div className="mt-3 rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3">
<p className="text-xs text-yellow-400 font-medium mb-1">Drift detected — VLAN 20 (iot)</p>
<p className="text-xs text-slate-400">ap-002.lan: VLAN name mismatch. Expected "iot", got "IoT-devices".</p>
<button className="mt-2 text-xs px-2.5 py-1 rounded bg-sky-600 hover:bg-sky-500 text-white transition-colors">Fix now</button>
</div>
</div>
)
}
function MockConfigDiff() {
const snapshots = [
{ id: 'a3f9c1', when: '2 min ago', label: 'current' },
{ id: '7e2b04', when: '1 h ago' },
{ id: 'd819e6', when: '6 h ago' },
]
return (
<div className="bg-slate-950 p-4">
<div className="mb-3 flex items-center justify-between">
<span className="text-xs font-semibold text-slate-100">Config — fw-001.lan</span>
<button className="text-xs px-2.5 py-1 rounded bg-sky-600 hover:bg-sky-500 text-white transition-colors">Restore</button>
</div>
<div className="flex gap-2 mb-3">
{snapshots.map((s) => (
<span key={s.id} className={`text-xs font-mono px-2 py-1 rounded border ${s.label ? 'border-sky-500/40 bg-sky-500/10 text-sky-400' : 'border-slate-800 text-slate-500'}`}>
{s.id} <span className="text-slate-600">· {s.when}</span>
</span>
))}
</div>
<div className="rounded-lg border border-slate-800 overflow-hidden font-mono text-xs">
<div className="px-3 py-1.5 bg-slate-900 text-slate-500 border-b border-slate-800">7e2b04 → a3f9c1</div>
<div className="px-3 py-1 bg-red-500/10 text-red-400">- set firewall.rule_42.destination_port='22'</div>
<div className="px-3 py-1 bg-green-500/10 text-green-400">+ set firewall.rule_42.destination_port='2222'</div>
<div className="px-3 py-1 text-slate-500"> commit</div>
</div>
<div className="mt-3 rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3">
<p className="text-xs text-yellow-400 font-medium">Unauthorized change detected — fw-001.lan</p>
<p className="text-xs text-slate-400 mt-1">Configuration changed outside netOrk between the last two polls.</p>
</div>
</div>
)
}
function MockDashboard() {
const widgets = [
{ label: 'Stats', span: 'col-span-2' },
{ label: 'Device Warnings', span: 'col-span-1' },
{ label: 'Network Topology', span: 'col-span-2' },
{ label: 'EOL Status', span: 'col-span-1' },
]
return (
<div className="bg-slate-950 p-4">
<div className="mb-3 flex items-center justify-between">
<span className="text-xs font-semibold text-slate-100">My Dashboard</span>
<div className="flex items-center gap-2">
<span className="text-xs px-2 py-0.5 rounded-full bg-sky-500/15 text-sky-400 border border-sky-500/20">shared</span>
<span className="text-xs px-2.5 py-1 rounded-md bg-sky-600 text-white">+ Add Widget</span>
</div>
</div>
<div className="grid grid-cols-3 gap-2">
{widgets.map((w) => (
<div key={w.label} className={`${w.span} rounded-lg border border-dashed border-slate-700 bg-slate-900 p-3 h-16 flex items-center justify-center`}>
<span className="text-xs text-slate-500">{w.label}</span>
</div>
))}
</div>
</div>
)
}
function MockCompliance() {
const checks = [
{ label: 'Asset inventory', detail: '18 / 18 devices tracked', ok: true },
{ label: 'Patch status', detail: 'All devices polled < 60 min', ok: true },
{ label: 'Wazuh agents', detail: '15 / 18 agents active', ok: false },
{ label: 'Syslog forwarding', detail: '14 / 18 forwarding to Graylog', ok: false },
{ label: 'Config drift', detail: '0 drifted devices', ok: true },
{ label: 'Audit log', detail: '23 actions logged (24 h)', ok: true },
]
return (
<div className="bg-slate-950 p-4">
<div className="mb-3 flex items-center justify-between">
<span className="text-xs font-semibold text-slate-100">Compliance overview — HQ</span>
<span className="text-xs font-mono text-sky-500">Art. 21 NIS2</span>
</div>
<div className="space-y-1.5">
{checks.map((c) => (
<div key={c.label} className="flex items-center justify-between rounded-lg border border-slate-800 bg-slate-900 px-3 py-2">
<div className="flex items-center gap-2.5">
<span className={`text-xs font-semibold w-3 ${c.ok ? 'text-green-400' : 'text-yellow-400'}`}>
{c.ok ? '✓' : '⚠'}
</span>
<span className="text-xs text-slate-300">{c.label}</span>
</div>
<span className="text-xs text-slate-500">{c.detail}</span>
</div>
))}
</div>
</div>
)
}
function MockSecurity() {
return (
<div className="bg-slate-950 p-4">
<div className="mb-3">
<span className="text-xs font-semibold text-slate-100">Security — ap-001.lan</span>
</div>
<div className="space-y-2">
<div className="rounded-lg border border-slate-800 bg-slate-900 p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-slate-300">Wazuh Agent</span>
<StatusBadge status="active" />
</div>
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="text-center rounded bg-red-500/10 border border-red-500/20 py-1.5">
<p className="text-red-400 font-semibold">2</p>
<p className="text-slate-500">Critical</p>
</div>
<div className="text-center rounded bg-yellow-500/10 border border-yellow-500/20 py-1.5">
<p className="text-yellow-400 font-semibold">7</p>
<p className="text-slate-500">High</p>
</div>
<div className="text-center rounded bg-blue-500/10 border border-blue-500/20 py-1.5">
<p className="text-blue-400 font-semibold">14</p>
<p className="text-slate-500">Medium</p>
</div>
</div>
</div>
<div className="rounded-lg border border-slate-800 bg-slate-900 p-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-slate-300">Graylog syslog</span>
<span className="text-xs font-medium px-2 py-0.5 rounded-full bg-green-500/20 text-green-400">forwarding</span>
</div>
<p className="text-xs text-slate-500 mt-1">10.0.0.50:514 (UDP)</p>
</div>
<div className="rounded-lg border border-slate-800 bg-slate-900 p-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-slate-300">CrowdSec</span>
<span className="text-xs font-medium px-2 py-0.5 rounded-full bg-green-500/20 text-green-400">active</span>
</div>
<p className="text-xs text-slate-500 mt-1">3 decisions · 0 bans (24 h)</p>
</div>
</div>
<img src={src} alt={alt} loading="lazy" className="block w-full" width={1600} height={1000} />
</div>
)
}
@@ -336,9 +61,7 @@ export default function Home() {
</div>
</div>
<div className="max-w-7xl mx-auto px-6 mt-16">
<BrowserFrame label="netork.local / devices">
<MockDeviceList />
</BrowserFrame>
<Screenshot src="/screenshots/devices.webp" label="netork / devices" alt={h.shotAlt.devices} />
</div>
</section>
@@ -414,45 +137,42 @@ export default function Home() {
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot1.heading}</h2>
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.screenshot1.body)}</p>
</div>
<BrowserFrame label="netork.local / devices / ap-001">
<MockDeviceDetail />
</BrowserFrame>
<Screenshot src="/screenshots/device-detail.webp" label="netork / devices / ap-floor1" alt={h.shotAlt.deviceDetail} />
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<BrowserFrame label="netork.local / vlans">
<MockVlans />
</BrowserFrame>
<div>
<div className="md:order-2">
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot2.heading}</h2>
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.screenshot2.body)}</p>
</div>
<Screenshot src="/screenshots/vlans.webp" label="netork / vlans" alt={h.shotAlt.vlans} />
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot3.heading}</h2>
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.screenshot3.body)}</p>
</div>
<BrowserFrame label="netork.local / devices / ap-001 / security">
<MockSecurity />
</BrowserFrame>
<Screenshot src="/screenshots/device-security.webp" label="netork / devices / proxy-01 / security" alt={h.shotAlt.deviceSecurity} />
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<BrowserFrame label="netork.local / devices / fw-001 / config">
<MockConfigDiff />
</BrowserFrame>
<div>
<div className="md:order-2">
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot4.heading}</h2>
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.screenshot4.body)}</p>
</div>
<Screenshot src="/screenshots/vulnerabilities.webp" label="netork / vulnerabilities" alt={h.shotAlt.vulnerabilities} />
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot5.heading}</h2>
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.screenshot5.body)}</p>
</div>
<BrowserFrame label="netork.local / dashboards / my-dashboard">
<MockDashboard />
</BrowserFrame>
<Screenshot src="/screenshots/dashboard.webp" label="netork / dashboard" alt={h.shotAlt.dashboard} />
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="md:order-2">
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot6.heading}</h2>
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.screenshot6.body)}</p>
</div>
<Screenshot src="/screenshots/service-checks.webp" label="netork / monitoring / checks" alt={h.shotAlt.serviceChecks} />
</div>
</div>
</section>
@@ -485,9 +205,7 @@ export default function Home() {
))}
</div>
</div>
<BrowserFrame label="netork.local / compliance / HQ">
<MockCompliance />
</BrowserFrame>
<Screenshot src="/screenshots/audit-log.webp" label="netork / audit-log" alt={h.shotAlt.auditLog} />
</div>
</div>
</section>