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:
1 parent
2705f1c889
commit
80ab0f491a
4 files changed
+189
-35
No files matched your search
@@ -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
@@ -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 läuft.
|
||||
Moderne, durchdachte All-in-One-Software für Branchen, die zu
|
||||
lange übersehen 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 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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user