feat: initial website implementation
CI / TypeScript — type-check (push) Successful in 8s
CI / Publish — build & push image (push) Successful in 11s
CI / Deploy — pull & restart on host (push) Failing after 3s

React 18 + TypeScript + Vite + Tailwind CSS website for netOrk.
Built by a parallel instance based on the product docs.

Pages: Home (landing), Features, Drivers, Getting Started
Components: Nav, Footer
Assets: public/logo.png (1024x1024 RGBA)
Docker: multi-stage Dockerfile (node:20 builder + nginx:alpine)
Compose: netork-website service on proxy-net (Zoraxy integration)

docs/DESIGN.md: Logo Assets section added (PNG only, SVG removed);
wordmark pattern and <head> references documented.
.gitignore: fixed (was literal \n, now proper newlines); excludes
node_modules/, dist/, memory/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-06-28 10:19:50 +02:00
co-authored by Claude Sonnet 4.6
parent 00c483c2a2
commit efa01b5f41
26 changed files with 4118 additions and 1 deletions
+450
View File
@@ -0,0 +1,450 @@
import { Link } from 'react-router-dom'
import {
MagnifyingGlassIcon,
ChartBarIcon,
WrenchScrewdriverIcon,
PuzzlePieceIcon,
} from '@heroicons/react/24/outline'
const DRIVERS = [
'OpenWRT', 'OPNsense', 'Proxmox VE', 'Linux', 'HP ProCurve / Aruba',
'TP-Link Jetstream', 'Netgear', 'Fritz!Box', 'Zyxel', 'OpenMediaVault',
'Sonos', 'Cisco IOS', 'Cisco IOS-XE', 'Cisco NX-OS', 'Arista EOS', 'Juniper JunOS',
]
function BrowserFrame({ label, children }: { label: string; children: React.ReactNode }) {
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">
<span className="h-2.5 w-2.5 rounded-full bg-red-500/70" />
<span className="h-2.5 w-2.5 rounded-full bg-yellow-500/70" />
<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 bg-sky-600 text-white">+ Add device</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 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>
</div>
)
}
export default function Home() {
return (
<>
{/* 1 — Hero */}
<section className="py-24 md:py-32">
<div className="max-w-7xl mx-auto px-6 text-center">
<h1 className="text-4xl md:text-6xl font-bold text-slate-100 leading-tight mb-6">
Network orchestration<br />
<span className="text-sky-400">for heterogeneous infrastructure.</span>
</h1>
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mx-auto mb-10">
netOrk discovers, monitors, and manages your routers, switches, access points,
firewalls, and servers from a single UI — regardless of vendor.
No SaaS dependency. Runs on your infrastructure.
</p>
<div className="flex flex-wrap gap-4 justify-center">
<Link
to="/docs/getting-started"
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-sky-600 hover:bg-sky-500 text-white font-medium transition-colors"
>
Get started →
</Link>
<Link
to="/features"
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-slate-700 hover:border-slate-500 text-slate-300 hover:text-slate-100 transition-colors"
>
View features
</Link>
</div>
</div>
<div className="max-w-7xl mx-auto px-6 mt-16">
<BrowserFrame label="netork.local / devices">
<MockDeviceList />
</BrowserFrame>
</div>
</section>
<div className="border-t border-slate-800" />
{/* 2 — Problem Statement */}
<section className="py-16">
<div className="max-w-3xl mx-auto px-6 text-center">
<p className="text-base text-slate-400 leading-relaxed">
Managing a mixed network means juggling a different admin UI for every vendor —
one for OPNsense, one for HP ProCurve, one for OpenWRT, one for Proxmox.
Config changes happen directly on devices with no audit trail.
You find out something drifted when it breaks.
</p>
</div>
</section>
<div className="border-t border-slate-800" />
{/* 3 — Core Capabilities */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-6">
<div className="grid md:grid-cols-3 gap-6">
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<MagnifyingGlassIcon className="h-5 w-5 text-sky-400" />
</div>
<h3 className="mb-2 text-lg font-semibold text-slate-100">Discover everything on your network</h3>
<p className="text-sm text-slate-400 leading-relaxed">
ICMP sweep, SNMP scan, and HTTP probing find devices before you add them.
Fingerprinting identifies vendor and platform automatically.
Adopt results into your inventory with a single click.
</p>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<ChartBarIcon className="h-5 w-5 text-sky-400" />
</div>
<h3 className="mb-2 text-lg font-semibold text-slate-100">Poll device state continuously</h3>
<p className="text-sm text-slate-400 leading-relaxed">
Every device is polled on a configurable interval via NAPALM.
Interface status, ARP tables, DHCP leases, VLAN membership,
Docker containers, and SNMP health metrics — all in one place.
</p>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<WrenchScrewdriverIcon className="h-5 w-5 text-sky-400" />
</div>
<h3 className="mb-2 text-lg font-semibold text-slate-100">Detect drift. Fix it.</h3>
<p className="text-sm text-slate-400 leading-relaxed">
Define desired state in netOrk. On every poll, device config is compared against it.
Drifted devices get a warning; a one-click fix stream applies the correction and
shows you live SSH output.
</p>
</div>
</div>
</div>
</section>
<div className="border-t border-slate-800" />
{/* 4 — Driver Grid */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-6 text-center">
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Works with your hardware</h2>
<p className="text-base text-slate-400 leading-relaxed max-w-xl mx-auto mb-10">
netOrk ships with custom NAPALM drivers for 11 device types, plus all built-in NAPALM drivers.
New drivers follow a documented registration pattern.
</p>
<div className="flex flex-wrap justify-center gap-3">
{DRIVERS.map((d) => (
<span
key={d}
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full border border-slate-700 bg-slate-900 text-xs font-medium text-slate-300"
>
{d}
</span>
))}
</div>
<div className="mt-8">
<Link to="/drivers" className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
Full driver reference →
</Link>
</div>
</div>
</section>
<div className="border-t border-slate-800" />
{/* 5 — Screenshot Walkthrough */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-6 space-y-24">
{/* Row 1 — left text, right screenshot */}
<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">Device detail at a glance</h2>
<p className="text-base text-slate-400 leading-relaxed">
Hostname, IP, vendor, OS version, last poll time, and active warnings on one card.
Tabbed detail view for interfaces, LLDP neighbors, ARP table, VLAN membership,
packages, services, and scheduled jobs.
</p>
</div>
<BrowserFrame label="netork.local / devices / ap-001">
<MockDeviceDetail />
</BrowserFrame>
</div>
{/* Row 2 — right text, left screenshot */}
<div className="grid md:grid-cols-2 gap-12 items-center">
<BrowserFrame label="netork.local / vlans">
<MockVlans />
</BrowserFrame>
<div>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Intent-based VLAN and SSID management</h2>
<p className="text-base text-slate-400 leading-relaxed">
Define VLAN names and SSID settings once. netOrk compares them against every polled
device and pushes corrections automatically via UCI (OpenWRT) or the device's native API.
</p>
</div>
</div>
{/* Row 3 — left text, right screenshot */}
<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">Security visibility per device</h2>
<p className="text-base text-slate-400 leading-relaxed">
Wazuh agent status, CVE counts by severity, and recent alerts — all linked to the device
record. One-click agent install if the agent is missing.
Graylog syslog forwarding status with auto-fix.
</p>
</div>
<BrowserFrame label="netork.local / devices / ap-001 / security">
<MockSecurity />
</BrowserFrame>
</div>
</div>
</section>
<div className="border-t border-slate-800" />
{/* 6 — Plugin System */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-6">
<div className="rounded-xl border border-slate-800 bg-slate-900 p-8 md:p-12">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<PuzzlePieceIcon className="h-5 w-5 text-sky-400" />
</div>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Built to extend</h2>
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mb-6">
Integrations (Wazuh, Graylog, CrowdSec, apt-cacher-ng) are plugins that register into
the plugin system — they can be enabled or disabled per deployment without code changes.
Adding a new integration follows a documented pattern with a hook bus, typed metadata,
and a plugin registry.
</p>
<Link to="/plugins" className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
Plugin system docs →
</Link>
</div>
</div>
</section>
<div className="border-t border-slate-800" />
{/* 7 — Deployment */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-6">
<div className="grid md:grid-cols-2 gap-12 items-start">
<div>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Self-hosted. One command.</h2>
<p className="text-base text-slate-400 leading-relaxed">
netOrk runs in Docker Compose. Five containers: API, two worker pools, a Beat scheduler,
and an nginx UI server. No external dependencies beyond Redis and PostgreSQL.
</p>
</div>
<pre className="rounded-xl border border-slate-800 bg-slate-900 p-6 font-mono text-sm text-slate-300 overflow-x-auto">
<code>{`# Clone + configure
git clone https://gitea.example.com/netork/netork.git
cp .env.example .env
# edit .env (DB URL, Redis password, secret key)
# Deploy
bash scripts/deploy.sh 192.168.1.10`}</code>
</pre>
</div>
</div>
</section>
<div className="border-t border-slate-800" />
{/* 8 — CTA Footer */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-6 text-center">
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-8">Start managing your network.</h2>
<Link
to="/docs/getting-started"
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-sky-600 hover:bg-sky-500 text-white font-medium transition-colors"
>
Read the docs →
</Link>
</div>
</section>
</>
)
}