feat: add persona landing pages for IT department, IT support, and MSPs
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:
co-authored by
Claude Sonnet 5
parent
d57734767d
commit
2d85aa2e47
+47
-5
@@ -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`
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user