feat: add site-wide glossary with hover tooltips
CI / TypeScript — type-check (push) Successful in 8s
CI / Publish — build & push image (push) Successful in 8s
CI / Deploy — pull & restart on host (push) Successful in 2s

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:
Christian Manivong
2026-07-02 18:37:35 +02:00
co-authored by Claude Sonnet 5
parent 2a8a87bdd1
commit 38c7fb1db9
14 changed files with 758 additions and 31 deletions
+2
View File
@@ -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 />
+1
View File
@@ -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>
+31
View File
@@ -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>
)
}
+8 -1
View File
@@ -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>
+579
View File
@@ -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
View File
@@ -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 }
+33
View File
@@ -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>
)
})
}
+3 -2
View File
@@ -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
+2 -1
View File
@@ -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>
+56
View File
@@ -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
View File
@@ -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
View File
@@ -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>
))} ))}
+8 -7
View File
@@ -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>
+2 -1
View File
@@ -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>
))} ))}