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
### 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
```
netOrk — self-hosted network orchestration
Links: Resources: Legal:
Features Getting Started MIT License
Drivers Architecture Privacy (none collected)
Links: Resources: Legal:
Features Getting Started MIT License
Drivers Architecture Privacy (none collected)
Plugins Changelog
Roadmap NIS2
Glossary
For IT Departments
For IT Support
For MSPs
```
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 Plugins from './pages/Plugins'
import Glossary from './pages/Glossary'
import ForItDepartment from './pages/ForItDepartment'
import ForItSupport from './pages/ForItSupport'
import ForMsp from './pages/ForMsp'
export default function App() {
return (
@@ -25,6 +28,9 @@ export default function App() {
<Route path="/nis2" element={<Nis2 />} />
<Route path="/plugins" element={<Plugins />} />
<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>
</main>
<Footer />
+3
View File
@@ -36,6 +36,9 @@ export default function Footer() {
<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="/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>
</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 { ChevronDownIcon } from '@heroicons/react/24/outline'
import { useLang } from '../context/LangContext'
import type { Lang } from '../i18n/translations'
export default function Nav() {
const [docsOpen, setDocsOpen] = useState(false)
const dropdownItemCls =
'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 dropdownRef = useRef<HTMLDivElement>(null)
const { lang, setLang, t } = useLang()
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setDocsOpen(false)
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
@@ -21,9 +31,33 @@ export default function Nav() {
}, [])
useEffect(() => {
setDocsOpen(false)
setOpen(false)
}, [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) =>
`text-sm transition-colors ${
location.pathname === path
@@ -33,6 +67,7 @@ export default function Nav() {
const isDocsActive =
location.pathname.startsWith('/docs') || location.pathname === '/nis2' || location.pathname === '/glossary'
const isForActive = location.pathname.startsWith('/for/') || location.pathname === '/nis2'
return (
<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}
</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'
}`}
>
{t.nav.docs}
<ChevronDownIcon
className={`h-4 w-4 transition-transform ${docsOpen ? 'rotate-180' : ''}`}
/>
</button>
{docsOpen && (
<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
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"
>
{t.nav.docsItems.gettingStarted}
</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"
>
{t.nav.docsItems.architecture}
</Link>
<div className="my-1 border-t border-slate-800" />
<Link
to="/nis2"
className="block px-4 py-2 text-sm text-slate-400 hover:text-slate-100 hover:bg-slate-800 transition-colors"
>
{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>
<NavDropdown label={t.nav.docs} active={isDocsActive}>
<Link to="/docs/getting-started" className={dropdownItemCls}>
{t.nav.docsItems.gettingStarted}
</Link>
<Link to="/docs/architecture" className={dropdownItemCls}>
{t.nav.docsItems.architecture}
</Link>
<div className="my-1 border-t border-slate-800" />
<Link to="/nis2" className={dropdownItemCls}>
{t.nav.docsItems.nis2}
</Link>
<Link to="/glossary" className={dropdownItemCls}>
{t.nav.docsItems.glossary}
</Link>
</NavDropdown>
<NavDropdown label={t.nav.forWhom} active={isForActive}>
<Link to="/for/it-department" className={dropdownItemCls}>
{t.nav.forItems.itDepartment}
</Link>
<Link to="/for/it-support" className={dropdownItemCls}>
{t.nav.forItems.itSupport}
</Link>
<Link to="/for/msp" className={dropdownItemCls}>
{t.nav.forItems.msp}
</Link>
<div className="my-1 border-t border-slate-800" />
<Link to="/nis2" className={dropdownItemCls}>
{t.nav.docsItems.nis2}
</Link>
</NavDropdown>
<Link to="/plugins" className={linkCls('/plugins')}>
{t.nav.plugins}
+72
View File
@@ -14,6 +14,12 @@ const en = {
nis2: 'NIS2 Compliance',
glossary: 'Glossary',
},
forWhom: 'Who it\'s for',
forItems: {
itDepartment: 'IT Department',
itSupport: 'IT Support',
msp: 'MSP',
},
},
footer: {
tagline: 'Self-hosted network orchestration',
@@ -26,6 +32,9 @@ const en = {
nis2: 'NIS2',
roadmap: 'Roadmap',
glossary: 'Glossary',
forItDepartment: 'For IT Departments',
forItSupport: 'For IT Support',
forMsp: 'For MSPs',
mit: 'MIT License',
privacy: 'Privacy (none collected)',
},
@@ -374,6 +383,33 @@ const en = {
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.',
},
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
@@ -392,6 +428,12 @@ const de: Translations = {
nis2: 'NIS2-Compliance',
glossary: 'Glossar',
},
forWhom: 'Für wen',
forItems: {
itDepartment: 'IT-Abteilung',
itSupport: 'IT-Support',
msp: 'MSP',
},
},
footer: {
tagline: 'Self-hosted Netzwerk-Orchestrierung',
@@ -404,6 +446,9 @@ const de: Translations = {
nis2: 'NIS2',
roadmap: 'Roadmap',
glossary: 'Glossar',
forItDepartment: 'Für IT-Abteilungen',
forItSupport: 'Für IT-Support',
forMsp: 'Für MSPs',
mit: 'MIT-Lizenz',
privacy: 'Datenschutz (keine Daten erhoben)',
},
@@ -752,6 +797,33 @@ const de: Translations = {
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.',
},
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 }
+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>
)
}