Hero-Texte an Außenkommunikationspaket angepasst

Hero-Headline auf die offizielle Underline gesetzt (moderne, durch-
dachte All-in-One-Software für übersehene Branchen), Untertext aus
One-Liner und Elevator-Pitch mit UI/UX-USP. Headline-Größe und
Meta-Description entsprechend angepasst.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ToolCTRLandClaude Opus 4.7 committed 2026-05-22 21:56:28 +02:00
1 parent 2705f1c889
commit 80ab0f491a
4 files changed
+189 -35

No files matched your search

+74 -23
View File
@@ -1,8 +1,9 @@
# ToolCTRL — Landing Page
# ToolCTRL — Website
Eine-Seiten-Website im Visitenkarten-Stil für den Messeauftritt: Landing Page
plus Lead-Formular zur Kontaktsammlung. Monochrom (weiß / schwarz), an die
ToolCTRL Brand Identity angelehnt, ohne externe Abhängigkeiten.
Mehrseitige Website im Visitenkarten-Stil: Landing Page mit Lead-Formular,
TechDay-2026-Seite, AI-GameJam-Seite mit Datei-Upload sowie Impressum und
Datenschutz. Monochrom (weiß / schwarz), an die ToolCTRL Brand Identity
angelehnt. Der Server läuft ohne npm-Pakete (nur Node-Standardbibliothek).
## Starten
@@ -10,35 +11,85 @@ ToolCTRL Brand Identity angelehnt, ohne externe Abhängigkeiten.
node server.js
```
Der Server sucht automatisch einen freien Port (zuerst `4317`) und gibt die
URL aus, z. B. `http://127.0.0.1:4317`. Stoppen mit `Strg + C`.
Der Server sucht automatisch einen freien Port (zuerst `4317`) und gibt lokale
und Netzwerk-URL aus. Stoppen mit `Strg + C`.
Voraussetzung: Node.js (getestet mit v26). Keine Installation, keine `npm`-Pakete.
Voraussetzung: Node.js (getestet mit v26).
## Aufbau
```
public/ statische Website (HTML, CSS, JS, Assets)
index.html Landing Page + Kontaktformular
styles.css Designsystem
script.js Formular-Logik & Mobile-Navigation
assets/ Logos, Emblem, Favicon, Icon-Set
server.js abhängigkeitsfreier Node-HTTP-Server + Lead-Endpunkt
leads/ gesammelte Anfragen (wird automatisch angelegt)
leads.csv Anfragen als CSV — direkt in Excel öffenbar
leads.ndjson dieselben Anfragen als JSON pro Zeile
public/ statische Website
index.html Landing Page + Kontaktformular
techday.html TechDay Bruchsal 2026
gamejam.html AI GameJam (eigenes Styling, Spiel-Upload)
impressum.html Impressum
datenschutz.html Datenschutzerklärung
styles.css Designsystem
script.js Formular-Logik & Mobile-Navigation
assets/ Logos, Emblem, Favicon, Icons, Schriften, Mockups
server.js abhängigkeitsfreier Node-HTTP-Server + API
leads/ gesammelte Kontaktanfragen (wird automatisch angelegt)
gamejam/ GameJam-Einreichungen, Metadaten (wird automatisch angelegt)
```
`leads/`, `gamejam/` und hochgeladene Spiele sind per `.gitignore` vom Repo
ausgeschlossen.
## Leads
Jede abgesendete Anfrage landet sofort lokal auf der Platte — in `leads.csv`
**und** `leads.ndjson`. So geht auf der Messe keine Anfrage verloren, auch
ohne Internet. Ein Honeypot-Feld fängt einfache Spam-Bots ab.
`POST /api/lead` (JSON, Pflichtfelder `name`, `email`, `nachricht`). Jede
Anfrage landet sofort lokal in `leads/leads.csv` **und** `leads.ndjson` — so
geht auf der Messe nichts verloren. Ein Honeypot-Feld fängt einfache Bots ab.
Endpunkt: `POST /api/lead` (JSON). Pflichtfelder: `name`, `email`, `nachricht`.
## GameJam-Upload & Virenscan (ClamAV)
`POST /api/gamejam/upload` nimmt eine `.html`-Datei entgegen (max. 5 MB).
**Jede hochgeladene Datei wird vor dem Speichern per ClamAV auf Viren
geprüft.** Befund → Ablehnung; ist der Scanner nicht erreichbar, wird der
Upload ebenfalls abgelehnt (fail-closed, kein ungeprüftes Speichern).
Der Server spricht den ClamAV-Daemon `clamd` direkt über sein Socket-Protokoll
an — keine zusätzliche Abhängigkeit. Voraussetzung: ClamAV ist installiert und
`clamd` läuft. Beim Start meldet der Server `Virenscan: aktiv` bzw. eine
Warnung, falls `clamd` nicht erreichbar ist.
### ClamAV einrichten — macOS
```bash
brew install clamav
cd "$(brew --prefix)/etc/clamav"
cp freshclam.conf.sample freshclam.conf
cp clamd.conf.sample clamd.conf
# In BEIDEN Dateien die Zeile "Example" entfernen oder auskommentieren.
# In clamd.conf zusätzlich diese zwei Zeilen aktivieren (Raute entfernen):
# TCPSocket 3310
# TCPAddr 127.0.0.1
freshclam # Virendefinitionen laden (einmalig)
clamd # Daemon starten
```
### ClamAV einrichten — Linux (Debian/Ubuntu)
```bash
sudo apt install clamav clamav-daemon
# In /etc/clamav/clamd.conf: TCPSocket 3310 und TCPAddr 127.0.0.1 setzen
sudo systemctl restart clamav-daemon
```
### Konfiguration
Host/Port und An/Aus stehen in `server.js` im Objekt `CLAMAV`. Mit
`enabled: false` läuft der Upload ohne Virenscan (nur für lokale Tests
ohne ClamAV gedacht).
### Funktion testen
Eine Datei mit der EICAR-Testsignatur hochladen — `clamd` meldet dann
`Eicar-Test-Signature FOUND`, der Upload wird abgelehnt.
## Hinweis für die Messe
Für den QR-Code am Stand sollte der Rechner mit dem Server im selben WLAN
erreichbar sein. Soll die Seite öffentlich laufen, hinter einen Reverse-Proxy
mit HTTPS setzen oder bei einem Hoster bereitstellen — `tool-ctrl.de`.
Für den QR-Code am Stand muss der Rechner im selben WLAN erreichbar sein.
Soll die Seite öffentlich laufen, hinter einen Reverse-Proxy mit HTTPS setzen
oder bei einem Hoster bereitstellen — `tool-ctrl.de`.
+7 -7
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ToolCTRL — from vision to version</title>
<meta name="description" content="ToolCTRL entwickelt Automatisierungen, interne Tools und individuelle Software — schnell umgesetzt, zuverlässig im Betrieb." />
<meta name="description" content="ToolCTRL baut moderne, durchdachte All-in-One-Software für Branchen, die zu lange mit veralteten Insellösungen auskommen mussten. Mit Fokus auf Bedienung und Gestaltung." />
<link rel="icon" type="image/png" href="/assets/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="/styles.css" />
@@ -46,14 +46,14 @@
<div class="wrap hero-grid">
<div class="hero-copy">
<h1 class="hero-title">
Software und Automatisierung,<br />
die einfach&nbsp;läuft.
Moderne, durchdachte All-in-One-Software für Branchen, die zu
lange übersehen&nbsp;wurden.
</h1>
<p class="hero-lead">
ToolCTRL plant, baut und betreibt individuelle Software — von der
Prozessautomatisierung über interne Tools bis zur Integration
bestehender Systeme. Schnell umgesetzt, sauber konstruiert,
zuverlässig im&nbsp;Betrieb.
Ganze Branchen arbeiten noch mit veralteten, zusammengestückelten
Insellösungen. Wir bei ToolCTRL bauen die Alternative: Software mit
echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert,
klar aussieht und sich schnell bedienen lässt.
</p>
<div class="hero-actions">
<a class="btn btn-solid" href="#kontakt">Projekt besprechen</a>
+3 -3
View File
@@ -283,9 +283,9 @@ a { color: inherit; text-decoration: none; }
padding: 96px 32px 104px;
}
.hero-title {
font-size: clamp(36px, 4.6vw, 62px);
line-height: 1.05;
letter-spacing: -.022em;
font-size: clamp(30px, 3.6vw, 48px);
line-height: 1.12;
letter-spacing: -.02em;
font-weight: 700;
margin: 0 0 24px;
}
+105 -2
View File
@@ -3,7 +3,8 @@
Abhängigkeitsfrei (nur Node-Standardbibliothek).
- Liefert /public
- POST /api/lead — Kontakt-Leads
- POST /api/gamejam/upload — GameJam: Spiel einreichen (Datei-Upload)
- POST /api/gamejam/upload — GameJam: Spiel einreichen (Datei-Upload,
wird per ClamAV auf Viren geprüft)
- GET /api/gamejam/games — GameJam: eingereichte Spiele
============================================================ */
@@ -14,6 +15,7 @@ const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const os = require("os");
const net = require("net");
const ROOT = __dirname;
const PUBLIC_DIR = path.join(ROOT, "public");
@@ -27,6 +29,17 @@ const SUBMISSIONS_JSON = path.join(GAMEJAM_DIR, "submissions.ndjson");
const GAMES_DIR = path.join(PUBLIC_DIR, "gamejam-games");
const MAX_UPLOAD = 5 * 1024 * 1024; /* 5 MB pro eingereichtem Spiel */
/* Virenscan über ClamAV. Der Server spricht das clamd-Socket-Protokoll
direkt — keine zusätzliche Abhängigkeit. Voraussetzung: ClamAV ist
installiert und der Daemon "clamd" läuft (Einrichtung siehe README).
enabled:false schaltet den Scan ab (z. B. lokal ohne ClamAV). */
const CLAMAV = {
enabled: true,
host: "127.0.0.1",
port: 3310, /* clamd TCPSocket-Standardport */
timeoutMs: 15000
};
/* Bevorzugte Ports — der erste freie wird genutzt. */
const PORT_CANDIDATES = [4317, 4318, 4417, 5317, 8317, 0];
/* 0.0.0.0 = auf allen Schnittstellen erreichbar, auch für andere
@@ -235,6 +248,65 @@ function parseMultipart(buffer, boundary) {
return result;
}
/* ---------- Virenscan via ClamAV (clamd INSTREAM) ---------- */
function scanWithClamAV(buffer) {
return new Promise((resolve, reject) => {
const socket = net.createConnection({ host: CLAMAV.host, port: CLAMAV.port });
let reply = "";
let settled = false;
const done = (fn, arg) => {
if (settled) return;
settled = true;
socket.destroy();
fn(arg);
};
socket.setTimeout(CLAMAV.timeoutMs);
socket.on("timeout", () => done(reject, new Error("clamd-Zeitüberschreitung")));
socket.on("error", (e) => done(reject, e));
socket.on("data", (d) => { reply += d.toString("utf8"); });
socket.on("end", () => {
const r = reply.replace(/\0/g, "").trim();
if (/\bOK$/.test(r)) return done(resolve, { clean: true, signature: null });
const m = r.match(/:\s*(.+)\s+FOUND$/);
if (m) return done(resolve, { clean: false, signature: m[1] });
done(reject, new Error("Unerwartete clamd-Antwort: " + r));
});
socket.on("connect", () => {
socket.write("zINSTREAM\0");
const CHUNK = 64 * 1024;
for (let i = 0; i < buffer.length; i += CHUNK) {
const part = buffer.subarray(i, i + CHUNK);
const len = Buffer.alloc(4);
len.writeUInt32BE(part.length, 0);
socket.write(len);
socket.write(part);
}
socket.write(Buffer.alloc(4)); /* 0-Länge = Stream-Ende */
});
});
}
/* Verfügbarkeits-Check: clamd PING -> PONG */
function clamavPing() {
return new Promise((resolve) => {
const socket = net.createConnection({ host: CLAMAV.host, port: CLAMAV.port });
let reply = "";
let settled = false;
const done = (v) => {
if (settled) return;
settled = true;
socket.destroy();
resolve(v);
};
socket.setTimeout(3000);
socket.on("timeout", () => done(false));
socket.on("error", () => done(false));
socket.on("data", (d) => { reply += d.toString("utf8"); });
socket.on("end", () => done(/PONG/.test(reply)));
socket.on("connect", () => socket.write("zPING\0"));
});
}
/* ---------- GameJam: Spiel einreichen ---------- */
function handleGamejamUpload(req, res) {
const ctype = req.headers["content-type"] || "";
@@ -258,7 +330,7 @@ function handleGamejamUpload(req, res) {
chunks.push(chunk);
});
req.on("end", () => {
req.on("end", async () => {
if (tooLarge) {
return sendJSON(res, 413, { error: "Datei zu groß (max. 5 MB)." });
}
@@ -284,6 +356,28 @@ function handleGamejamUpload(req, res) {
return sendJSON(res, 400, { error: "Nur einzelne .html-Dateien." });
}
/* Virenscan vor dem Speichern */
if (CLAMAV.enabled) {
let scan;
try {
scan = await scanWithClamAV(file.data);
} catch (e) {
console.error("[gamejam] Virenscan nicht möglich:", e.message);
return sendJSON(res, 503, {
error: "Sicherheitsprüfung derzeit nicht verfügbar. Bitte später erneut versuchen."
});
}
if (!scan.clean) {
console.warn(
"[gamejam] Upload ABGELEHNT — Fund: " + scan.signature +
" (Datei: " + (file.filename || "?") + ")"
);
return sendJSON(res, 422, {
error: "Die Datei wurde von der Sicherheitsprüfung abgelehnt."
});
}
}
const id = Date.now().toString(36) + "-" + crypto.randomBytes(4).toString("hex");
const entry = {
id: id,
@@ -408,6 +502,15 @@ function listen(candidates) {
}
console.log(" Leads: " + LEADS_CSV);
console.log(" GameJam-Einreichungen: " + SUBMISSIONS_JSON);
if (CLAMAV.enabled) {
clamavPing().then((ok) => {
console.log(" Virenscan: " + (ok
? "aktiv (clamd " + CLAMAV.host + ":" + CLAMAV.port + ")"
: "clamd NICHT erreichbar — GameJam-Uploads werden abgelehnt!"));
});
} else {
console.log(" Virenscan: deaktiviert (CLAMAV.enabled = false)");
}
console.log(" Stop: Strg + C");
console.log("");
});