feat: initial website implementation
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:
co-authored by
Claude Sonnet 4.6
parent
00c483c2a2
commit
efa01b5f41
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
@@ -0,0 +1,79 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: ['**']
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: TypeScript — type-check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
- name: Type-check (tsc)
|
||||
run: npx tsc --noEmit
|
||||
|
||||
publish:
|
||||
name: Publish — build & push image
|
||||
runs-on: ubuntu-latest
|
||||
needs: [typecheck]
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to registry
|
||||
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.netork.io -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: Build & push
|
||||
run: |
|
||||
SHA=$(git rev-parse --short HEAD)
|
||||
docker build \
|
||||
-t registry.netork.io/netork/website:latest \
|
||||
-t registry.netork.io/netork/website:main-${SHA} \
|
||||
.
|
||||
docker push registry.netork.io/netork/website:latest
|
||||
docker push registry.netork.io/netork/website:main-${SHA}
|
||||
|
||||
- name: Logout
|
||||
if: always()
|
||||
run: docker logout registry.netork.io
|
||||
|
||||
deploy:
|
||||
name: Deploy — pull & restart on host
|
||||
runs-on: ubuntu-latest
|
||||
needs: [publish]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Write SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no \
|
||||
"${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
|
||||
"cd /opt/netork-website && \
|
||||
echo '${{ secrets.REGISTRY_PASSWORD }}' | docker login registry.netork.io -u '${{ secrets.REGISTRY_USER }}' --password-stdin && \
|
||||
docker compose pull && \
|
||||
docker compose up -d --remove-orphans && \
|
||||
docker logout registry.netork.io"
|
||||
|
||||
- name: Clean up SSH key
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/deploy_key
|
||||
+7
-1
@@ -1 +1,7 @@
|
||||
node_modules/\ndist/\n.env\n.env.local\n*.local\n.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
memory/
|
||||
.env
|
||||
.env.local
|
||||
*.local
|
||||
.DS_Store
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Serve stage
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
netork-website:
|
||||
build: .
|
||||
image: netork-website:latest
|
||||
container_name: netork-website
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "80"
|
||||
networks:
|
||||
- proxy-net
|
||||
|
||||
# Zoraxy proxies to netork-website:80 via proxy-net.
|
||||
# No host port binding needed — SSL terminated by Zoraxy.
|
||||
networks:
|
||||
proxy-net:
|
||||
external: true
|
||||
@@ -6,6 +6,43 @@ between the product UI and the marketing site.
|
||||
|
||||
---
|
||||
|
||||
## Logo Assets
|
||||
|
||||
The logo file is in `public/` — use it directly, do not recreate.
|
||||
|
||||
| File | Format | Size | Use |
|
||||
|---|---|---|---|
|
||||
| `public/logo.png` | PNG | 1024×1024, RGBA | Nav logo, OG image, hero, press kit, favicon fallback |
|
||||
|
||||
### Usage in `<img>` (nav, hero)
|
||||
|
||||
```tsx
|
||||
<img src="/logo.png" alt="netOrk" className="h-8 w-8" />
|
||||
```
|
||||
|
||||
For the nav, pair it with the wordmark:
|
||||
|
||||
```tsx
|
||||
<a href="/" className="flex items-center gap-2.5">
|
||||
<img src="/logo.png" alt="" className="h-7 w-7" aria-hidden="true" />
|
||||
<span className="font-semibold text-slate-100 tracking-tight">
|
||||
net<span className="text-sky-400">Ork</span>
|
||||
</span>
|
||||
</a>
|
||||
```
|
||||
|
||||
Use `net<span class="text-sky-400">Ork</span>` consistently — the `Ork` part
|
||||
in sky-400 ties the wordmark to the accent color.
|
||||
|
||||
### `<head>` references
|
||||
|
||||
```html
|
||||
<link rel="icon" href="/logo.png" />
|
||||
<meta property="og:image" content="/logo.png" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Principle
|
||||
|
||||
**The website looks like a dark-mode dev tool, not a SaaS landing page.**
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="netOrk — self-hosted network orchestration for heterogeneous infrastructure. Discover, monitor, and manage routers, switches, firewalls, and servers from a single UI." />
|
||||
<title>netOrk — Network Orchestration Platform</title>
|
||||
<link rel="icon" href="/logo.png" />
|
||||
<meta property="og:image" content="/logo.png" />
|
||||
<meta property="og:title" content="netOrk — Network Orchestration Platform" />
|
||||
<meta property="og:description" content="Self-hosted network orchestration for heterogeneous infrastructure." />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 1024;
|
||||
gzip_vary on;
|
||||
|
||||
# SPA fallback — all routes resolve to index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Immutable cache for hashed assets (Vite adds content hashes)
|
||||
location ~* \.(js|css|woff2?)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location ~* \.(png|jpg|jpeg|gif|svg|ico|webp)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
}
|
||||
Generated
+2771
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "netork-website",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
+26
@@ -0,0 +1,26 @@
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
||||
import Nav from './components/Nav'
|
||||
import Footer from './components/Footer'
|
||||
import Home from './pages/Home'
|
||||
import Features from './pages/Features'
|
||||
import Drivers from './pages/Drivers'
|
||||
import GettingStarted from './pages/GettingStarted'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-slate-950 flex flex-col">
|
||||
<Nav />
|
||||
<main className="flex-1">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/features" element={<Features />} />
|
||||
<Route path="/drivers" element={<Drivers />} />
|
||||
<Route path="/docs/getting-started" element={<GettingStarted />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-slate-900 border-t border-slate-800 mt-auto">
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
<div className="flex flex-col md:flex-row gap-10 md:gap-16">
|
||||
<div className="shrink-0">
|
||||
<Link to="/" className="flex items-center gap-2.5 mb-3">
|
||||
<img src="/logo.png" alt="" className="h-6 w-6" aria-hidden="true" />
|
||||
<span className="font-semibold text-slate-100 tracking-tight">
|
||||
net<span className="text-sky-400">Ork</span>
|
||||
</span>
|
||||
</Link>
|
||||
<p className="text-sm text-slate-500">Self-hosted network orchestration</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-10 flex-1 text-sm">
|
||||
<div>
|
||||
<p className="font-medium text-slate-400 mb-3">Links</p>
|
||||
<ul className="space-y-2">
|
||||
<li><Link to="/features" className="text-slate-500 hover:text-slate-100 transition-colors">Features</Link></li>
|
||||
<li><Link to="/drivers" className="text-slate-500 hover:text-slate-100 transition-colors">Drivers</Link></li>
|
||||
<li><Link to="/plugins" className="text-slate-500 hover:text-slate-100 transition-colors">Plugins</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-slate-400 mb-3">Resources</p>
|
||||
<ul className="space-y-2">
|
||||
<li><Link to="/docs/getting-started" className="text-slate-500 hover:text-slate-100 transition-colors">Getting Started</Link></li>
|
||||
<li><Link to="/docs/architecture" className="text-slate-500 hover:text-slate-100 transition-colors">Architecture</Link></li>
|
||||
<li><span className="text-slate-600">Changelog</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-slate-400 mb-3">Legal</p>
|
||||
<ul className="space-y-2">
|
||||
<li><span className="text-slate-600">MIT License</span></li>
|
||||
<li><span className="text-slate-600">Privacy (none collected)</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
export default function Nav() {
|
||||
const [docsOpen, setDocsOpen] = useState(false)
|
||||
const location = useLocation()
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setDocsOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setDocsOpen(false)
|
||||
}, [location.pathname])
|
||||
|
||||
const linkCls = (path: string) =>
|
||||
`text-sm transition-colors ${
|
||||
location.pathname === path
|
||||
? 'text-slate-100'
|
||||
: 'text-slate-400 hover:text-slate-100'
|
||||
}`
|
||||
|
||||
const isDocsActive = location.pathname.startsWith('/docs')
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-10 bg-slate-900/80 backdrop-blur border-b border-slate-800">
|
||||
<div className="max-w-7xl mx-auto px-6 h-14 flex items-center gap-8">
|
||||
<Link to="/" className="flex items-center gap-2.5 mr-2">
|
||||
<img src="/logo.png" alt="" className="h-7 w-7" aria-hidden="true" />
|
||||
<span className="font-semibold text-slate-100 tracking-tight">
|
||||
net<span className="text-sky-400">Ork</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Link to="/features" className={linkCls('/features')}>
|
||||
Features
|
||||
</Link>
|
||||
|
||||
<Link to="/drivers" className={linkCls('/drivers')}>
|
||||
Drivers
|
||||
</Link>
|
||||
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setDocsOpen((v) => !v)}
|
||||
className={`flex items-center gap-1 text-sm transition-colors ${
|
||||
isDocsActive ? 'text-slate-100' : 'text-slate-400 hover:text-slate-100'
|
||||
}`}
|
||||
>
|
||||
Docs
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 transition-transform ${docsOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
{docsOpen && (
|
||||
<div className="absolute top-full mt-2 left-0 w-48 rounded-lg border border-slate-800 bg-slate-900 py-1 shadow-xl">
|
||||
<Link
|
||||
to="/docs/getting-started"
|
||||
className="block px-4 py-2 text-sm text-slate-400 hover:text-slate-100 hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
Getting Started
|
||||
</Link>
|
||||
<Link
|
||||
to="/docs/architecture"
|
||||
className="block px-4 py-2 text-sm text-slate-400 hover:text-slate-100 hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
Architecture
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link to="/plugins" className={linkCls('/plugins')}>
|
||||
Plugins
|
||||
</Link>
|
||||
|
||||
<div className="ml-auto">
|
||||
<Link
|
||||
to="/docs/getting-started"
|
||||
className="inline-flex items-center gap-2 px-4 py-1.5 text-sm rounded-lg bg-sky-600 hover:bg-sky-500 text-white font-medium transition-colors"
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
@apply bg-slate-950 text-slate-100;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,133 @@
|
||||
type Cap = '✓' | '—'
|
||||
|
||||
interface Driver {
|
||||
name: string
|
||||
deviceType: string
|
||||
facts: Cap
|
||||
interfaces: Cap
|
||||
lldp: Cap
|
||||
vlans: Cap
|
||||
ssids: Cap
|
||||
health: Cap
|
||||
docker: Cap
|
||||
reboot: Cap
|
||||
configPush: Cap
|
||||
status: 'stable' | 'beta'
|
||||
}
|
||||
|
||||
const DRIVERS: Driver[] = [
|
||||
{ name: 'openwrt', deviceType: 'OpenWRT access points', facts: '✓', interfaces: '✓', lldp: '—', vlans: '✓', ssids: '✓', health: '✓', docker: '—', reboot: '✓', configPush: '✓', status: 'stable' },
|
||||
{ name: 'opnsense', deviceType: 'OPNsense firewalls', facts: '✓', interfaces: '✓', lldp: '✓', vlans: '✓', ssids: '—', health: '✓', docker: '—', reboot: '✓', configPush: '—', status: 'stable' },
|
||||
{ name: 'proxmox', deviceType: 'Proxmox VE hypervisors', facts: '✓', interfaces: '✓', lldp: '—', vlans: '—', ssids: '—', health: '✓', docker: '✓', reboot: '✓', configPush: '—', status: 'stable' },
|
||||
{ name: 'linux', deviceType: 'Generic Linux servers', facts: '✓', interfaces: '✓', lldp: '✓', vlans: '—', ssids: '—', health: '✓', docker: '✓', reboot: '✓', configPush: '—', status: 'stable' },
|
||||
{ name: 'procurve', deviceType: 'HP ProCurve / Aruba switches', facts: '✓', interfaces: '✓', lldp: '✓', vlans: '✓', ssids: '—', health: '✓', docker: '—', reboot: '✓', configPush: '—', status: 'stable' },
|
||||
{ name: 'tplink_jetstream', deviceType: 'TP-Link Jetstream switches', facts: '✓', interfaces: '✓', lldp: '✓', vlans: '✓', ssids: '—', health: '—', docker: '—', reboot: '✓', configPush: '—', status: 'stable' },
|
||||
{ name: 'netgear', deviceType: 'Netgear switches', facts: '✓', interfaces: '✓', lldp: '✓', vlans: '✓', ssids: '—', health: '—', docker: '—', reboot: '—', configPush: '—', status: 'beta' },
|
||||
{ name: 'fritzbox', deviceType: 'AVM Fritz!Box routers', facts: '✓', interfaces: '✓', lldp: '—', vlans: '—', ssids: '✓', health: '—', docker: '—', reboot: '✓', configPush: '—', status: 'stable' },
|
||||
{ name: 'zyxel', deviceType: 'Zyxel switches', facts: '✓', interfaces: '✓', lldp: '✓', vlans: '✓', ssids: '—', health: '—', docker: '—', reboot: '—', configPush: '—', status: 'beta' },
|
||||
{ name: 'openmediavault', deviceType: 'OpenMediaVault NAS', facts: '✓', interfaces: '✓', lldp: '—', vlans: '—', ssids: '—', health: '✓', docker: '✓', reboot: '✓', configPush: '—', status: 'stable' },
|
||||
{ name: 'sonos', deviceType: 'Sonos speakers', facts: '✓', interfaces: '—', lldp: '—', vlans: '—', ssids: '—', health: '—', docker: '—', reboot: '—', configPush: '—', status: 'beta' },
|
||||
]
|
||||
|
||||
const NAPALM_BUILTIN = [
|
||||
'Cisco IOS',
|
||||
'Cisco IOS-XE',
|
||||
'Cisco NX-OS',
|
||||
'Arista EOS',
|
||||
'Juniper JunOS',
|
||||
]
|
||||
|
||||
const CAPS: { key: keyof Driver; label: string }[] = [
|
||||
{ key: 'facts', label: 'Facts' },
|
||||
{ key: 'interfaces', label: 'Ifaces' },
|
||||
{ key: 'lldp', label: 'LLDP' },
|
||||
{ key: 'vlans', label: 'VLANs' },
|
||||
{ key: 'ssids', label: 'SSIDs' },
|
||||
{ key: 'health', label: 'Health' },
|
||||
{ key: 'docker', label: 'Docker' },
|
||||
{ key: 'reboot', label: 'Reboot' },
|
||||
{ key: 'configPush', label: 'Config push' },
|
||||
]
|
||||
|
||||
function StatusBadge({ status }: { status: 'stable' | 'beta' }) {
|
||||
return (
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${
|
||||
status === 'stable'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-yellow-500/20 text-yellow-400'
|
||||
}`}>
|
||||
{status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Drivers() {
|
||||
return (
|
||||
<div className="py-16 md:py-24">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="mb-12">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">Supported Devices</h1>
|
||||
<p className="text-base text-slate-400 leading-relaxed max-w-xl">
|
||||
netOrk ships with custom NAPALM drivers for 11 device types and supports all built-in NAPALM
|
||||
drivers. Capability availability varies by driver.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Custom drivers table */}
|
||||
<div className="mb-16 overflow-x-auto">
|
||||
<h2 className="text-xl font-semibold text-slate-200 mb-4">Custom Drivers</h2>
|
||||
<div className="rounded-xl border border-slate-800 overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 bg-slate-900">
|
||||
<th className="px-4 py-3 text-left font-medium text-slate-500">Driver</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-slate-500">Device type</th>
|
||||
{CAPS.map((c) => (
|
||||
<th key={c.key} className="px-3 py-3 text-center font-medium text-slate-500 whitespace-nowrap">
|
||||
{c.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="px-4 py-3 text-left font-medium text-slate-500">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{DRIVERS.map((d) => (
|
||||
<tr key={d.name} className="border-b border-slate-800 last:border-0 hover:bg-slate-800/40 transition-colors">
|
||||
<td className="px-4 py-3 font-mono text-sky-400 whitespace-nowrap">{d.name}</td>
|
||||
<td className="px-4 py-3 text-slate-300 whitespace-nowrap">{d.deviceType}</td>
|
||||
{CAPS.map((c) => (
|
||||
<td key={c.key} className="px-3 py-3 text-center">
|
||||
<span className={d[c.key] === '✓' ? 'text-green-400' : 'text-slate-700'}>
|
||||
{d[c.key]}
|
||||
</span>
|
||||
</td>
|
||||
))}
|
||||
<td className="px-4 py-3"><StatusBadge status={d.status} /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* NAPALM built-ins */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-200 mb-4">Built-in NAPALM Drivers</h2>
|
||||
<p className="text-sm text-slate-400 mb-6">
|
||||
All standard NAPALM drivers are supported. Capabilities depend on the upstream NAPALM implementation.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{NAPALM_BUILTIN.map((name) => (
|
||||
<span
|
||||
key={name}
|
||||
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"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
function Section({ title, items }: { title: string; items: string[] }) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-200 mb-4 pb-2 border-b border-slate-800">
|
||||
{title}
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<li key={item} className="flex items-start gap-2 text-sm text-slate-400 leading-relaxed">
|
||||
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{
|
||||
title: 'Device Management',
|
||||
items: [
|
||||
'CRUD for devices with credential profiles and SSH key management',
|
||||
'Per-device poll intervals (minutes) or manual-only',
|
||||
'Status tracking: planned / staged / active / decommissioning / offline / disabled',
|
||||
'Vendor / model / OS auto-populated from NAPALM get_facts()',
|
||||
'Site assignment with FK to structured Site records',
|
||||
'AP Profile assignment for grouped OpenWRT config',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Discovery',
|
||||
items: [
|
||||
'ICMP ping sweep, SNMP scan, HTTP/HTTPS probing',
|
||||
'Device fingerprinting: vendor + platform confidence scoring',
|
||||
'FQDN resolution (reverse DNS)',
|
||||
'Manual adoption from scan results — no auto-create to avoid inventory noise',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Supported Drivers',
|
||||
items: [
|
||||
'openwrt — OpenWRT access points',
|
||||
'opnsense — OPNsense firewalls',
|
||||
'proxmox — Proxmox VE hypervisors',
|
||||
'linux — Generic Linux servers',
|
||||
'procurve — HP ProCurve / Aruba switches',
|
||||
'tplink_jetstream — TP-Link Jetstream managed switches',
|
||||
'netgear — Netgear switches',
|
||||
'fritzbox — AVM Fritz!Box routers',
|
||||
'zyxel — Zyxel switches',
|
||||
'openmediavault — OpenMediaVault NAS',
|
||||
'sonos — Sonos speakers',
|
||||
'Plus all built-in NAPALM drivers: Cisco IOS / IOS-XE / NX-OS, Arista EOS, Juniper JunOS',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Networking & Inventory',
|
||||
items: [
|
||||
'Interface browser with IPv4/IPv6 addresses, MAC, speed, MTU',
|
||||
'LLDP neighbor discovery and topology graph',
|
||||
'ARP table and DHCP lease browser per device',
|
||||
'Subnet browser with interface-to-subnet assignments',
|
||||
'VLAN list grouped by site; per-VLAN device membership view',
|
||||
'SSID management with push to OpenWRT APs via UCI',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Configuration Management & Drift',
|
||||
items: [
|
||||
'Config drift detection: desired state (DB) vs device state (poll snapshot)',
|
||||
'One-click drift fix stream with live SSH output in the browser',
|
||||
'UCI-based config push for OpenWRT (VLAN names, SSID settings, radio config)',
|
||||
'AP profile system: country code, HT/VHT mode, 802.11r, NTP, syslog, SSH port',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Scheduled Operations',
|
||||
items: [
|
||||
'Scheduled reboots for OpenWRT APs with per-site concurrency lock',
|
||||
'Failback cron script written to device for netOrk-unreachable scenarios',
|
||||
'Scheduled config drift fixes with time-window enforcement',
|
||||
'Package update scheduling and one-click apply',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Monitoring & Health',
|
||||
items: [
|
||||
'SNMP health metrics (CPU, memory, interface counters) via get_health_metrics()',
|
||||
'Per-device warning system with severity levels (error / warning / info)',
|
||||
'Docker container and image status (Proxmox/Linux)',
|
||||
'Service status and start/stop/restart (systemd)',
|
||||
'VM/container list with OS device cross-linking (Proxmox)',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Security Integrations',
|
||||
items: [
|
||||
'Wazuh — agent enrollment tracking, vulnerability counts by severity, recent alert history, CIS benchmark scores, one-click agent install fix stream',
|
||||
'Graylog — rsyslog forwarding status per device, one-click fix to write rule',
|
||||
'CrowdSec — org-level decisions, remediation metrics, top attack scenarios',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'DNS Management',
|
||||
items: [
|
||||
'DNS zone management with authoritative device assignment',
|
||||
'Forward record provisioning (A records from device interfaces)',
|
||||
'PTR record provisioning to reverse zones',
|
||||
'Pending job queue for zone changes when device is unreachable',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Access Control (RBAC)',
|
||||
items: [
|
||||
'JWT authentication with remember-me (localStorage) or session-only (sessionStorage)',
|
||||
'RBAC with four built-in roles: viewer / operator / engineer / administrator',
|
||||
'Custom roles with any permission combination',
|
||||
'Full audit log of all orchestration actions',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'NetBox Sync',
|
||||
items: [
|
||||
'Pushes vendor, model, OS version, status to NetBox dcim.devices',
|
||||
'Syncs interfaces, IP addresses, prefixes, VLANs',
|
||||
'VM interfaces and disks for Proxmox hosts',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Developer Experience',
|
||||
items: [
|
||||
'OpenAPI / Swagger at /docs',
|
||||
'Plugin system: new integrations follow a documented pattern',
|
||||
'Celery task queue with dedicated queues per workload type',
|
||||
'Docker Compose deployment (single command)',
|
||||
'Alembic migrations run automatically on deploy',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default function Features() {
|
||||
return (
|
||||
<div className="py-16 md:py-24">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="mb-16">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">Features</h1>
|
||||
<p className="text-base text-slate-400 leading-relaxed max-w-xl">
|
||||
Complete reference for all netOrk capabilities.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-12">
|
||||
{SECTIONS.map((s) => (
|
||||
<Section key={s.title} title={s.title} items={s.items} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
function CodeBlock({ code }: { code: string }) {
|
||||
return (
|
||||
<pre className="rounded-xl border border-slate-800 bg-slate-900 p-6 font-mono text-sm text-slate-300 overflow-x-auto">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
function Step({ n, title, children }: { n: number; title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex gap-6">
|
||||
<div className="shrink-0 flex h-8 w-8 items-center justify-center rounded-full bg-sky-600/20 text-sky-400 text-sm font-semibold">
|
||||
{n}
|
||||
</div>
|
||||
<div className="flex-1 pb-10">
|
||||
<h2 className="text-xl font-semibold text-slate-200 mb-4">{title}</h2>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function GettingStarted() {
|
||||
return (
|
||||
<div className="py-16 md:py-24">
|
||||
<div className="max-w-3xl mx-auto px-6">
|
||||
<div className="mb-12">
|
||||
<p className="text-sm font-medium text-sky-400 mb-2">Documentation</p>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">Getting Started</h1>
|
||||
<p className="text-base text-slate-400 leading-relaxed">
|
||||
Get netOrk running from zero to a managed device in under 10 minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-800 pt-10">
|
||||
<Step n={1} title="Prerequisites">
|
||||
<ul className="space-y-2 mb-0">
|
||||
{[
|
||||
'Docker + Docker Compose (v2)',
|
||||
'A Linux host reachable from the server by SSH',
|
||||
'PostgreSQL (or use the bundled Compose profile)',
|
||||
'Redis (or use the bundled Compose profile)',
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-start gap-2 text-sm text-slate-400">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Step>
|
||||
|
||||
<Step n={2} title="Quick start">
|
||||
<p className="text-sm text-slate-400 mb-4">
|
||||
Clone the repository, copy the example environment file, and deploy with a single command.
|
||||
</p>
|
||||
<CodeBlock code={`git clone https://gitea.example.com/netork/netork.git
|
||||
cd netork
|
||||
cp .env.example .env
|
||||
# Edit .env — set DB_URL, REDIS_URL, SECRET_KEY
|
||||
|
||||
bash scripts/deploy.sh 192.168.1.10`} />
|
||||
<p className="mt-4 text-sm text-slate-400">
|
||||
The deploy script builds images, pushes them to the target host, and starts Docker Compose.
|
||||
Alembic migrations run automatically on first start.
|
||||
</p>
|
||||
</Step>
|
||||
|
||||
<Step n={3} title="First run">
|
||||
<p className="text-sm text-slate-400 mb-4">
|
||||
Once the containers are up, navigate to your server IP in a browser.
|
||||
</p>
|
||||
<CodeBlock code={`http://192.168.1.10`} />
|
||||
<ul className="mt-4 space-y-2">
|
||||
{[
|
||||
'Complete the setup wizard — it creates the admin user and writes the initial config',
|
||||
'Log in with the admin credentials you just set',
|
||||
'You land on the device inventory — currently empty',
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-start gap-2 text-sm text-slate-400">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Step>
|
||||
|
||||
<Step n={4} title="Adding your first device">
|
||||
<ol className="space-y-3">
|
||||
{[
|
||||
'Click "Add device" and fill in hostname / IP, driver, and credentials',
|
||||
'Click "Poll" to verify connectivity — you should see facts populate (vendor, OS, model)',
|
||||
'Set a poll interval (e.g. 5 minutes) for continuous monitoring',
|
||||
'Save — the device is now active and polled on the configured schedule',
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-start gap-3 text-sm text-slate-400">
|
||||
<span className="shrink-0 text-xs font-mono text-sky-400 mt-0.5">{i + 1}.</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Step>
|
||||
|
||||
<Step n={5} title="Next steps">
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
{[
|
||||
{ heading: 'NetBox sync', body: 'Connect netOrk to your NetBox instance to keep device records in sync automatically.' },
|
||||
{ heading: 'Wazuh', body: 'Enable the Wazuh plugin to track agent enrollment and CVE counts per device.' },
|
||||
{ heading: 'Scheduled reboots', body: 'Configure scheduled AP reboots for OpenWRT devices (GTK key rotation workaround).' },
|
||||
].map((card) => (
|
||||
<div key={card.heading} className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||
<h3 className="text-sm font-semibold text-slate-100 mb-2">{card.heading}</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">{card.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Step>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-800 pt-8 flex gap-4">
|
||||
<Link
|
||||
to="/drivers"
|
||||
className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium"
|
||||
>
|
||||
Supported drivers →
|
||||
</Link>
|
||||
<Link
|
||||
to="/features"
|
||||
className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium"
|
||||
>
|
||||
Full feature list →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user