feat: add persona landing pages for IT department, IT support, and MSPs
CI / TypeScript — type-check (push) Successful in 9s
CI / Publish — build & push image (push) Successful in 9s
CI / Deploy — pull & restart on host (push) Successful in 2s

Adds three "who it's for" pages reachable via a new nav dropdown, each
built around a distinct day-to-day workflow rather than a reworded
homepage pitch:

- /for/it-department — the core admin/engineer audience: drift-fix,
  Ansible automation, VM provisioning, dashboards
- /for/it-support — day-to-day operators: status at a glance, one-click
  Ack, Wake-on-LAN, scheduled reboots, filterable audit log
- /for/msp — managed service providers: Satellite deployments for
  unreachable client sites, audit trail as client-facing evidence,
  self-hosted with no per-seat SaaS

The MSP page deliberately avoids claiming per-site/per-customer RBAC —
verified against netork/models/role.py that permissions are global
role-based sets, not site-scoped, and phrased the copy accordingly.

Nav.tsx's Docs dropdown logic is extracted into a reusable NavDropdown
component (open state, outside-click, route-change-close) instead of
being duplicated for the new "Für wen" dropdown, which also links to
the existing /nis2 page rather than rebuilding that persona.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-07-17 08:54:54 +02:00
co-authored by Claude Sonnet 5
parent d57734767d
commit 2d85aa2e47
8 changed files with 534 additions and 55 deletions
+47 -5
View File
@@ -399,25 +399,67 @@ expanded into a readable overview with the architecture diagram (ASCII or SVG).
--- ---
## `/for/*` — Persona Pages
**Purpose:** Answer "is this for me?" from the perspective of a specific
buyer/user, instead of one generic homepage pitch. Reachable via the "Für
wen" / "Who it's for" nav dropdown.
**Shared layout:** hero (icon + heading + sub) → "Your day today" pain-point
cards (persona-specific, concrete workflow friction) → feature-callout cards
(only shipped capabilities, cited from `docs/PRODUCT.md`) → CTA block linking
to `/docs/getting-started`. Same card/section classes as `/plugins`.
- **`/for/it-department`** — core admin/engineer audience. Pain points:
per-vendor admin UIs, no single inventory view, undocumented config
changes, manual SSH just to check state. Features: Device Management,
config drift + one-click fix, Git-backed config history, Ansible
automation, VM Provisioning, Dashboards. Plus a short supported-drivers
strip linking to `/drivers`.
- **`/for/it-support`** — day-to-day operators, less config depth. Pain
points: "is it up right now?", repeated manual reboots, no change record,
full admin access for one ticket. Features: warning system + dashboard
widget, one-click Ack, Wake-on-LAN, scheduled reboots/updates, filterable
audit log + export, roles scoped below engineer level.
- **`/for/msp`** — managed service providers, strongest standalone buying
case. Pain points: unreachable client sites, no cross-client view, proving
what was done, client data in someone else's cloud. Features: Satellite
Deployments, automatic routing around unreachable sites (with the honest
caveat that SNMP metrics + WebSSH still need direct reach), audit trail as
client-facing evidence, per-technician custom roles (**not** phrased as
per-site RBAC — netOrk's roles are global permission sets, not
site-scoped), self-hosted/no per-seat SaaS.
The existing `/nis2` page (security/compliance persona) is linked from the
same dropdown rather than duplicated.
---
## Global Layout ## Global Layout
### Navigation (all pages) ### Navigation (all pages)
``` ```
[ netOrk ] Features Drivers Docs ▾ Plugins [ Get started ] [ netOrk ] Features Drivers Docs ▾ Für wen ▾ Plugins Roadmap [ Get started ]
``` ```
`Docs` is a dropdown: Getting Started / Architecture `Docs ▾`: Getting Started / Architecture / NIS2 Compliance / Glossary
`Für wen ▾`: IT Department / IT Support / MSP / NIS2 Compliance (reuses the Docs dropdown's NIS2 link/label)
### Footer ### Footer
``` ```
netOrk — self-hosted network orchestration netOrk — self-hosted network orchestration
Links: Resources: Legal: Links: Resources: Legal:
Features Getting Started MIT License Features Getting Started MIT License
Drivers Architecture Privacy (none collected) Drivers Architecture Privacy (none collected)
Plugins Changelog Plugins Changelog
Roadmap NIS2
Glossary
For IT Departments
For IT Support
For MSPs
``` ```
Footer background: `bg-slate-900 border-t border-slate-800` Footer background: `bg-slate-900 border-t border-slate-800`
+6
View File
@@ -9,6 +9,9 @@ import Roadmap from './pages/Roadmap'
import Nis2 from './pages/Nis2' import Nis2 from './pages/Nis2'
import Plugins from './pages/Plugins' import Plugins from './pages/Plugins'
import Glossary from './pages/Glossary' import Glossary from './pages/Glossary'
import ForItDepartment from './pages/ForItDepartment'
import ForItSupport from './pages/ForItSupport'
import ForMsp from './pages/ForMsp'
export default function App() { export default function App() {
return ( return (
@@ -25,6 +28,9 @@ export default function App() {
<Route path="/nis2" element={<Nis2 />} /> <Route path="/nis2" element={<Nis2 />} />
<Route path="/plugins" element={<Plugins />} /> <Route path="/plugins" element={<Plugins />} />
<Route path="/glossary" element={<Glossary />} /> <Route path="/glossary" element={<Glossary />} />
<Route path="/for/it-department" element={<ForItDepartment />} />
<Route path="/for/it-support" element={<ForItSupport />} />
<Route path="/for/msp" element={<ForMsp />} />
</Routes> </Routes>
</main> </main>
<Footer /> <Footer />
+3
View File
@@ -36,6 +36,9 @@ export default function Footer() {
<li><span className="text-slate-600">{t.footer.changelog}</span></li> <li><span className="text-slate-600">{t.footer.changelog}</span></li>
<li><Link to="/nis2" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.nis2}</Link></li> <li><Link to="/nis2" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.nis2}</Link></li>
<li><Link to="/glossary" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.glossary}</Link></li> <li><Link to="/glossary" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.glossary}</Link></li>
<li><Link to="/for/it-department" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.forItDepartment}</Link></li>
<li><Link to="/for/it-support" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.forItSupport}</Link></li>
<li><Link to="/for/msp" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.forMsp}</Link></li>
</ul> </ul>
</div> </div>
<div> <div>
+74 -50
View File
@@ -1,19 +1,29 @@
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect, type ReactNode } from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { ChevronDownIcon } from '@heroicons/react/24/outline' import { ChevronDownIcon } from '@heroicons/react/24/outline'
import { useLang } from '../context/LangContext' import { useLang } from '../context/LangContext'
import type { Lang } from '../i18n/translations' import type { Lang } from '../i18n/translations'
export default function Nav() { const dropdownItemCls =
const [docsOpen, setDocsOpen] = useState(false) 'block px-4 py-2 text-sm text-slate-400 hover:text-slate-100 hover:bg-slate-800 transition-colors'
function NavDropdown({
label,
active,
children,
}: {
label: string
active: boolean
children: ReactNode
}) {
const [open, setOpen] = useState(false)
const location = useLocation() const location = useLocation()
const dropdownRef = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const { lang, setLang, t } = useLang()
useEffect(() => { useEffect(() => {
function handleClickOutside(e: MouseEvent) { function handleClickOutside(e: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { if (ref.current && !ref.current.contains(e.target as Node)) {
setDocsOpen(false) setOpen(false)
} }
} }
document.addEventListener('mousedown', handleClickOutside) document.addEventListener('mousedown', handleClickOutside)
@@ -21,9 +31,33 @@ export default function Nav() {
}, []) }, [])
useEffect(() => { useEffect(() => {
setDocsOpen(false) setOpen(false)
}, [location.pathname]) }, [location.pathname])
return (
<div className="relative" ref={ref}>
<button
onClick={() => setOpen((v) => !v)}
className={`flex items-center gap-1 text-sm transition-colors ${
active ? 'text-slate-100' : 'text-slate-400 hover:text-slate-100'
}`}
>
{label}
<ChevronDownIcon className={`h-4 w-4 transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<div className="absolute top-full mt-2 left-0 w-52 rounded-lg border border-slate-800 bg-slate-900 py-1 shadow-xl">
{children}
</div>
)}
</div>
)
}
export default function Nav() {
const location = useLocation()
const { lang, setLang, t } = useLang()
const linkCls = (path: string) => const linkCls = (path: string) =>
`text-sm transition-colors ${ `text-sm transition-colors ${
location.pathname === path location.pathname === path
@@ -33,6 +67,7 @@ export default function Nav() {
const isDocsActive = const isDocsActive =
location.pathname.startsWith('/docs') || location.pathname === '/nis2' || location.pathname === '/glossary' location.pathname.startsWith('/docs') || location.pathname === '/nis2' || location.pathname === '/glossary'
const isForActive = location.pathname.startsWith('/for/') || location.pathname === '/nis2'
return ( return (
<nav className="sticky top-0 z-10 bg-slate-900/80 backdrop-blur border-b border-slate-800"> <nav className="sticky top-0 z-10 bg-slate-900/80 backdrop-blur border-b border-slate-800">
@@ -52,48 +87,37 @@ export default function Nav() {
{t.nav.drivers} {t.nav.drivers}
</Link> </Link>
<div className="relative" ref={dropdownRef}> <NavDropdown label={t.nav.docs} active={isDocsActive}>
<button <Link to="/docs/getting-started" className={dropdownItemCls}>
onClick={() => setDocsOpen((v) => !v)} {t.nav.docsItems.gettingStarted}
className={`flex items-center gap-1 text-sm transition-colors ${ </Link>
isDocsActive ? 'text-slate-100' : 'text-slate-400 hover:text-slate-100' <Link to="/docs/architecture" className={dropdownItemCls}>
}`} {t.nav.docsItems.architecture}
> </Link>
{t.nav.docs} <div className="my-1 border-t border-slate-800" />
<ChevronDownIcon <Link to="/nis2" className={dropdownItemCls}>
className={`h-4 w-4 transition-transform ${docsOpen ? 'rotate-180' : ''}`} {t.nav.docsItems.nis2}
/> </Link>
</button> <Link to="/glossary" className={dropdownItemCls}>
{docsOpen && ( {t.nav.docsItems.glossary}
<div className="absolute top-full mt-2 left-0 w-52 rounded-lg border border-slate-800 bg-slate-900 py-1 shadow-xl"> </Link>
<Link </NavDropdown>
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" <NavDropdown label={t.nav.forWhom} active={isForActive}>
> <Link to="/for/it-department" className={dropdownItemCls}>
{t.nav.docsItems.gettingStarted} {t.nav.forItems.itDepartment}
</Link> </Link>
<Link <Link to="/for/it-support" className={dropdownItemCls}>
to="/docs/architecture" {t.nav.forItems.itSupport}
className="block px-4 py-2 text-sm text-slate-400 hover:text-slate-100 hover:bg-slate-800 transition-colors" </Link>
> <Link to="/for/msp" className={dropdownItemCls}>
{t.nav.docsItems.architecture} {t.nav.forItems.msp}
</Link> </Link>
<div className="my-1 border-t border-slate-800" /> <div className="my-1 border-t border-slate-800" />
<Link <Link to="/nis2" className={dropdownItemCls}>
to="/nis2" {t.nav.docsItems.nis2}
className="block px-4 py-2 text-sm text-slate-400 hover:text-slate-100 hover:bg-slate-800 transition-colors" </Link>
> </NavDropdown>
{t.nav.docsItems.nis2}
</Link>
<Link
to="/glossary"
className="block px-4 py-2 text-sm text-slate-400 hover:text-slate-100 hover:bg-slate-800 transition-colors"
>
{t.nav.docsItems.glossary}
</Link>
</div>
)}
</div>
<Link to="/plugins" className={linkCls('/plugins')}> <Link to="/plugins" className={linkCls('/plugins')}>
{t.nav.plugins} {t.nav.plugins}
+72
View File
@@ -14,6 +14,12 @@ const en = {
nis2: 'NIS2 Compliance', nis2: 'NIS2 Compliance',
glossary: 'Glossary', glossary: 'Glossary',
}, },
forWhom: 'Who it\'s for',
forItems: {
itDepartment: 'IT Department',
itSupport: 'IT Support',
msp: 'MSP',
},
}, },
footer: { footer: {
tagline: 'Self-hosted network orchestration', tagline: 'Self-hosted network orchestration',
@@ -26,6 +32,9 @@ const en = {
nis2: 'NIS2', nis2: 'NIS2',
roadmap: 'Roadmap', roadmap: 'Roadmap',
glossary: 'Glossary', glossary: 'Glossary',
forItDepartment: 'For IT Departments',
forItSupport: 'For IT Support',
forMsp: 'For MSPs',
mit: 'MIT License', mit: 'MIT License',
privacy: 'Privacy (none collected)', privacy: 'Privacy (none collected)',
}, },
@@ -374,6 +383,33 @@ const en = {
heading: 'Glossary', heading: 'Glossary',
sub: 'Every abbreviation and technical term used on this site, in one place. Hover any underlined term anywhere on the site for a quick definition.', sub: 'Every abbreviation and technical term used on this site, in one place. Hover any underlined term anywhere on the site for a quick definition.',
}, },
forItDepartment: {
heading: 'For the team that runs the network',
sub: 'One UI for every vendor in your stack, drift detection that catches unauthorized changes, and automation that scales past manual SSH sessions.',
cta: {
heading: 'See it on your own devices.',
body: 'Deploy in one command, add your first device, and watch the first poll come in.',
button: 'Get started →',
},
},
forItSupport: {
heading: 'Day-to-day operations, without the config depth',
sub: 'Device status at a glance, one-click fixes, and a full record of who did what — without needing engineer-level access to every device.',
cta: {
heading: 'Get status at a glance.',
body: 'Deploy in one command and see every device\'s status on one dashboard.',
button: 'Get started →',
},
},
forMsp: {
heading: 'One platform, every client site',
sub: 'Reach sites you can\'t touch directly, hand clients a real audit trail, and keep every deployment self-hosted — no per-seat SaaS, no client data in someone else\'s cloud.',
cta: {
heading: 'Deploy your first satellite site.',
body: 'One Docker Compose command per deployment — yours or a client\'s infrastructure.',
button: 'Get started →',
},
},
} }
type Translations = typeof en type Translations = typeof en
@@ -392,6 +428,12 @@ const de: Translations = {
nis2: 'NIS2-Compliance', nis2: 'NIS2-Compliance',
glossary: 'Glossar', glossary: 'Glossar',
}, },
forWhom: 'Für wen',
forItems: {
itDepartment: 'IT-Abteilung',
itSupport: 'IT-Support',
msp: 'MSP',
},
}, },
footer: { footer: {
tagline: 'Self-hosted Netzwerk-Orchestrierung', tagline: 'Self-hosted Netzwerk-Orchestrierung',
@@ -404,6 +446,9 @@ const de: Translations = {
nis2: 'NIS2', nis2: 'NIS2',
roadmap: 'Roadmap', roadmap: 'Roadmap',
glossary: 'Glossar', glossary: 'Glossar',
forItDepartment: 'Für IT-Abteilungen',
forItSupport: 'Für IT-Support',
forMsp: 'Für MSPs',
mit: 'MIT-Lizenz', mit: 'MIT-Lizenz',
privacy: 'Datenschutz (keine Daten erhoben)', privacy: 'Datenschutz (keine Daten erhoben)',
}, },
@@ -752,6 +797,33 @@ const de: Translations = {
heading: 'Glossar', heading: 'Glossar',
sub: 'Alle Abkürzungen und Fachbegriffe dieser Website an einem Ort. Beim Hovern über einen unterstrichenen Begriff wird überall auf der Seite eine Kurzdefinition angezeigt.', sub: 'Alle Abkürzungen und Fachbegriffe dieser Website an einem Ort. Beim Hovern über einen unterstrichenen Begriff wird überall auf der Seite eine Kurzdefinition angezeigt.',
}, },
forItDepartment: {
heading: 'Für das Team, das das Netzwerk betreibt',
sub: 'Eine Oberfläche für jeden Hersteller im Stack, Drift-Erkennung, die nicht autorisierte Änderungen abfängt, und Automatisierung, die über manuelle SSH-Sitzungen hinaus skaliert.',
cta: {
heading: 'Auf euren eigenen Geräten ausprobieren.',
body: 'Mit einem Befehl deployen, erstes Gerät hinzufügen und den ersten Poll live sehen.',
button: 'Loslegen →',
},
},
forItSupport: {
heading: 'Alltagsbetrieb, ohne Konfigurationstiefe',
sub: 'Gerätestatus auf einen Blick, Ein-Klick-Fixes und ein vollständiges Protokoll, wer was gemacht hat — ohne Engineer-Zugriff auf jedes Gerät zu brauchen.',
cta: {
heading: 'Status auf einen Blick.',
body: 'Mit einem Befehl deployen und den Status jedes Geräts auf einem Dashboard sehen.',
button: 'Loslegen →',
},
},
forMsp: {
heading: 'Eine Plattform, jeder Kundenstandort',
sub: 'Standorte erreichen, die ihr nicht direkt anfassen könnt, Kunden einen echten Audit-Trail vorlegen und jedes Deployment self-hosted halten — kein Pro-Seat-SaaS, keine Kundendaten in fremden Clouds.',
cta: {
heading: 'Ersten Satellite-Standort deployen.',
body: 'Ein Docker-Compose-Befehl pro Deployment — eure Infrastruktur oder die eines Kunden.',
button: 'Loslegen →',
},
},
} }
export const translations: Record<Lang, Translations> = { en, de } export const translations: Record<Lang, Translations> = { en, de }
+120
View File
@@ -0,0 +1,120 @@
import { Link } from 'react-router-dom'
import { ServerStackIcon } from '@heroicons/react/24/outline'
import { useLang } from '../context/LangContext'
import { linkify } from '../lib/glossary'
type Item = { title: string; body: string }
const SECTION_LABELS: Record<'en' | 'de', { pain: string; features: string; drivers: string }> = {
en: { pain: 'Your day today', features: 'Where netOrk helps', drivers: 'Runs on what you already have' },
de: { pain: 'Euer Alltag heute', features: 'Wo netOrk hilft', drivers: 'Läuft auf dem, was ihr schon habt' },
}
const PAIN_POINTS: Record<'en' | 'de', Item[]> = {
en: [
{ title: 'A different admin UI per vendor', body: 'OPNsense here, HP ProCurve there, a separate tab for OpenWRT and Proxmox. Every task starts with figuring out which UI to open.' },
{ title: 'No single view of what\'s running where', body: 'Vendor, model, firmware, and status per device live in nobody\'s head — or a spreadsheet that\'s already out of date.' },
{ title: 'Config changes with no audit trail', body: 'Someone tweaks a firewall rule directly on the device. Nobody documents it. It breaks something three weeks later.' },
{ title: 'Manual SSH just to check a VLAN', body: 'Verifying interface status or VLAN membership means logging into the device directly, every time.' },
],
de: [
{ title: 'Für jeden Hersteller eine andere Oberfläche', body: 'OPNsense hier, HP ProCurve dort, ein eigener Tab für OpenWRT und Proxmox. Jede Aufgabe beginnt damit, herauszufinden, welche UI überhaupt zuständig ist.' },
{ title: 'Keine einzige Ansicht, was wo läuft', body: 'Hersteller, Modell, Firmware und Status pro Gerät stehen in niemandes Kopf — oder in einer Tabelle, die längst veraltet ist.' },
{ title: 'Konfigurationsänderungen ohne Audit-Trail', body: 'Jemand ändert eine Firewall-Regel direkt am Gerät. Niemand dokumentiert es. Drei Wochen später bricht etwas.' },
{ title: 'Manuelles SSH nur um ein VLAN zu prüfen', body: 'Um Interface-Status oder VLAN-Zugehörigkeit zu verifizieren, muss man sich jedes Mal direkt am Gerät einloggen.' },
],
}
const FEATURES: Record<'en' | 'de', Item[]> = {
en: [
{ title: 'Device Management', body: 'CRUD for every device with credential profiles and SSH key management. Vendor, model, and OS auto-populate from NAPALM get_facts() — no manual data entry.' },
{ title: 'Config drift detection & one-click fix', body: 'Every poll compares device state against the desired state in netOrk. Drifted devices get a warning; fixing it streams live SSH output straight to the browser.' },
{ title: 'Git-backed config history', body: 'Every poll snapshots the config into a local Git repository — full history, side-by-side diffs between any two points in time, and one-click restore for OPNsense.' },
{ title: 'Ansible automation', body: '11 built-in roles (base, docker, adguard, wireguard, fail2ban, and more) with automatic dependency resolution. Write your own roles too, with full run history per device.' },
{ title: 'VM Provisioning', body: 'Cloud-Init VMs on Proxmox straight from the hypervisor\'s VMs tab — pick an image, a VLAN, and an IP, and netOrk handles the DHCP reservation and Device linking.' },
{ title: 'Dashboards', body: 'Configurable, shareable dashboards built from 13 widgets on a WYSIWYG grid — replace the fixed layout with the view your team actually needs.' },
],
de: [
{ title: 'Geräteverwaltung', body: 'CRUD für jedes Gerät mit Credential-Profilen und SSH-Schlüsselverwaltung. Hersteller, Modell und OS werden automatisch über NAPALM get_facts() befüllt — keine manuelle Eingabe.' },
{ title: 'Konfigurationsdrift-Erkennung & Ein-Klick-Fix', body: 'Bei jedem Poll wird der Gerätezustand mit dem Sollzustand in netOrk verglichen. Abweichende Geräte erhalten eine Warnung; der Fix streamt Live-SSH-Output direkt in den Browser.' },
{ title: 'Git-basierte Konfigurationshistorie', body: 'Bei jedem Poll wird die Konfiguration in ein lokales Git-Repository gesnapshottet — vollständige Historie, Side-by-Side-Diffs zwischen beliebigen Zeitpunkten und Ein-Klick-Restore für OPNsense.' },
{ title: 'Ansible-Automatisierung', body: '11 eingebaute Rollen (base, docker, adguard, wireguard, fail2ban und mehr) mit automatischer Abhängigkeitsauflösung. Eigene Rollen schreiben inklusive vollständiger Lauf-Historie pro Gerät.' },
{ title: 'VM-Provisioning', body: 'Cloud-Init-VMs auf Proxmox direkt aus dem VMs-Tab des Hypervisors — Image, VLAN und IP auswählen, netOrk übernimmt DHCP-Reservierung und Geräteverknüpfung.' },
{ title: 'Dashboards', body: 'Konfigurierbare, teilbare Dashboards aus 13 Widgets auf einem WYSIWYG-Grid — statt festem Layout die Ansicht, die euer Team wirklich braucht.' },
],
}
export default function ForItDepartment() {
const { lang, t } = useLang()
const labels = SECTION_LABELS[lang]
const painPoints = PAIN_POINTS[lang]
const features = FEATURES[lang]
return (
<div className="py-16 md:py-24">
<div className="max-w-4xl mx-auto px-6">
<div className="mb-16">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<ServerStackIcon className="h-5 w-5 text-sky-400" />
</div>
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.forItDepartment.heading}</h1>
<p className="text-base text-slate-400 leading-relaxed max-w-2xl">{linkify(t.forItDepartment.sub)}</p>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{labels.pain}
</h2>
<div className="grid sm:grid-cols-2 gap-6">
{painPoints.map((p) => (
<div key={p.title} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
<p className="text-sm font-semibold text-slate-200 mb-1.5">{p.title}</p>
<p className="text-sm text-slate-500 leading-relaxed">{linkify(p.body)}</p>
</div>
))}
</div>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{labels.features}
</h2>
<div className="grid sm:grid-cols-2 gap-6">
{features.map((f) => (
<div key={f.title} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
<p className="text-sm font-semibold text-slate-200 mb-1.5">{f.title}</p>
<p className="text-sm text-slate-500 leading-relaxed">{linkify(f.body)}</p>
</div>
))}
</div>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-4 pb-2 border-b border-slate-800">
{labels.drivers}
</h2>
<p className="text-sm text-slate-400 leading-relaxed mb-4">
{lang === 'en'
? 'Custom NAPALM drivers for 11 device types, plus every built-in NAPALM driver.'
: 'Eigene NAPALM-Treiber für 11 Gerätetypen, plus alle integrierten NAPALM-Treiber.'}
</p>
<Link to="/drivers" className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
{lang === 'en' ? 'Full driver reference →' : 'Vollständige Treiberreferenz →'}
</Link>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900 p-8 text-center">
<h2 className="text-xl font-semibold text-slate-100 mb-3">{t.forItDepartment.cta.heading}</h2>
<p className="text-sm text-slate-400 leading-relaxed mb-6 max-w-md mx-auto">{t.forItDepartment.cta.body}</p>
<Link
to="/docs/getting-started"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-sky-600 hover:bg-sky-500 text-white text-sm font-medium transition-colors"
>
{t.forItDepartment.cta.button}
</Link>
</div>
</div>
</div>
)
}
+106
View File
@@ -0,0 +1,106 @@
import { Link } from 'react-router-dom'
import { LifebuoyIcon } from '@heroicons/react/24/outline'
import { useLang } from '../context/LangContext'
import { linkify } from '../lib/glossary'
type Item = { title: string; body: string }
const SECTION_LABELS: Record<'en' | 'de', { pain: string; features: string }> = {
en: { pain: 'Your day today', features: 'Where netOrk helps' },
de: { pain: 'Euer Alltag heute', features: 'Wo netOrk hilft' },
}
const PAIN_POINTS: Record<'en' | 'de', Item[]> = {
en: [
{ title: 'Is this device actually up right now?', body: 'Checking status means opening the vendor UI for that one device — or pinging it yourself and hoping.' },
{ title: 'The same manual reboot, over and over', body: 'A flaky AP needs a power cycle every few weeks, and someone has to remember to do it and be there for it.' },
{ title: '"Who changed this?" — with no record', body: 'A setting is different than it was yesterday. Nobody can say who touched it, when, or why.' },
{ title: 'Full admin access just to fix one ticket', body: 'Resolving a single warning shouldn\'t require the keys to the whole network configuration.' },
],
de: [
{ title: 'Läuft das Gerät gerade wirklich?', body: 'Status prüfen heißt: die Hersteller-UI für genau dieses Gerät öffnen — oder selbst pingen und hoffen.' },
{ title: 'Derselbe manuelle Reboot, immer wieder', body: 'Ein zickiger AP braucht alle paar Wochen einen Power-Cycle — und jemand muss daran denken und dafür vor Ort sein.' },
{ title: '„Wer hat das geändert?" — ohne Protokoll', body: 'Eine Einstellung ist anders als gestern. Niemand kann sagen, wer sie wann und warum geändert hat.' },
{ title: 'Voller Admin-Zugriff nur für ein Ticket', body: 'Eine einzelne Warnung zu beheben sollte nicht den Schlüssel zur gesamten Netzwerkkonfiguration erfordern.' },
],
}
const FEATURES: Record<'en' | 'de', Item[]> = {
en: [
{ title: 'Status at a glance', body: 'Per-device warning system with severity levels (error / warning / info); a dashboard warnings widget surfaces every open issue across sites without hunting per device.' },
{ title: 'One-click Ack', body: 'Clears a warning immediately and writes an audit log entry. Config-change warnings accept the current state as the new baseline in the same click.' },
{ title: 'Wake-on-LAN', body: 'Saved WOL targets per device, sent via the firewall\'s driver. On-demand "Wake now" or a recurring schedule; save a seen host as a target directly from the DHCP/ARP tabs.' },
{ title: 'Scheduled reboots & updates', body: 'Scheduled reboots for OpenWRT APs with per-site concurrency locking, and package updates scheduled or applied with one click.' },
{ title: 'Full audit log, filterable', body: 'Every action — who, when, what — filterable by date range, user, action, or resource, exportable to CSV or PDF.' },
{ title: 'Roles scoped to the job', body: 'Viewer/operator roles, or a custom permission set, hand out exactly the access support work needs — without granting engineer-level config rights.' },
],
de: [
{ title: 'Status auf einen Blick', body: 'Gerätespezifisches Warnsystem mit Schweregraden (Fehler / Warnung / Info); ein Dashboard-Warnungs-Widget zeigt jedes offene Problem standortübergreifend, ohne pro Gerät zu suchen.' },
{ title: 'Ein-Klick-Ack', body: 'Löscht eine Warnung sofort und protokolliert die Aktion im Audit-Log. Config-Change-Warnungen akzeptieren im selben Klick den aktuellen Zustand als neue Baseline.' },
{ title: 'Wake-on-LAN', body: 'Gespeicherte WOL-Ziele pro Gerät, gesendet über den Treiber der Firewall. Ein-Klick-„Jetzt wecken" oder wiederkehrender Zeitplan; ein gesehener Host lässt sich direkt aus den DHCP-/ARP-Tabs als Ziel speichern.' },
{ title: 'Geplante Reboots & Updates', body: 'Geplante Neustarts für OpenWRT-APs mit standortbezogener Concurrency-Sperre, Paket-Updates geplant oder per Ein-Klick angewendet.' },
{ title: 'Vollständiges, filterbares Audit-Log', body: 'Jede Aktion — wer, wann, was — filterbar nach Datumsbereich, Benutzer, Aktion oder Ressource, exportierbar als CSV oder PDF.' },
{ title: 'Rollen passend zur Aufgabe', body: 'Betrachter-/Operator-Rollen oder ein eigener Berechtigungssatz geben genau den Zugriff, den Support-Arbeit braucht — ohne Engineer-Rechte für die Konfiguration.' },
],
}
export default function ForItSupport() {
const { lang, t } = useLang()
const labels = SECTION_LABELS[lang]
const painPoints = PAIN_POINTS[lang]
const features = FEATURES[lang]
return (
<div className="py-16 md:py-24">
<div className="max-w-4xl mx-auto px-6">
<div className="mb-16">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<LifebuoyIcon className="h-5 w-5 text-sky-400" />
</div>
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.forItSupport.heading}</h1>
<p className="text-base text-slate-400 leading-relaxed max-w-2xl">{linkify(t.forItSupport.sub)}</p>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{labels.pain}
</h2>
<div className="grid sm:grid-cols-2 gap-6">
{painPoints.map((p) => (
<div key={p.title} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
<p className="text-sm font-semibold text-slate-200 mb-1.5">{p.title}</p>
<p className="text-sm text-slate-500 leading-relaxed">{linkify(p.body)}</p>
</div>
))}
</div>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{labels.features}
</h2>
<div className="grid sm:grid-cols-2 gap-6">
{features.map((f) => (
<div key={f.title} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
<p className="text-sm font-semibold text-slate-200 mb-1.5">{f.title}</p>
<p className="text-sm text-slate-500 leading-relaxed">{linkify(f.body)}</p>
</div>
))}
</div>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900 p-8 text-center">
<h2 className="text-xl font-semibold text-slate-100 mb-3">{t.forItSupport.cta.heading}</h2>
<p className="text-sm text-slate-400 leading-relaxed mb-6 max-w-md mx-auto">{t.forItSupport.cta.body}</p>
<Link
to="/docs/getting-started"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-sky-600 hover:bg-sky-500 text-white text-sm font-medium transition-colors"
>
{t.forItSupport.cta.button}
</Link>
</div>
</div>
</div>
)
}
+106
View File
@@ -0,0 +1,106 @@
import { Link } from 'react-router-dom'
import { BuildingOffice2Icon } from '@heroicons/react/24/outline'
import { useLang } from '../context/LangContext'
import { linkify } from '../lib/glossary'
type Item = { title: string; body: string }
const SECTION_LABELS: Record<'en' | 'de', { pain: string; features: string }> = {
en: { pain: 'Your day today', features: 'Where netOrk helps' },
de: { pain: 'Euer Alltag heute', features: 'Wo netOrk hilft' },
}
const PAIN_POINTS: Record<'en' | 'de', Item[]> = {
en: [
{ title: 'Client sites you can\'t reach directly', body: 'Firewalled or disconnected networks mean juggling a VPN per client just to check a device.' },
{ title: 'No unified view across clients', body: 'Each client\'s network lives in its own silo — there\'s no single dashboard across every site you manage.' },
{ title: 'Proving what you did, and when', body: 'A client asks what changed after an incident, and there\'s no clean record to hand over — just memory and Slack messages.' },
{ title: 'Client data in someone else\'s cloud', body: 'SaaS network-management tools mean client infrastructure data sits on a vendor\'s servers you don\'t control.' },
],
de: [
{ title: 'Kundenstandorte, die ihr nicht direkt erreicht', body: 'Firewalled oder getrennte Netzwerke bedeuten VPN-Jonglieren pro Kunde, nur um ein Gerät zu prüfen.' },
{ title: 'Keine einheitliche Sicht über alle Kunden', body: 'Jedes Kundennetzwerk lebt in seiner eigenen Silo — kein einziges Dashboard über alle betreuten Standorte.' },
{ title: 'Belegen, was ihr wann gemacht habt', body: 'Ein Kunde fragt nach einem Vorfall, was sich geändert hat — und es gibt kein sauberes Protokoll, nur Erinnerung und Slack-Nachrichten.' },
{ title: 'Kundendaten in fremder Cloud', body: 'SaaS-Netzwerk-Tools bedeuten, dass die Infrastrukturdaten eurer Kunden auf Servern eines Anbieters liegen, den ihr nicht kontrolliert.' },
],
}
const FEATURES: Record<'en' | 'de', Item[]> = {
en: [
{ title: 'Satellite Deployments', body: 'A lightweight Docker agent deployed at a client site netOrk can\'t reach directly — polls devices locally and syncs results back to Central over HTTPS. Deployed in one flow via VM Provisioning.' },
{ title: 'Central auto-routes around unreachable sites', body: 'Once a satellite is online and heartbeating, Central automatically skips direct polling for that site — no manual toggling per client.' },
{ title: 'Actions work the same everywhere', body: 'Scheduled/on-demand reboots, the SNMP auto-fix flow, and discovery jobs all route through the same command channel, direct or behind a satellite. SNMP health-metric polling and WebSSH still need direct reach today.' },
{ title: 'A real audit trail to hand a client', body: 'Every orchestration action is logged — who, what, when — filterable and exportable to CSV or PDF. Evidence, not a verbal assurance.' },
{ title: 'Roles scoped per technician', body: 'Custom roles control exactly what each technician can do, from read-only visibility to full config access, per engagement.' },
{ title: 'Self-hosted, no per-seat SaaS', body: 'Docker Compose deployment, no telemetry, no cloud dependency — runs on your infrastructure or a client\'s, not a vendor\'s.' },
],
de: [
{ title: 'Satellite-Deployments', body: 'Ein leichtgewichtiger Docker-Agent an einem Kundenstandort, den netOrk nicht direkt erreicht — pollt Geräte lokal und synct Ergebnisse per HTTPS zurück an Central. In einem Ablauf per VM-Provisioning deployt.' },
{ title: 'Central umgeht nicht erreichbare Standorte automatisch', body: 'Sobald ein Satellite online ist und heartbeatet, überspringt Central automatisch das direkte Polling für diesen Standort — kein manuelles Umschalten pro Kunde.' },
{ title: 'Aktionen funktionieren überall gleich', body: 'Geplante/On-Demand-Reboots, der SNMP-Auto-Fix und Discovery-Jobs laufen über denselben Command-Kanal, egal ob direkt oder hinter einem Satellite. SNMP-Health-Metrik-Polling und WebSSH brauchen heute noch direkten Zugriff.' },
{ title: 'Ein echter Audit-Trail für den Kunden', body: 'Jede Orchestrierungsaktion wird protokolliert — wer, was, wann — filterbar und exportierbar als CSV oder PDF. Beleg statt mündlicher Zusicherung.' },
{ title: 'Rollen pro Techniker', body: 'Benutzerdefinierte Rollen legen genau fest, was jeder Techniker darf — von reinem Lesezugriff bis vollem Konfigurationszugriff, je nach Einsatz.' },
{ title: 'Self-hosted, kein Pro-Seat-SaaS', body: 'Docker-Compose-Deployment, keine Telemetrie, keine Cloud-Abhängigkeit — läuft auf eurer Infrastruktur oder der eines Kunden, nicht bei einem Anbieter.' },
],
}
export default function ForMsp() {
const { lang, t } = useLang()
const labels = SECTION_LABELS[lang]
const painPoints = PAIN_POINTS[lang]
const features = FEATURES[lang]
return (
<div className="py-16 md:py-24">
<div className="max-w-4xl mx-auto px-6">
<div className="mb-16">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<BuildingOffice2Icon className="h-5 w-5 text-sky-400" />
</div>
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.forMsp.heading}</h1>
<p className="text-base text-slate-400 leading-relaxed max-w-2xl">{linkify(t.forMsp.sub)}</p>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{labels.pain}
</h2>
<div className="grid sm:grid-cols-2 gap-6">
{painPoints.map((p) => (
<div key={p.title} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
<p className="text-sm font-semibold text-slate-200 mb-1.5">{p.title}</p>
<p className="text-sm text-slate-500 leading-relaxed">{linkify(p.body)}</p>
</div>
))}
</div>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{labels.features}
</h2>
<div className="grid sm:grid-cols-2 gap-6">
{features.map((f) => (
<div key={f.title} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
<p className="text-sm font-semibold text-slate-200 mb-1.5">{f.title}</p>
<p className="text-sm text-slate-500 leading-relaxed">{linkify(f.body)}</p>
</div>
))}
</div>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900 p-8 text-center">
<h2 className="text-xl font-semibold text-slate-100 mb-3">{t.forMsp.cta.heading}</h2>
<p className="text-sm text-slate-400 leading-relaxed mb-6 max-w-md mx-auto">{t.forMsp.cta.body}</p>
<Link
to="/docs/getting-started"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-sky-600 hover:bg-sky-500 text-white text-sm font-medium transition-colors"
>
{t.forMsp.cta.button}
</Link>
</div>
</div>
</div>
)
}