feat: add site-wide glossary with hover tooltips
Every abbreviation and technical term used in the site's copy (RBAC, NAPALM, config drift, ...) now links to a new /glossary page and shows a short definition on hover, wherever it appears in body text. Product and vendor brand names are deliberately excluded — the glossary stays a dictionary of vocabulary, not a company directory. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2a8a87bdd1
commit
38c7fb1db9
@@ -8,6 +8,7 @@ import GettingStarted from './pages/GettingStarted'
|
|||||||
import Roadmap from './pages/Roadmap'
|
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'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -23,6 +24,7 @@ export default function App() {
|
|||||||
<Route path="/roadmap" element={<Roadmap />} />
|
<Route path="/roadmap" element={<Roadmap />} />
|
||||||
<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 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export default function Footer() {
|
|||||||
<li><Link to="/docs/architecture" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.architecture}</Link></li>
|
<li><Link to="/docs/architecture" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.architecture}</Link></li>
|
||||||
<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>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { useLang } from '../context/LangContext'
|
||||||
|
import { GLOSSARY } from '../glossary/terms'
|
||||||
|
|
||||||
|
const BY_ID = new Map(GLOSSARY.map((entry) => [entry.id, entry]))
|
||||||
|
|
||||||
|
export default function GlossaryMark({ id, children }: { id: string; children: ReactNode }) {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const entry = BY_ID.get(id)
|
||||||
|
if (!entry) return <>{children}</>
|
||||||
|
|
||||||
|
const name = entry.fullName ? entry.fullName[lang] : entry.display
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`/glossary#${entry.id}`}
|
||||||
|
className="group relative border-b border-dotted border-sky-500/60 hover:border-sky-400 hover:text-sky-300 transition-colors"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<span
|
||||||
|
className="pointer-events-none absolute bottom-full left-1/2 z-20 mb-2 w-64 -translate-x-1/2 rounded-lg
|
||||||
|
border border-slate-700 bg-slate-900 p-3 text-left text-xs font-normal normal-case
|
||||||
|
text-slate-400 opacity-0 shadow-2xl transition-opacity group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
<span className="mb-1 block font-semibold text-slate-200">{name}</span>
|
||||||
|
{entry.definition[lang]}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -31,7 +31,8 @@ export default function Nav() {
|
|||||||
: 'text-slate-400 hover:text-slate-100'
|
: 'text-slate-400 hover:text-slate-100'
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const isDocsActive = location.pathname.startsWith('/docs') || location.pathname === '/nis2'
|
const isDocsActive =
|
||||||
|
location.pathname.startsWith('/docs') || location.pathname === '/nis2' || location.pathname === '/glossary'
|
||||||
|
|
||||||
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">
|
||||||
@@ -84,6 +85,12 @@ export default function Nav() {
|
|||||||
>
|
>
|
||||||
{t.nav.docsItems.nis2}
|
{t.nav.docsItems.nis2}
|
||||||
</Link>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,579 @@
|
|||||||
|
export type GlossaryCategory = 'networking' | 'security' | 'compliance' | 'architecture' | 'general'
|
||||||
|
|
||||||
|
export interface GlossaryEntry {
|
||||||
|
id: string
|
||||||
|
category: GlossaryCategory
|
||||||
|
display: string
|
||||||
|
fullName?: { en: string; de: string }
|
||||||
|
definition: { en: string; de: string }
|
||||||
|
match: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CATEGORY_ORDER: GlossaryCategory[] = ['networking', 'security', 'compliance', 'architecture', 'general']
|
||||||
|
|
||||||
|
export const CATEGORY_LABELS: Record<GlossaryCategory, { en: string; de: string }> = {
|
||||||
|
networking: { en: 'Networking & Protocols', de: 'Netzwerk & Protokolle' },
|
||||||
|
security: { en: 'Security & Access', de: 'Sicherheit & Zugriff' },
|
||||||
|
compliance: { en: 'Compliance', de: 'Compliance' },
|
||||||
|
architecture: { en: 'netOrk Architecture', de: 'netOrk-Architektur' },
|
||||||
|
general: { en: 'General Tech', de: 'Allgemeine Technik' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GLOSSARY: GlossaryEntry[] = [
|
||||||
|
// ── Networking & Protocols ──────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'ap',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'AP',
|
||||||
|
fullName: { en: 'Access Point', de: 'Access Point' },
|
||||||
|
definition: {
|
||||||
|
en: 'A device that lets wireless clients join the network — in netOrk, typically an OpenWRT-managed radio.',
|
||||||
|
de: 'Ein Gerät, über das sich WLAN-Clients mit dem Netzwerk verbinden — in netOrk in der Regel ein von OpenWRT verwalteter Access Point.',
|
||||||
|
},
|
||||||
|
match: ['APs', 'AP'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'arp',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'ARP',
|
||||||
|
fullName: { en: 'Address Resolution Protocol', de: 'Address Resolution Protocol' },
|
||||||
|
definition: {
|
||||||
|
en: 'The protocol that maps an IP address to the physical MAC address of a device on the same network segment.',
|
||||||
|
de: 'Das Protokoll, das eine IP-Adresse auf die physische MAC-Adresse eines Geräts im selben Netzwerksegment abbildet.',
|
||||||
|
},
|
||||||
|
match: ['ARP'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dhcp',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'DHCP',
|
||||||
|
fullName: { en: 'Dynamic Host Configuration Protocol', de: 'Dynamic Host Configuration Protocol' },
|
||||||
|
definition: {
|
||||||
|
en: 'The protocol that automatically hands out IP addresses and network settings to devices as they join.',
|
||||||
|
de: 'Das Protokoll, das Geräten beim Verbinden automatisch IP-Adressen und Netzwerkeinstellungen zuweist.',
|
||||||
|
},
|
||||||
|
match: ['DHCP'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dns',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'DNS',
|
||||||
|
fullName: { en: 'Domain Name System', de: 'Domain Name System' },
|
||||||
|
definition: {
|
||||||
|
en: 'Translates human-readable hostnames into IP addresses.',
|
||||||
|
de: 'Übersetzt menschenlesbare Hostnamen in IP-Adressen.',
|
||||||
|
},
|
||||||
|
match: ['DNS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fqdn',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'FQDN',
|
||||||
|
fullName: { en: 'Fully Qualified Domain Name', de: 'Fully Qualified Domain Name' },
|
||||||
|
definition: {
|
||||||
|
en: 'The complete domain name that uniquely identifies a host, e.g. router.example.com.',
|
||||||
|
de: 'Der vollständige Domainname, der einen Host eindeutig identifiziert, z. B. router.example.com.',
|
||||||
|
},
|
||||||
|
match: ['FQDN'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'http-https',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'HTTP/HTTPS',
|
||||||
|
fullName: { en: 'Hypertext Transfer Protocol (Secure)', de: 'Hypertext Transfer Protocol (Secure)' },
|
||||||
|
definition: {
|
||||||
|
en: 'The protocol web browsers and APIs use to exchange data; HTTPS adds TLS encryption on top.',
|
||||||
|
de: 'Das Protokoll, über das Webbrowser und APIs Daten austauschen; HTTPS ergänzt es um TLS-Verschlüsselung.',
|
||||||
|
},
|
||||||
|
match: ['HTTP/HTTPS', 'HTTPS', 'HTTP'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'icmp',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'ICMP',
|
||||||
|
fullName: { en: 'Internet Control Message Protocol', de: 'Internet Control Message Protocol' },
|
||||||
|
definition: {
|
||||||
|
en: 'The protocol behind network diagnostics like ping — used by netOrk\'s discovery sweep to find live hosts.',
|
||||||
|
de: 'Das Protokoll hinter Netzwerkdiagnosen wie Ping — netOrk nutzt es beim Discovery-Sweep, um aktive Hosts zu finden.',
|
||||||
|
},
|
||||||
|
match: ['ICMP'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ip',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'IP',
|
||||||
|
fullName: { en: 'Internet Protocol', de: 'Internet Protocol' },
|
||||||
|
definition: {
|
||||||
|
en: 'The addressing scheme (IPv4/IPv6) that lets devices find and reach each other on a network.',
|
||||||
|
de: 'Das Adressierungsschema (IPv4/IPv6), über das Geräte sich im Netzwerk finden und erreichen.',
|
||||||
|
},
|
||||||
|
match: ['IP'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lldp',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'LLDP',
|
||||||
|
fullName: { en: 'Link Layer Discovery Protocol', de: 'Link Layer Discovery Protocol' },
|
||||||
|
definition: {
|
||||||
|
en: 'Lets neighboring devices advertise their identity and capabilities, which netOrk uses to build the topology graph.',
|
||||||
|
de: 'Ermöglicht benachbarten Geräten, Identität und Fähigkeiten bekanntzugeben — netOrk nutzt das für den Topologie-Graphen.',
|
||||||
|
},
|
||||||
|
match: ['LLDP'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mac',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'MAC',
|
||||||
|
fullName: { en: 'Media Access Control (address)', de: 'Media Access Control (Adresse)' },
|
||||||
|
definition: {
|
||||||
|
en: 'The hardware address burned into a network interface, unique per device.',
|
||||||
|
de: 'Die in eine Netzwerkschnittstelle eingebrannte Hardware-Adresse, eindeutig pro Gerät.',
|
||||||
|
},
|
||||||
|
match: ['MAC'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mtu',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'MTU',
|
||||||
|
fullName: { en: 'Maximum Transmission Unit', de: 'Maximum Transmission Unit' },
|
||||||
|
definition: {
|
||||||
|
en: 'The largest packet size an interface will forward without fragmenting it.',
|
||||||
|
de: 'Die größte Paketgröße, die eine Schnittstelle weiterleitet, ohne sie zu fragmentieren.',
|
||||||
|
},
|
||||||
|
match: ['MTU'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ntp',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'NTP',
|
||||||
|
fullName: { en: 'Network Time Protocol', de: 'Network Time Protocol' },
|
||||||
|
definition: {
|
||||||
|
en: 'Keeps device clocks synchronized — netOrk pushes NTP settings as part of AP profiles.',
|
||||||
|
de: 'Synchronisiert die Uhrzeit von Geräten — netOrk setzt NTP-Einstellungen als Teil von AP-Profilen.',
|
||||||
|
},
|
||||||
|
match: ['NTP'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ptr-record',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'PTR record',
|
||||||
|
definition: {
|
||||||
|
en: 'A reverse-DNS record that maps an IP address back to a hostname.',
|
||||||
|
de: 'Ein Reverse-DNS-Eintrag, der eine IP-Adresse auf einen Hostnamen zurückführt.',
|
||||||
|
},
|
||||||
|
match: ['PTR record', 'PTR'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'snmp',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'SNMP',
|
||||||
|
fullName: { en: 'Simple Network Management Protocol', de: 'Simple Network Management Protocol' },
|
||||||
|
definition: {
|
||||||
|
en: 'The protocol netOrk uses to pull health metrics — CPU, memory, interface counters — straight from devices.',
|
||||||
|
de: 'Das Protokoll, über das netOrk Gesundheitsmetriken — CPU, Speicher, Schnittstellenzähler — direkt von Geräten abfragt.',
|
||||||
|
},
|
||||||
|
match: ['SNMP'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ssh',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'SSH',
|
||||||
|
fullName: { en: 'Secure Shell', de: 'Secure Shell' },
|
||||||
|
definition: {
|
||||||
|
en: 'An encrypted remote-access protocol — netOrk uses it to run commands, push config, and stream the browser-based console.',
|
||||||
|
de: 'Ein verschlüsseltes Protokoll für den Fernzugriff — netOrk nutzt es, um Befehle auszuführen, Konfigurationen zu pushen und die browserbasierte Konsole zu streamen.',
|
||||||
|
},
|
||||||
|
match: ['SSH'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ssid',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'SSID',
|
||||||
|
fullName: { en: 'Service Set Identifier', de: 'Service Set Identifier' },
|
||||||
|
definition: {
|
||||||
|
en: 'The public name of a Wi-Fi network, e.g. what shows up in a phone\'s Wi-Fi list.',
|
||||||
|
de: 'Der öffentliche Name eines WLANs — das, was z. B. in der WLAN-Liste eines Smartphones erscheint.',
|
||||||
|
},
|
||||||
|
match: ['SSIDs', 'SSID'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vlan',
|
||||||
|
category: 'networking',
|
||||||
|
display: 'VLAN',
|
||||||
|
fullName: { en: 'Virtual Local Area Network', de: 'Virtual Local Area Network' },
|
||||||
|
definition: {
|
||||||
|
en: 'A logically segmented broadcast domain that runs on shared switching hardware.',
|
||||||
|
de: 'Eine logisch abgegrenzte Broadcast-Domäne, die auf gemeinsam genutzter Switching-Hardware läuft.',
|
||||||
|
},
|
||||||
|
match: ['VLANs', 'VLAN'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Security & Access ────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'cis-benchmark',
|
||||||
|
category: 'security',
|
||||||
|
display: 'CIS',
|
||||||
|
fullName: { en: 'Center for Internet Security', de: 'Center for Internet Security' },
|
||||||
|
definition: {
|
||||||
|
en: 'A vendor-neutral hardening standard — netOrk surfaces a device\'s CIS benchmark score via the Wazuh integration.',
|
||||||
|
de: 'Ein herstellerneutraler Hardening-Standard — netOrk zeigt den CIS-Benchmark-Score eines Geräts über die Wazuh-Integration.',
|
||||||
|
},
|
||||||
|
match: ['CIS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cve',
|
||||||
|
category: 'security',
|
||||||
|
display: 'CVE',
|
||||||
|
fullName: { en: 'Common Vulnerabilities and Exposures', de: 'Common Vulnerabilities and Exposures' },
|
||||||
|
definition: {
|
||||||
|
en: 'A public identifier for a known security vulnerability, e.g. CVE-2026-44405.',
|
||||||
|
de: 'Eine öffentliche Kennung für eine bekannte Sicherheitslücke, z. B. CVE-2026-44405.',
|
||||||
|
},
|
||||||
|
match: ['CVEs', 'CVE'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cvss',
|
||||||
|
category: 'security',
|
||||||
|
display: 'CVSS',
|
||||||
|
fullName: { en: 'Common Vulnerability Scoring System', de: 'Common Vulnerability Scoring System' },
|
||||||
|
definition: {
|
||||||
|
en: 'A standardized 0–10 score for how severe a vulnerability is.',
|
||||||
|
de: 'Ein standardisierter Score von 0–10 für den Schweregrad einer Sicherheitslücke.',
|
||||||
|
},
|
||||||
|
match: ['CVSS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'eol',
|
||||||
|
category: 'security',
|
||||||
|
display: 'EOL',
|
||||||
|
fullName: { en: 'End of Life', de: 'End of Life' },
|
||||||
|
definition: {
|
||||||
|
en: 'Software or firmware that no longer receives vendor security updates.',
|
||||||
|
de: 'Software oder Firmware, die keine Sicherheitsupdates vom Hersteller mehr erhält.',
|
||||||
|
},
|
||||||
|
match: ['EOL'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hmac-sha256',
|
||||||
|
category: 'security',
|
||||||
|
display: 'HMAC-SHA256',
|
||||||
|
fullName: { en: 'Hash-based Message Authentication Code (SHA-256)', de: 'Hash-based Message Authentication Code (SHA-256)' },
|
||||||
|
definition: {
|
||||||
|
en: 'A cryptographic signature that proves a webhook payload wasn\'t tampered with in transit.',
|
||||||
|
de: 'Eine kryptografische Signatur, die belegt, dass ein Webhook-Payload unterwegs nicht manipuliert wurde.',
|
||||||
|
},
|
||||||
|
match: ['HMAC-SHA256', 'HMAC'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jwt',
|
||||||
|
category: 'security',
|
||||||
|
display: 'JWT',
|
||||||
|
fullName: { en: 'JSON Web Token', de: 'JSON Web Token' },
|
||||||
|
definition: {
|
||||||
|
en: 'A signed token that proves a logged-in user\'s identity on every API request.',
|
||||||
|
de: 'Ein signiertes Token, das die Identität eines angemeldeten Benutzers bei jeder API-Anfrage belegt.',
|
||||||
|
},
|
||||||
|
match: ['JWT'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mfa',
|
||||||
|
category: 'security',
|
||||||
|
display: 'MFA',
|
||||||
|
fullName: { en: 'Multi-Factor Authentication', de: 'Multi-Faktor-Authentifizierung' },
|
||||||
|
definition: {
|
||||||
|
en: 'Requiring a second proof of identity — like a TOTP code — in addition to a password.',
|
||||||
|
de: 'Verlangt neben dem Passwort einen zweiten Identitätsnachweis — etwa einen TOTP-Code.',
|
||||||
|
},
|
||||||
|
match: ['MFA'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nvd',
|
||||||
|
category: 'security',
|
||||||
|
display: 'NVD',
|
||||||
|
fullName: { en: 'National Vulnerability Database', de: 'National Vulnerability Database' },
|
||||||
|
definition: {
|
||||||
|
en: 'The U.S. government\'s public feed of known CVEs.',
|
||||||
|
de: 'Die öffentliche CVE-Datenbank der US-Regierung.',
|
||||||
|
},
|
||||||
|
match: ['NVD'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'osv',
|
||||||
|
category: 'security',
|
||||||
|
display: 'OSV',
|
||||||
|
fullName: { en: 'Open Source Vulnerabilities', de: 'Open Source Vulnerabilities' },
|
||||||
|
definition: {
|
||||||
|
en: 'A community-run vulnerability database focused on open-source packages.',
|
||||||
|
de: 'Eine community-betriebene Schwachstellendatenbank mit Fokus auf Open-Source-Pakete.',
|
||||||
|
},
|
||||||
|
match: ['OSV'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rbac',
|
||||||
|
category: 'security',
|
||||||
|
display: 'RBAC',
|
||||||
|
fullName: { en: 'Role-Based Access Control', de: 'Rollenbasierte Zugriffskontrolle' },
|
||||||
|
definition: {
|
||||||
|
en: 'Restricting what a user can see or do based on the role they\'ve been assigned.',
|
||||||
|
de: 'Beschränkt, was ein Benutzer sehen oder tun darf, basierend auf der zugewiesenen Rolle.',
|
||||||
|
},
|
||||||
|
match: ['RBAC'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tls',
|
||||||
|
category: 'security',
|
||||||
|
display: 'TLS',
|
||||||
|
fullName: { en: 'Transport Layer Security', de: 'Transport Layer Security' },
|
||||||
|
definition: {
|
||||||
|
en: 'The encryption protocol behind HTTPS — also used to terminate traffic at an ingress in a Kubernetes deployment.',
|
||||||
|
de: 'Das Verschlüsselungsprotokoll hinter HTTPS — wird z. B. auch zur TLS-Terminierung am Ingress eines Kubernetes-Deployments genutzt.',
|
||||||
|
},
|
||||||
|
match: ['TLS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'totp',
|
||||||
|
category: 'security',
|
||||||
|
display: 'TOTP',
|
||||||
|
fullName: { en: 'Time-based One-Time Password', de: 'Time-based One-Time Password' },
|
||||||
|
definition: {
|
||||||
|
en: 'The rotating 6-digit code generated by an authenticator app, used as a second login factor.',
|
||||||
|
de: 'Der rotierende 6-stellige Code aus einer Authenticator-App, genutzt als zweiter Anmeldefaktor.',
|
||||||
|
},
|
||||||
|
match: ['TOTP'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Compliance ────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'nis2',
|
||||||
|
category: 'compliance',
|
||||||
|
display: 'NIS2',
|
||||||
|
fullName: { en: 'Network and Information Security Directive 2', de: 'Network and Information Security Directive 2' },
|
||||||
|
definition: {
|
||||||
|
en: 'The EU\'s second cybersecurity directive — Article 21 sets baseline technical and organizational measures for essential and important entities.',
|
||||||
|
de: 'Die zweite EU-Richtlinie zur Netzwerk- und Informationssicherheit — Art. 21 legt technische und organisatorische Mindestmaßnahmen für wesentliche und wichtige Einrichtungen fest.',
|
||||||
|
},
|
||||||
|
match: ['NIS2'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── netOrk Architecture ───────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'napalm',
|
||||||
|
category: 'architecture',
|
||||||
|
display: 'NAPALM',
|
||||||
|
fullName: {
|
||||||
|
en: 'Network Automation and Programmability Abstraction Layer with Multivendor support',
|
||||||
|
de: 'Network Automation and Programmability Abstraction Layer with Multivendor support',
|
||||||
|
},
|
||||||
|
definition: {
|
||||||
|
en: 'The open-source Python library netOrk uses to talk to network devices through one common interface, regardless of vendor.',
|
||||||
|
de: 'Die Open-Source-Python-Bibliothek, über die netOrk mit Netzwerkgeräten über eine gemeinsame Schnittstelle spricht — herstellerunabhängig.',
|
||||||
|
},
|
||||||
|
match: ['NAPALM'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'uci',
|
||||||
|
category: 'architecture',
|
||||||
|
display: 'UCI',
|
||||||
|
fullName: { en: 'Unified Configuration Interface', de: 'Unified Configuration Interface' },
|
||||||
|
definition: {
|
||||||
|
en: 'OpenWRT\'s own configuration system — netOrk pushes VLAN, SSID, and radio settings through it.',
|
||||||
|
de: 'OpenWRTs eigenes Konfigurationssystem — netOrk setzt VLAN-, SSID- und Radio-Einstellungen darüber.',
|
||||||
|
},
|
||||||
|
match: ['UCI'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'config-drift',
|
||||||
|
category: 'architecture',
|
||||||
|
display: 'Config drift',
|
||||||
|
definition: {
|
||||||
|
en: 'When a device\'s actual configuration silently diverges from the state netOrk expects — usually from a manual change made directly on the device.',
|
||||||
|
de: 'Wenn die tatsächliche Konfiguration eines Geräts stillschweigend vom erwarteten Sollzustand abweicht — meist durch eine manuelle Änderung direkt am Gerät.',
|
||||||
|
},
|
||||||
|
match: ['Config drift', 'config drift', 'Drifted', 'drifted', 'Drift', 'drift'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hook-bus',
|
||||||
|
category: 'architecture',
|
||||||
|
display: 'Hook bus',
|
||||||
|
definition: {
|
||||||
|
en: 'netOrk\'s internal event system — plugins subscribe handlers to events like poll.complete instead of core code calling into them directly.',
|
||||||
|
de: 'netOrks internes Event-System — Plugins registrieren Handler für Events wie poll.complete, statt dass der Kern-Code sie direkt aufruft.',
|
||||||
|
},
|
||||||
|
match: ['Hook bus', 'hook bus'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'plugin-registry',
|
||||||
|
category: 'architecture',
|
||||||
|
display: 'Plugin registry',
|
||||||
|
definition: {
|
||||||
|
en: 'The database-backed record of which plugins exist and whether each is enabled, checked before a plugin\'s router or hooks run.',
|
||||||
|
de: 'Die datenbankgestützte Übersicht, welche Plugins existieren und ob sie aktiviert sind — wird geprüft, bevor Router oder Hooks eines Plugins laufen.',
|
||||||
|
},
|
||||||
|
match: ['Plugin registry', 'plugin registry'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'config-snapshot',
|
||||||
|
category: 'architecture',
|
||||||
|
display: 'Configuration snapshot',
|
||||||
|
definition: {
|
||||||
|
en: 'A full copy of a device\'s configuration captured at poll time and committed to Git, so any point in history can be diffed or restored.',
|
||||||
|
de: 'Eine vollständige Kopie der Gerätekonfiguration, die bei jedem Poll erfasst und in Git committet wird — jeder Zeitpunkt lässt sich so diffen oder wiederherstellen.',
|
||||||
|
},
|
||||||
|
match: ['Configuration snapshot', 'configuration snapshot', 'Config snapshot', 'config snapshot'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── General Tech ──────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'api',
|
||||||
|
category: 'general',
|
||||||
|
display: 'API',
|
||||||
|
fullName: { en: 'Application Programming Interface', de: 'Application Programming Interface' },
|
||||||
|
definition: {
|
||||||
|
en: 'A defined set of endpoints other software — or netOrk\'s own UI — uses to talk to a system.',
|
||||||
|
de: 'Eine definierte Menge an Endpunkten, über die andere Software — oder netOrks eigene UI — mit einem System spricht.',
|
||||||
|
},
|
||||||
|
match: ['API'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'crud',
|
||||||
|
category: 'general',
|
||||||
|
display: 'CRUD',
|
||||||
|
fullName: { en: 'Create, Read, Update, Delete', de: 'Create, Read, Update, Delete' },
|
||||||
|
definition: {
|
||||||
|
en: 'The four basic operations for managing a record: creating, viewing, editing, and deleting it.',
|
||||||
|
de: 'Die vier Grundoperationen zur Verwaltung eines Datensatzes: anlegen, anzeigen, bearbeiten, löschen.',
|
||||||
|
},
|
||||||
|
match: ['CRUD'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'csv',
|
||||||
|
category: 'general',
|
||||||
|
display: 'CSV',
|
||||||
|
fullName: { en: 'Comma-Separated Values', de: 'Comma-Separated Values' },
|
||||||
|
definition: {
|
||||||
|
en: 'A plain-text spreadsheet format used for exporting tabular data like the audit log.',
|
||||||
|
de: 'Ein textbasiertes Tabellenformat für den Export tabellarischer Daten wie des Audit-Logs.',
|
||||||
|
},
|
||||||
|
match: ['CSV'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cpu',
|
||||||
|
category: 'general',
|
||||||
|
display: 'CPU',
|
||||||
|
fullName: { en: 'Central Processing Unit', de: 'Central Processing Unit' },
|
||||||
|
definition: {
|
||||||
|
en: 'A device\'s processor — netOrk tracks its load as a health metric.',
|
||||||
|
de: 'Der Prozessor eines Geräts — netOrk erfasst dessen Auslastung als Gesundheitsmetrik.',
|
||||||
|
},
|
||||||
|
match: ['CPU'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'db',
|
||||||
|
category: 'general',
|
||||||
|
display: 'DB',
|
||||||
|
fullName: { en: 'Database', de: 'Datenbank' },
|
||||||
|
definition: {
|
||||||
|
en: 'Shorthand for the database — where netOrk stores desired device state and configuration.',
|
||||||
|
de: 'Kurzform für die Datenbank — dort speichert netOrk den Sollzustand und die Konfiguration der Geräte.',
|
||||||
|
},
|
||||||
|
match: ['DB'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fk',
|
||||||
|
category: 'general',
|
||||||
|
display: 'FK',
|
||||||
|
fullName: { en: 'Foreign Key', de: 'Foreign Key' },
|
||||||
|
definition: {
|
||||||
|
en: 'A database reference from one record to another — e.g. linking a device to its Site record.',
|
||||||
|
de: 'Ein Datenbank-Verweis von einem Datensatz auf einen anderen — z. B. die Verknüpfung eines Geräts mit seinem Standort-Datensatz.',
|
||||||
|
},
|
||||||
|
match: ['FK'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nas',
|
||||||
|
category: 'general',
|
||||||
|
display: 'NAS',
|
||||||
|
fullName: { en: 'Network-Attached Storage', de: 'Network-Attached Storage' },
|
||||||
|
definition: {
|
||||||
|
en: 'A dedicated file-storage device that other machines on the network read and write to.',
|
||||||
|
de: 'Ein dediziertes Speichergerät im Netzwerk, auf das andere Rechner lesend und schreibend zugreifen.',
|
||||||
|
},
|
||||||
|
match: ['NAS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'os',
|
||||||
|
category: 'general',
|
||||||
|
display: 'OS',
|
||||||
|
fullName: { en: 'Operating System', de: 'Betriebssystem' },
|
||||||
|
definition: {
|
||||||
|
en: 'The system software running on a device — firmware, a Linux distribution, etc. — netOrk tracks its version per device.',
|
||||||
|
de: 'Die auf einem Gerät laufende Systemsoftware — Firmware, eine Linux-Distribution usw. — netOrk erfasst die Version pro Gerät.',
|
||||||
|
},
|
||||||
|
match: ['OS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf',
|
||||||
|
category: 'general',
|
||||||
|
display: 'PDF',
|
||||||
|
fullName: { en: 'Portable Document Format', de: 'Portable Document Format' },
|
||||||
|
definition: {
|
||||||
|
en: 'A fixed-layout document format used for audit log exports meant for an auditor.',
|
||||||
|
de: 'Ein Dokumentformat mit festem Layout — genutzt für Audit-Log-Exporte, die an einen Prüfer gehen.',
|
||||||
|
},
|
||||||
|
match: ['PDF'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'saas',
|
||||||
|
category: 'general',
|
||||||
|
display: 'SaaS',
|
||||||
|
fullName: { en: 'Software as a Service', de: 'Software as a Service' },
|
||||||
|
definition: {
|
||||||
|
en: 'Hosted software you access over the internet and don\'t run yourself — the opposite of netOrk\'s self-hosted model.',
|
||||||
|
de: 'Gehostete Software, die über das Internet genutzt wird, statt sie selbst zu betreiben — das Gegenteil von netOrks Self-Hosted-Modell.',
|
||||||
|
},
|
||||||
|
match: ['SaaS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sse',
|
||||||
|
category: 'general',
|
||||||
|
display: 'SSE',
|
||||||
|
fullName: { en: 'Server-Sent Events', de: 'Server-Sent Events' },
|
||||||
|
definition: {
|
||||||
|
en: 'A one-way streaming connection the server uses to push live output — like fix-stream logs — to the browser.',
|
||||||
|
de: 'Eine unidirektionale Streaming-Verbindung, über die der Server Live-Output — etwa Fix-Stream-Logs — an den Browser sendet.',
|
||||||
|
},
|
||||||
|
match: ['SSE'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ui',
|
||||||
|
category: 'general',
|
||||||
|
display: 'UI',
|
||||||
|
fullName: { en: 'User Interface', de: 'User Interface' },
|
||||||
|
definition: {
|
||||||
|
en: 'The interface you interact with — in netOrk\'s case, the web app itself.',
|
||||||
|
de: 'Die Oberfläche, mit der interagiert wird — bei netOrk die Web-App selbst.',
|
||||||
|
},
|
||||||
|
match: ['UI'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vm',
|
||||||
|
category: 'general',
|
||||||
|
display: 'VM',
|
||||||
|
fullName: { en: 'Virtual Machine', de: 'Virtuelle Maschine' },
|
||||||
|
definition: {
|
||||||
|
en: 'An emulated computer running on a hypervisor like Proxmox, sharing physical hardware with other VMs.',
|
||||||
|
de: 'Ein emulierter Computer auf einem Hypervisor wie Proxmox, der sich die physische Hardware mit anderen VMs teilt.',
|
||||||
|
},
|
||||||
|
match: ['VMs', 'VM'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'websocket',
|
||||||
|
category: 'general',
|
||||||
|
display: 'WebSocket',
|
||||||
|
definition: {
|
||||||
|
en: 'A persistent two-way connection between browser and server — used for the interactive Web-SSH console.',
|
||||||
|
de: 'Eine dauerhafte, bidirektionale Verbindung zwischen Browser und Server — genutzt für die interaktive Web-SSH-Konsole.',
|
||||||
|
},
|
||||||
|
match: ['WebSocket'],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -12,6 +12,7 @@ const en = {
|
|||||||
gettingStarted: 'Getting Started',
|
gettingStarted: 'Getting Started',
|
||||||
architecture: 'Architecture',
|
architecture: 'Architecture',
|
||||||
nis2: 'NIS2 Compliance',
|
nis2: 'NIS2 Compliance',
|
||||||
|
glossary: 'Glossary',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
@@ -24,6 +25,7 @@ const en = {
|
|||||||
changelog: 'Changelog',
|
changelog: 'Changelog',
|
||||||
nis2: 'NIS2',
|
nis2: 'NIS2',
|
||||||
roadmap: 'Roadmap',
|
roadmap: 'Roadmap',
|
||||||
|
glossary: 'Glossary',
|
||||||
mit: 'MIT License',
|
mit: 'MIT License',
|
||||||
privacy: 'Privacy (none collected)',
|
privacy: 'Privacy (none collected)',
|
||||||
},
|
},
|
||||||
@@ -295,6 +297,10 @@ const en = {
|
|||||||
button: 'Get started →',
|
button: 'Get started →',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
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.',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type Translations = typeof en
|
type Translations = typeof en
|
||||||
@@ -311,6 +317,7 @@ const de: Translations = {
|
|||||||
gettingStarted: 'Erste Schritte',
|
gettingStarted: 'Erste Schritte',
|
||||||
architecture: 'Architektur',
|
architecture: 'Architektur',
|
||||||
nis2: 'NIS2-Compliance',
|
nis2: 'NIS2-Compliance',
|
||||||
|
glossary: 'Glossar',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
@@ -323,6 +330,7 @@ const de: Translations = {
|
|||||||
changelog: 'Changelog',
|
changelog: 'Changelog',
|
||||||
nis2: 'NIS2',
|
nis2: 'NIS2',
|
||||||
roadmap: 'Roadmap',
|
roadmap: 'Roadmap',
|
||||||
|
glossary: 'Glossar',
|
||||||
mit: 'MIT-Lizenz',
|
mit: 'MIT-Lizenz',
|
||||||
privacy: 'Datenschutz (keine Daten erhoben)',
|
privacy: 'Datenschutz (keine Daten erhoben)',
|
||||||
},
|
},
|
||||||
@@ -594,6 +602,10 @@ const de: Translations = {
|
|||||||
button: 'Loslegen →',
|
button: 'Loslegen →',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
glossary: {
|
||||||
|
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.',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const translations: Record<Lang, Translations> = { en, de }
|
export const translations: Record<Lang, Translations> = { en, de }
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Fragment } from 'react'
|
||||||
|
import GlossaryMark from '../components/GlossaryMark'
|
||||||
|
import { GLOSSARY } from '../glossary/terms'
|
||||||
|
|
||||||
|
function escapeRegExp(value: string): string {
|
||||||
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
}
|
||||||
|
|
||||||
|
const MATCH_TO_ID = new Map<string, string>()
|
||||||
|
for (const entry of GLOSSARY) {
|
||||||
|
for (const surface of entry.match) {
|
||||||
|
MATCH_TO_ID.set(surface, entry.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_SURFACES = [...MATCH_TO_ID.keys()].sort((a, b) => b.length - a.length)
|
||||||
|
const GLOSSARY_REGEX = new RegExp(`\\b(${ALL_SURFACES.map(escapeRegExp).join('|')})\\b`, 'g')
|
||||||
|
|
||||||
|
/** Wraps every occurrence of a known glossary term in `text` with a hoverable, linked <GlossaryMark>. */
|
||||||
|
export function linkify(text: string): React.ReactNode {
|
||||||
|
const parts = text.split(GLOSSARY_REGEX)
|
||||||
|
if (parts.length === 1) return text
|
||||||
|
|
||||||
|
return parts.map((part, i) => {
|
||||||
|
const id = MATCH_TO_ID.get(part)
|
||||||
|
if (id === undefined) return <Fragment key={i}>{part}</Fragment>
|
||||||
|
return (
|
||||||
|
<GlossaryMark key={i} id={id}>
|
||||||
|
{part}
|
||||||
|
</GlossaryMark>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useLang } from '../context/LangContext'
|
import { useLang } from '../context/LangContext'
|
||||||
|
import { linkify } from '../lib/glossary'
|
||||||
|
|
||||||
type Cap = '✓' | '—'
|
type Cap = '✓' | '—'
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ export default function Drivers() {
|
|||||||
<div className="max-w-7xl mx-auto px-6">
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.drivers.heading}</h1>
|
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.drivers.heading}</h1>
|
||||||
<p className="text-base text-slate-400 leading-relaxed max-w-xl">{t.drivers.sub}</p>
|
<p className="text-base text-slate-400 leading-relaxed max-w-xl">{linkify(t.drivers.sub)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-16 overflow-x-auto">
|
<div className="mb-16 overflow-x-auto">
|
||||||
@@ -106,7 +107,7 @@ export default function Drivers() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-slate-200 mb-4">{t.drivers.napalmHeading}</h2>
|
<h2 className="text-xl font-semibold text-slate-200 mb-4">{t.drivers.napalmHeading}</h2>
|
||||||
<p className="text-sm text-slate-400 mb-6">{t.drivers.napalmSub}</p>
|
<p className="text-sm text-slate-400 mb-6">{linkify(t.drivers.napalmSub)}</p>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{NAPALM_BUILTIN.map((name) => (
|
{NAPALM_BUILTIN.map((name) => (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useLang } from '../context/LangContext'
|
import { useLang } from '../context/LangContext'
|
||||||
|
import { linkify } from '../lib/glossary'
|
||||||
|
|
||||||
function Section({ title, items }: { title: string; items: string[] }) {
|
function Section({ title, items }: { title: string; items: string[] }) {
|
||||||
return (
|
return (
|
||||||
@@ -10,7 +11,7 @@ function Section({ title, items }: { title: string; items: string[] }) {
|
|||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<li key={item} className="flex items-start gap-2 text-sm text-slate-400 leading-relaxed">
|
<li key={item} className="flex items-start gap-2 text-sm text-slate-400 leading-relaxed">
|
||||||
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
|
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
|
||||||
{item}
|
{linkify(item)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
import { useLang } from '../context/LangContext'
|
||||||
|
import { GLOSSARY, CATEGORY_ORDER, CATEGORY_LABELS } from '../glossary/terms'
|
||||||
|
|
||||||
|
export default function Glossary() {
|
||||||
|
const { lang, t } = useLang()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = location.hash.slice(1)
|
||||||
|
if (!id) return
|
||||||
|
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
}, [location.hash])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-16 md:py-24">
|
||||||
|
<div className="max-w-4xl mx-auto px-6">
|
||||||
|
<div className="mb-16">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.glossary.heading}</h1>
|
||||||
|
<p className="text-base text-slate-400 leading-relaxed max-w-2xl">{t.glossary.sub}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-14">
|
||||||
|
{CATEGORY_ORDER.map((category) => {
|
||||||
|
const entries = GLOSSARY.filter((e) => e.category === category).sort((a, b) =>
|
||||||
|
a.display.localeCompare(b.display),
|
||||||
|
)
|
||||||
|
if (entries.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={category}>
|
||||||
|
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
|
||||||
|
{CATEGORY_LABELS[category][lang]}
|
||||||
|
</h2>
|
||||||
|
<dl className="space-y-6">
|
||||||
|
{entries.map((entry) => (
|
||||||
|
<div key={entry.id} id={entry.id} className="scroll-mt-24">
|
||||||
|
<dt className="flex flex-wrap items-baseline gap-2 mb-1">
|
||||||
|
<span className="font-mono text-sm font-semibold text-sky-400">{entry.display}</span>
|
||||||
|
{entry.fullName && (
|
||||||
|
<span className="text-sm text-slate-300">{entry.fullName[lang]}</span>
|
||||||
|
)}
|
||||||
|
</dt>
|
||||||
|
<dd className="text-sm text-slate-500 leading-relaxed">{entry.definition[lang]}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+15
-14
@@ -7,6 +7,7 @@ import {
|
|||||||
ShieldCheckIcon,
|
ShieldCheckIcon,
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
import { useLang } from '../context/LangContext'
|
import { useLang } from '../context/LangContext'
|
||||||
|
import { linkify } from '../lib/glossary'
|
||||||
|
|
||||||
const DRIVERS = [
|
const DRIVERS = [
|
||||||
'Arista EOS', 'Cisco IOS', 'Cisco IOS-XE', 'Cisco NX-OS', 'Fritz!Box',
|
'Arista EOS', 'Cisco IOS', 'Cisco IOS-XE', 'Cisco NX-OS', 'Fritz!Box',
|
||||||
@@ -290,7 +291,7 @@ export default function Home() {
|
|||||||
<span className="text-sky-400">{h.hero.line2}</span>
|
<span className="text-sky-400">{h.hero.line2}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mx-auto mb-10">
|
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mx-auto mb-10">
|
||||||
{h.hero.sub}
|
{linkify(h.hero.sub)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-4 justify-center">
|
<div className="flex flex-wrap gap-4 justify-center">
|
||||||
<a
|
<a
|
||||||
@@ -319,7 +320,7 @@ export default function Home() {
|
|||||||
{/* 2 — Problem Statement */}
|
{/* 2 — Problem Statement */}
|
||||||
<section className="py-16">
|
<section className="py-16">
|
||||||
<div className="max-w-3xl mx-auto px-6 text-center">
|
<div className="max-w-3xl mx-auto px-6 text-center">
|
||||||
<p className="text-base text-slate-400 leading-relaxed">{h.problem}</p>
|
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.problem)}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -334,21 +335,21 @@ export default function Home() {
|
|||||||
<MagnifyingGlassIcon className="h-5 w-5 text-sky-400" />
|
<MagnifyingGlassIcon className="h-5 w-5 text-sky-400" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mb-2 text-lg font-semibold text-slate-100">{h.cap1.title}</h3>
|
<h3 className="mb-2 text-lg font-semibold text-slate-100">{h.cap1.title}</h3>
|
||||||
<p className="text-sm text-slate-400 leading-relaxed">{h.cap1.body}</p>
|
<p className="text-sm text-slate-400 leading-relaxed">{linkify(h.cap1.body)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
|
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
|
||||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
|
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
|
||||||
<ChartBarIcon className="h-5 w-5 text-sky-400" />
|
<ChartBarIcon className="h-5 w-5 text-sky-400" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mb-2 text-lg font-semibold text-slate-100">{h.cap2.title}</h3>
|
<h3 className="mb-2 text-lg font-semibold text-slate-100">{h.cap2.title}</h3>
|
||||||
<p className="text-sm text-slate-400 leading-relaxed">{h.cap2.body}</p>
|
<p className="text-sm text-slate-400 leading-relaxed">{linkify(h.cap2.body)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
|
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
|
||||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
|
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
|
||||||
<WrenchScrewdriverIcon className="h-5 w-5 text-sky-400" />
|
<WrenchScrewdriverIcon className="h-5 w-5 text-sky-400" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mb-2 text-lg font-semibold text-slate-100">{h.cap3.title}</h3>
|
<h3 className="mb-2 text-lg font-semibold text-slate-100">{h.cap3.title}</h3>
|
||||||
<p className="text-sm text-slate-400 leading-relaxed">{h.cap3.body}</p>
|
<p className="text-sm text-slate-400 leading-relaxed">{linkify(h.cap3.body)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -360,7 +361,7 @@ export default function Home() {
|
|||||||
<section className="py-24">
|
<section className="py-24">
|
||||||
<div className="max-w-7xl mx-auto px-6 text-center">
|
<div className="max-w-7xl mx-auto px-6 text-center">
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.driversHeading}</h2>
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.driversHeading}</h2>
|
||||||
<p className="text-base text-slate-400 leading-relaxed max-w-xl mx-auto mb-10">{h.driversSub}</p>
|
<p className="text-base text-slate-400 leading-relaxed max-w-xl mx-auto mb-10">{linkify(h.driversSub)}</p>
|
||||||
<div className="flex flex-wrap justify-center gap-3">
|
<div className="flex flex-wrap justify-center gap-3">
|
||||||
{DRIVERS.map((d) => (
|
{DRIVERS.map((d) => (
|
||||||
<span key={d} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full border border-slate-700 bg-slate-900 text-xs font-medium text-slate-300">
|
<span key={d} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full border border-slate-700 bg-slate-900 text-xs font-medium text-slate-300">
|
||||||
@@ -384,7 +385,7 @@ export default function Home() {
|
|||||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot1.heading}</h2>
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot1.heading}</h2>
|
||||||
<p className="text-base text-slate-400 leading-relaxed">{h.screenshot1.body}</p>
|
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.screenshot1.body)}</p>
|
||||||
</div>
|
</div>
|
||||||
<BrowserFrame label="netork.local / devices / ap-001">
|
<BrowserFrame label="netork.local / devices / ap-001">
|
||||||
<MockDeviceDetail />
|
<MockDeviceDetail />
|
||||||
@@ -396,13 +397,13 @@ export default function Home() {
|
|||||||
</BrowserFrame>
|
</BrowserFrame>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot2.heading}</h2>
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot2.heading}</h2>
|
||||||
<p className="text-base text-slate-400 leading-relaxed">{h.screenshot2.body}</p>
|
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.screenshot2.body)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot3.heading}</h2>
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot3.heading}</h2>
|
||||||
<p className="text-base text-slate-400 leading-relaxed">{h.screenshot3.body}</p>
|
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.screenshot3.body)}</p>
|
||||||
</div>
|
</div>
|
||||||
<BrowserFrame label="netork.local / devices / ap-001 / security">
|
<BrowserFrame label="netork.local / devices / ap-001 / security">
|
||||||
<MockSecurity />
|
<MockSecurity />
|
||||||
@@ -414,7 +415,7 @@ export default function Home() {
|
|||||||
</BrowserFrame>
|
</BrowserFrame>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot4.heading}</h2>
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot4.heading}</h2>
|
||||||
<p className="text-base text-slate-400 leading-relaxed">{h.screenshot4.body}</p>
|
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.screenshot4.body)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -429,7 +430,7 @@ export default function Home() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold tracking-widest uppercase text-sky-500 mb-3">{h.nis2Label}</p>
|
<p className="text-xs font-semibold tracking-widest uppercase text-sky-500 mb-3">{h.nis2Label}</p>
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.nis2Heading}</h2>
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.nis2Heading}</h2>
|
||||||
<p className="text-base text-slate-400 leading-relaxed mb-8">{h.nis2Body}</p>
|
<p className="text-base text-slate-400 leading-relaxed mb-8">{linkify(h.nis2Body)}</p>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<Link to="/nis2" className="inline-flex items-center gap-2 text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
|
<Link to="/nis2" className="inline-flex items-center gap-2 text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
|
||||||
<ShieldCheckIcon className="h-4 w-4" />
|
<ShieldCheckIcon className="h-4 w-4" />
|
||||||
@@ -442,7 +443,7 @@ export default function Home() {
|
|||||||
<span className="mt-0.5 shrink-0 w-28 text-xs font-mono text-sky-500">{m.art}</span>
|
<span className="mt-0.5 shrink-0 w-28 text-xs font-mono text-sky-500">{m.art}</span>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-slate-200">{m.label}</p>
|
<p className="text-sm font-medium text-slate-200">{m.label}</p>
|
||||||
<p className="text-xs text-slate-500 mt-0.5">{m.detail}</p>
|
<p className="text-xs text-slate-500 mt-0.5">{linkify(m.detail)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -465,7 +466,7 @@ export default function Home() {
|
|||||||
<PuzzlePieceIcon className="h-5 w-5 text-sky-400" />
|
<PuzzlePieceIcon className="h-5 w-5 text-sky-400" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.pluginsHeading}</h2>
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.pluginsHeading}</h2>
|
||||||
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mb-6">{h.pluginsBody}</p>
|
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mb-6">{linkify(h.pluginsBody)}</p>
|
||||||
<Link to="/plugins" className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
|
<Link to="/plugins" className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
|
||||||
{h.pluginsLink}
|
{h.pluginsLink}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -481,7 +482,7 @@ export default function Home() {
|
|||||||
<div className="grid md:grid-cols-2 gap-12 items-start">
|
<div className="grid md:grid-cols-2 gap-12 items-start">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.deployHeading}</h2>
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.deployHeading}</h2>
|
||||||
<p className="text-base text-slate-400 leading-relaxed">{h.deployBody}</p>
|
<p className="text-base text-slate-400 leading-relaxed">{linkify(h.deployBody)}</p>
|
||||||
</div>
|
</div>
|
||||||
<pre className="rounded-xl border border-slate-800 bg-slate-900 p-6 font-mono text-sm text-slate-300 overflow-x-auto">
|
<pre className="rounded-xl border border-slate-800 bg-slate-900 p-6 font-mono text-sm text-slate-300 overflow-x-auto">
|
||||||
<code>{`# Clone + configure
|
<code>{`# Clone + configure
|
||||||
|
|||||||
+6
-5
@@ -1,6 +1,7 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ShieldCheckIcon } from '@heroicons/react/24/outline'
|
import { ShieldCheckIcon } from '@heroicons/react/24/outline'
|
||||||
import { useLang } from '../context/LangContext'
|
import { useLang } from '../context/LangContext'
|
||||||
|
import { linkify } from '../lib/glossary'
|
||||||
|
|
||||||
type Coverage = 'covered' | 'partial' | 'roadmap' | 'na'
|
type Coverage = 'covered' | 'partial' | 'roadmap' | 'na'
|
||||||
type Requirement = { article: string; label: string; coverage: Coverage; netork: string }
|
type Requirement = { article: string; label: string; coverage: Coverage; netork: string }
|
||||||
@@ -147,7 +148,7 @@ export default function Nis2() {
|
|||||||
<ShieldCheckIcon className="h-5 w-5 text-sky-400" />
|
<ShieldCheckIcon className="h-5 w-5 text-sky-400" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.nis2.heading}</h1>
|
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.nis2.heading}</h1>
|
||||||
<p className="text-base text-slate-400 leading-relaxed max-w-2xl">{t.nis2.sub}</p>
|
<p className="text-base text-slate-400 leading-relaxed max-w-2xl">{linkify(t.nis2.sub)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-20">
|
<div className="mb-20">
|
||||||
@@ -164,7 +165,7 @@ export default function Nis2() {
|
|||||||
</div>
|
</div>
|
||||||
<CoverageTag coverage={r.coverage} labels={coverageLabels} />
|
<CoverageTag coverage={r.coverage} labels={coverageLabels} />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-500 leading-relaxed md:pl-24">{r.netork}</p>
|
<p className="text-sm text-slate-500 leading-relaxed md:pl-24">{linkify(r.netork)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -182,7 +183,7 @@ export default function Nis2() {
|
|||||||
<h2 className="text-xl font-semibold text-slate-200 mb-2 pb-2 border-b border-slate-800">
|
<h2 className="text-xl font-semibold text-slate-200 mb-2 pb-2 border-b border-slate-800">
|
||||||
{t.nis2.evidenceHeading}
|
{t.nis2.evidenceHeading}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-slate-500 mb-6">{t.nis2.evidenceSub}</p>
|
<p className="text-sm text-slate-500 mb-6">{linkify(t.nis2.evidenceSub)}</p>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{evidence.map((e) => (
|
{evidence.map((e) => (
|
||||||
<div key={e.trigger} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
|
<div key={e.trigger} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
|
||||||
@@ -191,7 +192,7 @@ export default function Nis2() {
|
|||||||
{e.produces.map((item) => (
|
{e.produces.map((item) => (
|
||||||
<li key={item} className="flex items-start gap-2 text-sm text-slate-400">
|
<li key={item} className="flex items-start gap-2 text-sm text-slate-400">
|
||||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
|
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
|
||||||
{item}
|
{linkify(item)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -210,7 +211,7 @@ export default function Nis2() {
|
|||||||
<div className="mt-2 shrink-0 h-1.5 w-1.5 rounded-full bg-sky-500" />
|
<div className="mt-2 shrink-0 h-1.5 w-1.5 rounded-full bg-sky-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-slate-200">{item.title}</p>
|
<p className="text-sm font-medium text-slate-200">{item.title}</p>
|
||||||
<p className="text-sm text-slate-500 mt-0.5">{item.detail}</p>
|
<p className="text-sm text-slate-500 mt-0.5">{linkify(item.detail)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { PuzzlePieceIcon } from '@heroicons/react/24/outline'
|
import { PuzzlePieceIcon } from '@heroicons/react/24/outline'
|
||||||
import { useLang } from '../context/LangContext'
|
import { useLang } from '../context/LangContext'
|
||||||
|
import { linkify } from '../lib/glossary'
|
||||||
|
|
||||||
type Building = { title: string; body: string }
|
type Building = { title: string; body: string }
|
||||||
type BuiltinPlugin = { label: string; body: string; hosts: string[]; hooks: string[] }
|
type BuiltinPlugin = { label: string; body: string; hosts: string[]; hooks: string[] }
|
||||||
@@ -145,7 +146,7 @@ export default function Plugins() {
|
|||||||
<PuzzlePieceIcon className="h-5 w-5 text-sky-400" />
|
<PuzzlePieceIcon className="h-5 w-5 text-sky-400" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.plugins.heading}</h1>
|
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.plugins.heading}</h1>
|
||||||
<p className="text-base text-slate-400 leading-relaxed max-w-2xl">{t.plugins.sub}</p>
|
<p className="text-base text-slate-400 leading-relaxed max-w-2xl">{linkify(t.plugins.sub)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-20">
|
<div className="mb-20">
|
||||||
@@ -156,7 +157,7 @@ export default function Plugins() {
|
|||||||
{blocks.map((b) => (
|
{blocks.map((b) => (
|
||||||
<div key={b.title} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
|
<div key={b.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">{b.title}</p>
|
<p className="text-sm font-semibold text-slate-200 mb-1.5">{b.title}</p>
|
||||||
<p className="text-sm text-slate-500 leading-relaxed">{b.body}</p>
|
<p className="text-sm text-slate-500 leading-relaxed">{linkify(b.body)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +171,7 @@ export default function Plugins() {
|
|||||||
{builtin.map((p) => (
|
{builtin.map((p) => (
|
||||||
<div key={p.label} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
|
<div key={p.label} 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.label}</p>
|
<p className="text-sm font-semibold text-slate-200 mb-1.5">{p.label}</p>
|
||||||
<p className="text-sm text-slate-500 leading-relaxed mb-3">{p.body}</p>
|
<p className="text-sm text-slate-500 leading-relaxed mb-3">{linkify(p.body)}</p>
|
||||||
{(p.hosts.length > 0 || p.hooks.length > 0) && (
|
{(p.hosts.length > 0 || p.hooks.length > 0) && (
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{p.hosts.map((h) => <HostTag key={h} value={h} />)}
|
{p.hosts.map((h) => <HostTag key={h} value={h} />)}
|
||||||
@@ -194,7 +195,7 @@ export default function Plugins() {
|
|||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-slate-200">{s.title}</p>
|
<p className="text-sm font-medium text-slate-200">{s.title}</p>
|
||||||
<p className="text-sm text-slate-500 leading-relaxed">{s.body}</p>
|
<p className="text-sm text-slate-500 leading-relaxed">{linkify(s.body)}</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -213,8 +214,8 @@ export default function Plugins() {
|
|||||||
{hookKinds.map((h) => (
|
{hookKinds.map((h) => (
|
||||||
<div key={h.name} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
|
<div key={h.name} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
|
||||||
<p className="text-sm font-mono font-semibold text-sky-400 mb-1.5">{h.name}</p>
|
<p className="text-sm font-mono font-semibold text-sky-400 mb-1.5">{h.name}</p>
|
||||||
<p className="text-sm text-slate-400 leading-relaxed mb-2">{h.behavior}</p>
|
<p className="text-sm text-slate-400 leading-relaxed mb-2">{linkify(h.behavior)}</p>
|
||||||
<p className="text-sm text-slate-500 leading-relaxed">{h.use}</p>
|
<p className="text-sm text-slate-500 leading-relaxed">{linkify(h.use)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -226,7 +227,7 @@ export default function Plugins() {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{registryParagraphs.map((p) => (
|
{registryParagraphs.map((p) => (
|
||||||
<p key={p} className="text-sm text-slate-400 leading-relaxed">{p}</p>
|
<p key={p} className="text-sm text-slate-400 leading-relaxed">{linkify(p)}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useLang } from '../context/LangContext'
|
import { useLang } from '../context/LangContext'
|
||||||
|
import { linkify } from '../lib/glossary'
|
||||||
|
|
||||||
type Item = { title: string; detail: string; nis2?: boolean }
|
type Item = { title: string; detail: string; nis2?: boolean }
|
||||||
type Group = { label: string; items: Item[] }
|
type Group = { label: string; items: Item[] }
|
||||||
@@ -193,7 +194,7 @@ export default function Roadmap() {
|
|||||||
<span className="text-sm font-medium text-slate-200">{item.title}</span>
|
<span className="text-sm font-medium text-slate-200">{item.title}</span>
|
||||||
{item.nis2 && <Nis2Badge />}
|
{item.nis2 && <Nis2Badge />}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-500 leading-relaxed">{item.detail}</p>
|
<p className="text-sm text-slate-500 leading-relaxed">{linkify(item.detail)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user