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:
co-authored by
Claude Opus 5.5
parent
38834100d1
commit
011f816fc9
+18
-300
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user